Back to Blog
AuthenticationAPISecurityOAuth

API Authentication Methods Compared

Choose the right auth method. From API keys to OAuth to JWTs with security trade-offs explained.

B
Bootspring Team
Engineering
April 28, 2022
6 min read

Different authentication methods suit different use cases. Here's how to choose and implement the right one.

API Keys#

1// Simple API key authentication 2// Best for: Server-to-server, simple integrations 3 4// Middleware 5function apiKeyAuth(req: Request, res: Response, next: NextFunction) { 6 const apiKey = req.headers['x-api-key']; 7 8 if (!apiKey) { 9 return res.status(401).json({ error: 'API key required' }); 10 } 11 12 const client = await db.apiKey.findUnique({ 13 where: { key: apiKey }, 14 include: { client: true }, 15 }); 16 17 if (!client || client.revoked) { 18 return res.status(401).json({ error: 'Invalid API key' }); 19 } 20 21 req.client = client.client; 22 next(); 23} 24 25// Generate API key 26function generateApiKey(): string { 27 return `sk_live_${crypto.randomBytes(32).toString('hex')}`; 28} 29 30// Store hashed 31async function createApiKey(clientId: string): Promise<string> { 32 const key = generateApiKey(); 33 const hash = await bcrypt.hash(key, 10); 34 35 await db.apiKey.create({ 36 data: { 37 keyHash: hash, 38 keyPrefix: key.slice(0, 12), // For identification 39 clientId, 40 }, 41 }); 42 43 return key; // Return once, never stored in plain text 44}
Pros: ✓ Simple to implement ✓ Easy to revoke ✓ Good for server-side use Cons: ✗ No expiration built-in ✗ Easy to leak in logs/URLs ✗ No user context

Bearer Tokens (JWT)#

1// JWT authentication 2// Best for: Stateless APIs, microservices 3 4import jwt from 'jsonwebtoken'; 5 6interface TokenPayload { 7 userId: string; 8 email: string; 9 role: string; 10} 11 12// Generate tokens 13function generateTokens(user: User) { 14 const accessToken = jwt.sign( 15 { userId: user.id, email: user.email, role: user.role }, 16 process.env.JWT_SECRET!, 17 { expiresIn: '15m' } 18 ); 19 20 const refreshToken = jwt.sign( 21 { userId: user.id, tokenVersion: user.tokenVersion }, 22 process.env.JWT_REFRESH_SECRET!, 23 { expiresIn: '7d' } 24 ); 25 26 return { accessToken, refreshToken }; 27} 28 29// Verify middleware 30function jwtAuth(req: Request, res: Response, next: NextFunction) { 31 const authHeader = req.headers.authorization; 32 33 if (!authHeader?.startsWith('Bearer ')) { 34 return res.status(401).json({ error: 'Token required' }); 35 } 36 37 const token = authHeader.slice(7); 38 39 try { 40 const payload = jwt.verify( 41 token, 42 process.env.JWT_SECRET! 43 ) as TokenPayload; 44 45 req.user = payload; 46 next(); 47 } catch (error) { 48 if (error.name === 'TokenExpiredError') { 49 return res.status(401).json({ error: 'Token expired' }); 50 } 51 return res.status(401).json({ error: 'Invalid token' }); 52 } 53} 54 55// Refresh token endpoint 56app.post('/auth/refresh', async (req, res) => { 57 const { refreshToken } = req.body; 58 59 try { 60 const payload = jwt.verify( 61 refreshToken, 62 process.env.JWT_REFRESH_SECRET! 63 ) as { userId: string; tokenVersion: number }; 64 65 const user = await db.user.findUnique({ 66 where: { id: payload.userId }, 67 }); 68 69 // Check token version for revocation 70 if (!user || user.tokenVersion !== payload.tokenVersion) { 71 return res.status(401).json({ error: 'Token revoked' }); 72 } 73 74 const tokens = generateTokens(user); 75 res.json(tokens); 76 } catch { 77 res.status(401).json({ error: 'Invalid refresh token' }); 78 } 79}); 80 81// Revoke all tokens for user 82async function revokeAllTokens(userId: string) { 83 await db.user.update({ 84 where: { id: userId }, 85 data: { tokenVersion: { increment: 1 } }, 86 }); 87}
Pros: ✓ Stateless (scalable) ✓ Contains user info ✓ Works across services Cons: ✗ Can't revoke individual tokens ✗ Token size can grow ✗ Must handle expiration

OAuth 2.0#

