Authentication Flows: JWT, OAuth, Sessions
Authentication flows are critical for securing applications. This article covers different authentication patterns.
Introduction
Authentication flows provide:
- User identity verification
- Secure token management
- Third-party integration
- Session management
- Access control
Understanding authentication flows helps you:
- Implement secure authentication
- Choose appropriate patterns
- Handle token refresh
- Integrate OAuth providers
- Manage user sessions
JWT Authentication Flow
JWT Flow Diagram
Client Server
| |
|--- POST /login -------> |
| (email, password) |
| |
|<----- JWT Token ------ |
| |
|--- GET /api/users -----> |
| (Authorization: Bearer JWT)|
| |
|<----- User Data ------ |
JWT Implementation
// โ
Good: JWT login flow
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create tokens
const accessToken = jwt.sign(
{ id: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ id: user._id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token
user.refreshToken = refreshToken;
await user.save();
res.json({
accessToken,
refreshToken,
expiresIn: 900 // 15 minutes
});
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// โ
Good: JWT verification middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
res.status(401).json({ error: 'Invalid token' });
}
};
// โ
Good: Refresh token endpoint
app.post('/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
// Verify refresh token
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// Find user and verify token
const user = await User.findById(decoded.id);
if (!user || user.refreshToken !== refreshToken) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Create new access token
const newAccessToken = jwt.sign(
{ id: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// โ
Good: Logout endpoint
app.post('/logout', authenticate, async (req, res) => {
try {
const user = await User.findById(req.user.id);
user.refreshToken = null;
await user.save();
res.json({ message: 'Logged out successfully' });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
OAuth 2.0 Flow
OAuth Flow Diagram
Client App Authorization Server
| |
|--- Redirect to login -----> |
| |
|<----- User logs in ----- |
| |
|<----- Authorization Code -- |
| |
|--- Exchange code for token --> |
| |
|<----- Access Token ----- |
| |
|--- Request user data -----> |
| |
|<----- User Data ----- |
OAuth Implementation
// โ
Good: OAuth with Passport
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
try {
// Find or create user
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = new User({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value
});
await user.save();
}
return done(null, user);
} catch (err) {
return done(err);
}
}));
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
// โ
Good: OAuth routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Create JWT token
const token = jwt.sign(
{ id: req.user._id, email: req.user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.redirect(`/dashboard?token=${token}`);
}
);
// โ
Good: OAuth logout
app.post('/logout', (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.json({ message: 'Logged out successfully' });
});
});
Session-Based Authentication Flow
Session Flow Diagram
Client Server
| |
|--- POST /login -------> |
| (email, password) |
| |
|<----- Set-Cookie: SID -- |
| |
|--- GET /api/users -----> |
| (Cookie: SID) |
| |
|<----- User Data ------ |
Session Implementation
// โ
Good: Session configuration
const session = require('express-session');
const MongoStore = require('connect-mongo');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: new MongoStore({
mongoUrl: process.env.MONGODB_URI,
touchAfter: 24 * 3600 // Lazy session update
}),
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 // 24 hours
}
}));
// โ
Good: Session login
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Store user in session
req.session.userId = user._id;
req.session.user = {
id: user._id,
email: user.email,
role: user.role
};
res.json({ message: 'Logged in successfully' });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// โ
Good: Session middleware
const requireLogin = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
next();
};
app.get('/api/profile', requireLogin, (req, res) => {
res.json({ user: req.session.user });
});
// โ
Good: Session logout
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.json({ message: 'Logged out successfully' });
});
});
Multi-Factor Authentication
MFA Implementation
// โ
Good: TOTP (Time-based One-Time Password)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate MFA secret
app.post('/mfa/setup', authenticate, async (req, res) => {
const secret = speakeasy.generateSecret({
name: `MyApp (${req.user.email})`,
issuer: 'MyApp'
});
// Generate QR code
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
res.json({
secret: secret.base32,
qrCode
});
});
// Verify MFA token
app.post('/mfa/verify', authenticate, async (req, res) => {
const { token, secret } = req.body;
const verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 2
});
if (!verified) {
return res.status(400).json({ error: 'Invalid token' });
}
// Save MFA secret
const user = await User.findByIdAndUpdate(
req.user.id,
{ mfaSecret: secret, mfaEnabled: true }
);
res.json({ message: 'MFA enabled' });
});
// โ
Good: Email-based MFA
app.post('/mfa/email', authenticate, async (req, res) => {
const code = Math.random().toString().slice(2, 8);
// Send email
await sendEmail(req.user.email, 'MFA Code', `Your code: ${code}`);
// Store code temporarily
await redis.setex(`mfa:${req.user.id}`, 300, code);
res.json({ message: 'Code sent to email' });
});
// Verify email MFA
app.post('/mfa/email/verify', authenticate, async (req, res) => {
const { code } = req.body;
const storedCode = await redis.get(`mfa:${req.user.id}`);
if (storedCode !== code) {
return res.status(400).json({ error: 'Invalid code' });
}
// Clear code
await redis.del(`mfa:${req.user.id}`);
res.json({ message: 'MFA verified' });
});
Best Practices
-
Use HTTPS:
// โ Good: HTTPS in production const https = require('https'); https.createServer(options, app).listen(443); // โ Bad: HTTP app.listen(3000); -
Secure token storage:
// โ Good: Store in httpOnly cookie res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'strict' }); // โ Bad: Store in localStorage localStorage.setItem('token', token); -
Implement rate limiting:
// โ Good: Rate limit login const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }); app.post('/login', loginLimiter, (req, res) => { // Login logic });
Summary
Authentication flows are essential. Key takeaways:
- Understand JWT flow
- Implement OAuth 2.0
- Use sessions appropriately
- Implement MFA
- Use HTTPS
- Secure token storage
- Rate limit authentication
- Handle token refresh
Related Resources
Next Steps
- Learn about Frontend-Backend Integration
- Explore Deployment
- Study Monitoring
- Practice authentication flows
- Build secure applications
Comments