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.