1// OAuth 2.0 with authorization code flow 2// Best for: Third-party integrations, social login 3 4import { OAuth2Client } from 'google-auth-library'; 5 6const googleClient = new OAuth2Client( 7 process.env.GOOGLE_CLIENT_ID, 8 process.env.GOOGLE_CLIENT_SECRET, 9 'http://localhost:3000/auth/google/callback' 10); 11 12// Redirect to Google 13app.get('/auth/google', (req, res) => { 14 const url = googleClient.generateAuthUrl({ 15 access_type: 'offline', 16 scope: ['profile', 'email'], 17 state: crypto.randomBytes(16).toString('hex'), 18 }); 19 20 res.redirect(url); 21}); 22 23// Handle callback 24app.get('/auth/google/callback', async (req, res) => { 25 const { code, state } = req.query; 26 27 // Verify state to prevent CSRF 28 if (!verifyState(state)) { 29 return res.status(400).json({ error: 'Invalid state' }); 30 } 31 32 try { 33 const { tokens } = await googleClient.getToken(code as string); 34 googleClient.setCredentials(tokens); 35 36 // Get user info 37 const ticket = await googleClient.verifyIdToken({ 38 idToken: tokens.id_token!, 39 audience: process.env.GOOGLE_CLIENT_ID, 40 }); 41 42 const payload = ticket.getPayload()!; 43 44 // Find or create user 45 let user = await db.user.findUnique({ 46 where: { email: payload.email }, 47 }); 48 49 if (!user) { 50 user = await db.user.create({ 51 data: { 52 email: payload.email!, 53 name: payload.name!, 54 googleId: payload.sub, 55 }, 56 }); 57 } 58 59 // Generate session token 60 const sessionToken = generateTokens(user); 61 res.json(sessionToken); 62 } catch (error) { 63 res.status(400).json({ error: 'Authentication failed' }); 64 } 65});
Pros: ✓ Delegated authentication ✓ No password handling ✓ Standardized flow Cons: ✗ Complex implementation ✗ Depends on external providers ✗ Token management required

Session-Based Auth#

1// Session authentication with cookies 2// Best for: Traditional web apps, same-origin requests 3 4import session from 'express-session'; 5import RedisStore from 'connect-redis'; 6import { createClient } from 'redis'; 7 8const redisClient = createClient(); 9await redisClient.connect(); 10 11app.use(session({ 12 store: new RedisStore({ client: redisClient }), 13 secret: process.env.SESSION_SECRET!, 14 resave: false, 15 saveUninitialized: false, 16 name: 'sessionId', 17 cookie: { 18 httpOnly: true, 19 secure: process.env.NODE_ENV === 'production', 20 sameSite: 'lax', 21 maxAge: 24 * 60 * 60 * 1000, // 24 hours 22 }, 23})); 24 25// Login 26app.post('/auth/login', async (req, res) => { 27 const { email, password } = req.body; 28 29 const user = await db.user.findUnique({ where: { email } }); 30 31 if (!user || !await bcrypt.compare(password, user.passwordHash)) { 32 return res.status(401).json({ error: 'Invalid credentials' }); 33 } 34 35 req.session.userId = user.id; 36 req.session.role = user.role; 37 38 res.json({ user: sanitizeUser(user) }); 39}); 40 41// Auth middleware 42function sessionAuth(req: Request, res: Response, next: NextFunction) { 43 if (!req.session.userId) { 44 return res.status(401).json({ error: 'Not authenticated' }); 45 } 46 next(); 47} 48 49// Logout 50app.post('/auth/logout', (req, res) => { 51 req.session.destroy((err) => { 52 if (err) { 53 return res.status(500).json({ error: 'Logout failed' }); 54 } 55 res.clearCookie('sessionId'); 56 res.json({ message: 'Logged out' }); 57 }); 58});
Pros: ✓ Simple to revoke ✓ Secure with httpOnly cookies ✓ Server controls state Cons: ✗ Requires session storage ✗ Not ideal for mobile apps ✗ Cross-origin complexity

Comparison Table#

| Method | Stateless | Mobile | Multi-service | Complexity | |-------------|-----------|--------|---------------|------------| | API Keys | Yes | Yes | Yes | Low | | JWT | Yes | Yes | Yes | Medium | | OAuth 2.0 | Depends | Yes | Yes | High | | Sessions | No | Poor | Poor | Low |

Security Best Practices#

1// Rate limiting 2import rateLimit from 'express-rate-limit'; 3 4const authLimiter = rateLimit({ 5 windowMs: 15 * 60 * 1000, 6 max: 5, 7 message: 'Too many login attempts', 8}); 9 10app.post('/auth/login', authLimiter, loginHandler); 11 12// HTTPS only 13if (process.env.NODE_ENV === 'production') { 14 app.use((req, res, next) => { 15 if (req.headers['x-forwarded-proto'] !== 'https') { 16 return res.redirect(`https://${req.headers.host}${req.url}`); 17 } 18 next(); 19 }); 20} 21 22// Timing-safe comparison 23import crypto from 'crypto'; 24 25function secureCompare(a: string, b: string): boolean { 26 const bufA = Buffer.from(a); 27 const bufB = Buffer.from(b); 28 29 if (bufA.length !== bufB.length) { 30 return false; 31 } 32 33 return crypto.timingSafeEqual(bufA, bufB); 34}

Choosing the Right Method#

Use API Keys when: ✓ Server-to-server communication ✓ Simple integration needed ✓ No user context required Use JWT when: ✓ Stateless architecture ✓ Multiple services/microservices ✓ Mobile app support needed Use OAuth when: ✓ Third-party integration ✓ Social login required ✓ Delegating authentication Use Sessions when: ✓ Traditional web app ✓ Same-origin requests ✓ Simple revocation needed

Best Practices#

Security: ✓ Always use HTTPS ✓ Implement rate limiting ✓ Hash stored credentials ✓ Use secure cookie settings Tokens: ✓ Short access token expiry ✓ Rotate refresh tokens ✓ Implement revocation ✓ Validate on each request Storage: ✓ Never store plain secrets ✓ Use secure session storage ✓ Encrypt sensitive data ✓ Audit access logs

Conclusion#

Choose authentication based on your architecture and security needs. API keys work for simple server integrations, JWTs for stateless APIs, OAuth for third-party auth, and sessions for traditional web apps. Always prioritize security with HTTPS, rate limiting, and proper token management.

Share this article

Help spread the word about Bootspring