Back to Blog
AuthenticationJWTOAuthSecurity

Authentication Strategies: JWT, Sessions, and OAuth

Choose the right authentication approach. Compare JWT tokens, server sessions, and OAuth for your application needs.

B
Bootspring Team
Engineering
December 28, 2023
5 min read

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.

Share this article

Help spread the word about Bootspring