Authentication Patterns
Authentication verifies who a user is. Three dominant patterns exist in modern backends: JWT tokens, server-side sessions, and OAuth 2.0 (delegated auth). Each has distinct trade-offs around scalability, security, and complexity.
JWT Flow
Flow:
- Login — User sends credentials (email + password)
- Verify — Server validates credentials against DB
- Sign Token — Server creates signed JWT with user claims
- Return Token — JWT sent to client in response body
- Attach Token — Client sends JWT in Authorization header
- Verify Token — Server verifies signature on each request
JWT Implementation
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
const JWT_SECRET = process.env.JWT_SECRET!;
const TOKEN_EXPIRY = '15m';
const REFRESH_EXPIRY = '7d';
interface TokenPair {
accessToken: string;
refreshToken: string;
}
export class AuthService {
constructor(private userRepo: UserRepository) {}
async login(email: string, password: string): Promise<TokenPair> {
const user = await this.userRepo.findByEmail(email);
if (!user) throw new Error('Invalid credentials');
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) throw new Error('Invalid credentials');
return this.generateTokens(user.id, user.email);
}
private generateTokens(userId: string, email: string): TokenPair {
const accessToken = jwt.sign(
{ sub: userId, email },
JWT_SECRET,
{ expiresIn: TOKEN_EXPIRY }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
JWT_SECRET,
{ expiresIn: REFRESH_EXPIRY }
);
return { accessToken, refreshToken };
}
verifyToken(token: string) {
return jwt.verify(token, JWT_SECRET) as { sub: string; email: string };
}
}
JWT Auth Middleware
import { Request, Response, NextFunction } from 'express';
import { AuthService } from './auth.service';
export function jwtAuth(authService: AuthService) {
return (req: Request, res: Response, next: NextFunction) => {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const token = header.split(' ')[1];
const payload = authService.verifyToken(token);
(req as any).userId = payload.sub;
(req as any).userEmail = payload.email;
next();
} catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
};
}
OAuth 2.0 Flow
Flow:
- User clicks Login — App redirects to Auth Server (Google, GitHub)
- Auth Server — User authenticates and grants consent
- Auth Code — Auth server redirects back with authorization code
- Exchange — App exchanges code for access token (server-side)
- Resource Server — App uses token to fetch user profile
Session-Based Flow
Flow:
- Login — User sends credentials
- Create Session — Server creates session in Redis/DB
- Set Cookie — Session ID sent as httpOnly cookie
- Subsequent Requests — Browser auto-sends cookie on each request
- Lookup Session — Server validates session ID in store
Comparison
| Aspect | JWT | Session | OAuth 2.0 |
|---|---|---|---|
| Storage | Client-side (token) | Server-side (Redis/DB) | Auth server + client token |
| Scalability | Stateless — scales easily | Requires shared session store | Depends on provider |
| Revocation | Hard (need deny-list) | Easy (delete session) | Managed by provider |
| Security | Vulnerable if secret leaks | CSRF protection needed | Most secure (delegated) |
| Complexity | Low | Medium | High (redirect flows) |
| Best for | APIs, microservices | Traditional web apps | Third-party login (SSO) |
CAUTION: Never store JWTs in
localStorage— they are vulnerable to XSS attacks. Use httpOnly cookies or keep tokens in memory with a refresh token in a cookie.
TIP: In practice, many apps combine patterns: OAuth for third-party login, then issue a JWT for API access, backed by a refresh token stored in a session.