Authentication verifies user identity. The right strategy depends on your application type, security requirements, and infrastructure. Here's how to choose.
Session-Based Authentication#
1// Express with express-session
2import session from 'express-session';
3import RedisStore from 'connect-redis';
4import { createClient } from 'redis';
5
6const redisClient = createClient();
7await redisClient.connect();
8
9app.use(session({
10 store: new RedisStore({ client: redisClient }),
11 secret: process.env.SESSION_SECRET,
12 resave: false,
13 saveUninitialized: false,
14 cookie: {
15 secure: process.env.NODE_ENV === 'production',
16 httpOnly: true,
17 maxAge: 24 * 60 * 60 * 1000, // 24 hours
18 sameSite: 'lax',
19 },
20}));
21
22// Login
23app.post('/login', async (req, res) => {
24 const { email, password } = req.body;
25 const user = await validateCredentials(email, password);
26
27 if (!user) {
28 return res.status(401).json({ error: 'Invalid credentials' });
29 }
30
31 req.session.userId = user.id;
32 req.session.role = user.role;
33
34 res.json({ user: { id: user.id, email: user.email } });
35});
36
37// Logout
38app.post('/logout', (req, res) => {
39 req.session.destroy((err) => {
40 if (err) {
41 return res.status(500).json({ error: 'Logout failed' });
42 }
43 res.clearCookie('connect.sid');
44 res.json({ message: 'Logged out' });
45 });
46});
47
48// Auth middleware
49function requireAuth(req, res, next) {
50 if (!req.session.userId) {
51 return res.status(401).json({ error: 'Authentication required' });
52 }
53 next();
54}Session Pros/Cons#
Pros:
✓ Easy to invalidate (delete from store)
✓ Server controls session data
✓ Smaller cookie size
✓ Can store complex data
Cons:
✗ Requires server storage
✗ Scaling needs shared store (Redis)
✗ Stateful - harder to distribute
JWT Authentication#
1import jwt from 'jsonwebtoken';
2
3const JWT_SECRET = process.env.JWT_SECRET;
4const ACCESS_TOKEN_EXPIRY = '15m';
5const REFRESH_TOKEN_EXPIRY = '7d';
6
7// Generate tokens
8function generateTokens(user: User) {
9 const accessToken = jwt.sign(
10 { userId: user.id, role: user.role },
11 JWT_SECRET,
12 { expiresIn: ACCESS_TOKEN_EXPIRY }
13 );
14
15 const refreshToken = jwt.sign(
16 { userId: user.id, tokenVersion: user.tokenVersion },
17 JWT_SECRET,
18 { expiresIn: REFRESH_TOKEN_EXPIRY }
19 );
20
21 return { accessToken, refreshToken };
22}
23
24// Login
25app.post('/login', async (req, res) => {
26 const { email, password } = req.body;
27 const user = await validateCredentials(email, password);
28
29 if (!user) {
30 return res.status(401).json({ error: 'Invalid credentials' });
31 }
32
33 const { accessToken, refreshToken } = generateTokens(user);
34
35 // Set refresh token as httpOnly cookie
36 res.cookie('refreshToken', refreshToken, {
37 httpOnly: true,
38 secure: process.env.NODE_ENV === 'production',
39 sameSite: 'strict',
40 maxAge: 7 * 24 * 60 * 60 * 1000,
41 });
42
43 res.json({ accessToken });
44});
45
46// Refresh token
47app.post('/refresh', async (req, res) => {
48 const { refreshToken } = req.cookies;
49
50 if (!refreshToken) {
51 return res.status(401).json({ error: 'No refresh token' });
52 }
53
54 try {
55 const payload = jwt.verify(refreshToken, JWT_SECRET);
56 const user = await db.user.findUnique({
57 where: { id: payload.userId },
58 });
59
60 // Check token version (for invalidation)
61 if (!user || user.tokenVersion !== payload.tokenVersion) {
62 return res.status(401).json({ error: 'Invalid token' });
63 }
64
65 const tokens = generateTokens(user);
66
67 res.cookie('refreshToken', tokens.refreshToken, {
68 httpOnly: true,
69 secure: true,
70 sameSite: 'strict',
71 });
72
73 res.json({ accessToken: tokens.accessToken });
74 } catch (error) {
75 res.status(401).json({ error: 'Invalid token' });
76 }
77});
78
79// Auth middleware
80function authenticateToken(req, res, next) {
81 const authHeader = req.headers.authorization;
82 const token = authHeader?.split(' ')[1];
83
84 if (!token) {
85 return res.status(401).json({ error: 'Token required' });
86 }
87
88 try {
89 const payload = jwt.verify(token, JWT_SECRET);
90 req.user = payload;
91 next();
92 } catch (error) {
93 res.status(401).json({ error: 'Invalid token' });
94 }
95}JWT Pros/Cons#
Pros:
✓ Stateless - no server storage
✓ Easy to scale horizontally
✓ Works across domains
✓ Contains user data
Cons:
✗ Can't easily invalidate
✗ Larger payload size
✗ Token theft risks
✗ Refresh token complexity
OAuth 2.0 / OpenID Connect#
1import { OAuth2Client } from 'google-auth-library';
2
3const googleClient = new OAuth2Client(
4 process.env.GOOGLE_CLIENT_ID,
5 process.env.GOOGLE_CLIENT_SECRET,
6 process.env.GOOGLE_REDIRECT_URI
7);
8
9// Generate OAuth URL
10app.get('/auth/google', (req, res) => {
11 const url = googleClient.generateAuthUrl({
12 access_type: 'offline',
13 scope: ['openid', 'email', 'profile'],
14 prompt: 'consent',
15 });
16
17 res.redirect(url);
18});
19
20// OAuth callback
21app.get('/auth/google/callback', async (req, res) => {
22 const { code } = req.query;
23
24 try {
25 const { tokens } = await googleClient.getToken(code);
26 googleClient.setCredentials(tokens);
27
28 // Verify ID token
29 const ticket = await googleClient.verifyIdToken({
30 idToken: tokens.id_token,
31 audience: process.env.GOOGLE_CLIENT_ID,
32 });
33
34 const payload = ticket.getPayload();
35 const { sub: googleId, email, name, picture } = payload;
36
37 // Find or create user
38 let user = await db.user.findUnique({
39 where: { googleId },
40 });
41
42 if (!user) {
43 user = await db.user.create({
44 data: { googleId, email, name, avatar: picture },
45 });
46 }
47
48 // Create session or JWT
49 const { accessToken, refreshToken } = generateTokens(user);
50
51 res.redirect(`/auth/success?token=${accessToken}`);
52 } catch (error) {
53 res.redirect('/auth/error');
54 }
55});Comparison Table#
| Feature | Sessions | JWT | OAuth |
|------------------|----------|----------|----------|
| Stateless | No | Yes | Depends |
| Revocation | Easy | Hard | Medium |
| Scaling | Redis | Easy | Easy |
| Cross-domain | Hard | Easy | Easy |
| Implementation | Simple | Medium | Complex |
| Third-party auth | No | No | Yes |
Security Best Practices#
1// Password hashing
2import bcrypt from 'bcrypt';
3
4const SALT_ROUNDS = 12;
5
6async function hashPassword(password: string): Promise<string> {
7 return bcrypt.hash(password, SALT_ROUNDS);
8}
9
10async function verifyPassword(password: string, hash: string): Promise<boolean> {
11 return bcrypt.compare(password, hash);
12}
13
14// Brute force protection
15import rateLimit from 'express-rate-limit';
16
17const loginLimiter = rateLimit({
18 windowMs: 15 * 60 * 1000,
19 max: 5,
20 skipSuccessfulRequests: true,
21 message: { error: 'Too many login attempts' },
22});
23
24app.post('/login', loginLimiter, loginHandler);
25
26// CSRF protection for sessions
27import csrf from 'csurf';
28
29const csrfProtection = csrf({ cookie: true });
30app.use(csrfProtection);When to Use What#
Use Sessions when:
- Traditional web app (server-rendered)
- Need easy session invalidation
- Single domain
- Can use Redis/Memcached
Use JWT when:
- API-first architecture
- Mobile apps
- Microservices
- Serverless functions
- Cross-domain requests
Use OAuth when:
- Third-party login (Google, GitHub)
- API access delegation
- Enterprise SSO integration
Conclusion#
There's no universally "best" authentication method. Sessions work great for traditional web apps, JWTs suit APIs and mobile apps, and OAuth enables third-party authentication.
Often the best approach combines methods—OAuth for social login, JWT for API access, with proper security measures throughout.