JWTs enable stateless authentication. Here's how to implement them securely.
JWT Structure#
1// JWT has three parts: header.payload.signature
2
3// Header
4{
5 "alg": "HS256", // Algorithm
6 "typ": "JWT" // Type
7}
8
9// Payload (Claims)
10{
11 "sub": "user123", // Subject (user ID)
12 "iat": 1609459200, // Issued at
13 "exp": 1609545600, // Expiration
14 "iss": "myapp.com", // Issuer
15 "aud": "myapp.com", // Audience
16 "role": "user", // Custom claim
17 "permissions": ["read"] // Custom claim
18}
19
20// Signature
21HMACSHA256(
22 base64UrlEncode(header) + "." + base64UrlEncode(payload),
23 secret
24)Token Generation#
1import jwt from 'jsonwebtoken';
2
3interface TokenPayload {
4 userId: string;
5 email: string;
6 role: string;
7}
8
9const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
10const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;
11
12function generateAccessToken(payload: TokenPayload): string {
13 return jwt.sign(payload, ACCESS_TOKEN_SECRET, {
14 expiresIn: '15m', // Short-lived
15 issuer: 'myapp.com',
16 audience: 'myapp.com',
17 });
18}
19
20function generateRefreshToken(userId: string): string {
21 return jwt.sign(
22 { userId, tokenVersion: 1 },
23 REFRESH_TOKEN_SECRET,
24 {
25 expiresIn: '7d', // Longer-lived
26 issuer: 'myapp.com',
27 }
28 );
29}
30
31// Login endpoint
32app.post('/auth/login', async (req, res) => {
33 const { email, password } = req.body;
34
35 const user = await validateCredentials(email, password);
36 if (!user) {
37 return res.status(401).json({ error: 'Invalid credentials' });
38 }
39
40 const accessToken = generateAccessToken({
41 userId: user.id,
42 email: user.email,
43 role: user.role,
44 });
45
46 const refreshToken = generateRefreshToken(user.id);
47
48 // Store refresh token hash in database
49 await db.refreshToken.create({
50 data: {
51 userId: user.id,
52 tokenHash: hashToken(refreshToken),
53 expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
54 },
55 });
56
57 // Send refresh token as httpOnly cookie
58 res.cookie('refreshToken', refreshToken, {
59 httpOnly: true,
60 secure: process.env.NODE_ENV === 'production',
61 sameSite: 'strict',
62 maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
63 });
64
65 res.json({ accessToken });
66});Token Verification#
1import jwt, { JwtPayload } from 'jsonwebtoken';
2
3interface VerifiedPayload extends JwtPayload {
4 userId: string;
5 email: string;
6 role: string;
7}
8
9function verifyAccessToken(token: string): VerifiedPayload {
10 return jwt.verify(token, ACCESS_TOKEN_SECRET, {
11 issuer: 'myapp.com',
12 audience: 'myapp.com',
13 }) as VerifiedPayload;
14}
15
16// Middleware
17function authenticate(req: Request, res: Response, next: NextFunction) {
18 const authHeader = req.headers.authorization;
19
20 if (!authHeader?.startsWith('Bearer ')) {
21 return res.status(401).json({ error: 'Missing token' });
22 }
23
24 const token = authHeader.slice(7);
25
26 try {
27 const payload = verifyAccessToken(token);
28 req.user = payload;
29 next();
30 } catch (error) {
31 if (error instanceof jwt.TokenExpiredError) {
32 return res.status(401).json({ error: 'Token expired' });
33 }
34 if (error instanceof jwt.JsonWebTokenError) {
35 return res.status(401).json({ error: 'Invalid token' });
36 }
37 return res.status(401).json({ error: 'Authentication failed' });
38 }
39}Refresh Token Flow#
1// Refresh endpoint
2app.post('/auth/refresh', async (req, res) => {
3 const refreshToken = req.cookies.refreshToken;
4
5 if (!refreshToken) {
6 return res.status(401).json({ error: 'Refresh token required' });
7 }
8
9 try {
10 const payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET) as {
11 userId: string;
12 tokenVersion: number;
13 };
14
15 // Verify token exists in database and hasn't been revoked
16 const storedToken = await db.refreshToken.findFirst({
17 where: {
18 userId: payload.userId,
19 tokenHash: hashToken(refreshToken),
20 revokedAt: null,
21 expiresAt: { gt: new Date() },
22 },
23 });
24
25 if (!storedToken) {
26 return res.status(401).json({ error: 'Invalid refresh token' });
27 }
28
29 // Get user
30 const user = await db.user.findUnique({
31 where: { id: payload.userId },
32 });
33
34 if (!user) {
35 return res.status(401).json({ error: 'User not found' });
36 }
37
38 // Check token version (for forced logout)
39 if (user.tokenVersion !== payload.tokenVersion) {
40 return res.status(401).json({ error: 'Token revoked' });
41 }
42
43 // Generate new tokens
44 const newAccessToken = generateAccessToken({
45 userId: user.id,
46 email: user.email,
47 role: user.role,
48 });
49
50 // Optionally rotate refresh token
51 const newRefreshToken = generateRefreshToken(user.id);
52
53 // Revoke old refresh token
54 await db.refreshToken.update({
55 where: { id: storedToken.id },
56 data: { revokedAt: new Date() },
57 });
58
59 // Store new refresh token
60 await db.refreshToken.create({
61 data: {
62 userId: user.id,
63 tokenHash: hashToken(newRefreshToken),
64 expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
65 },
66 });
67
68 res.cookie('refreshToken', newRefreshToken, {
69 httpOnly: true,
70 secure: process.env.NODE_ENV === 'production',
71 sameSite: 'strict',
72 maxAge: 7 * 24 * 60 * 60 * 1000,
73 });
74
75 res.json({ accessToken: newAccessToken });
76 } catch (error) {
77 return res.status(401).json({ error: 'Invalid refresh token' });
78 }
79});
80
81// Logout
82app.post('/auth/logout', async (req, res) => {
83 const refreshToken = req.cookies.refreshToken;
84
85 if (refreshToken) {
86 // Revoke refresh token
87 await db.refreshToken.updateMany({
88 where: { tokenHash: hashToken(refreshToken) },
89 data: { revokedAt: new Date() },
90 });
91 }
92
93 res.clearCookie('refreshToken');
94 res.json({ message: 'Logged out' });
95});
96
97// Force logout all sessions
98async function revokeAllTokens(userId: string) {
99 // Increment token version
100 await db.user.update({
101 where: { id: userId },
102 data: { tokenVersion: { increment: 1 } },
103 });
104
105 // Revoke all refresh tokens
106 await db.refreshToken.updateMany({
107 where: { userId, revokedAt: null },
108 data: { revokedAt: new Date() },
109 });
110}Client-Side Token Management#
1// Token storage and refresh
2class AuthService {
3 private accessToken: string | null = null;
4 private refreshPromise: Promise<string> | null = null;
5
6 async getAccessToken(): Promise<string | null> {
7 if (this.accessToken && !this.isTokenExpired(this.accessToken)) {
8 return this.accessToken;
9 }
10
11 return this.refreshAccessToken();
12 }
13
14 private isTokenExpired(token: string): boolean {
15 try {
16 const payload = JSON.parse(atob(token.split('.')[1]));
17 // Refresh 1 minute before expiry
18 return payload.exp * 1000 < Date.now() + 60000;
19 } catch {
20 return true;
21 }
22 }
23
24 async refreshAccessToken(): Promise<string | null> {
25 // Deduplicate concurrent refresh requests
26 if (this.refreshPromise) {
27 return this.refreshPromise;
28 }
29
30 this.refreshPromise = (async () => {
31 try {
32 const response = await fetch('/auth/refresh', {
33 method: 'POST',
34 credentials: 'include', // Include cookies
35 });
36
37 if (!response.ok) {
38 throw new Error('Refresh failed');
39 }
40
41 const { accessToken } = await response.json();
42 this.accessToken = accessToken;
43 return accessToken;
44 } catch (error) {
45 this.accessToken = null;
46 throw error;
47 } finally {
48 this.refreshPromise = null;
49 }
50 })();
51
52 return this.refreshPromise;
53 }
54
55 async fetch(url: string, options: RequestInit = {}): Promise<Response> {
56 const token = await this.getAccessToken();
57
58 const response = await fetch(url, {
59 ...options,
60 headers: {
61 ...options.headers,
62 Authorization: token ? `Bearer ${token}` : '',
63 },
64 });
65
66 if (response.status === 401) {
67 // Try refresh once
68 const newToken = await this.refreshAccessToken();
69
70 if (newToken) {
71 return fetch(url, {
72 ...options,
73 headers: {
74 ...options.headers,
75 Authorization: `Bearer ${newToken}`,
76 },
77 });
78 }
79 }
80
81 return response;
82 }
83}Security Considerations#
1// Use strong secrets
2// Generate with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
3
4// Validate algorithm
5const payload = jwt.verify(token, secret, {
6 algorithms: ['HS256'], // Explicitly specify allowed algorithms
7});
8
9// Never trust client-provided algorithm
10// Always verify signature server-side
11
12// Keep payloads minimal
13// Don't include sensitive data
14const payload = {
15 userId: user.id,
16 role: user.role,
17 // NOT: password, creditCard, etc.
18};
19
20// Use short expiration for access tokens
21const accessToken = jwt.sign(payload, secret, {
22 expiresIn: '15m', // 15 minutes max
23});
24
25// Implement token blocklist for immediate revocation
26const revokedTokens = new Set<string>();
27
28function isTokenRevoked(token: string): boolean {
29 const jti = jwt.decode(token)?.jti;
30 return jti ? revokedTokens.has(jti) : false;
31}Best Practices#
Token Security:
✓ Use strong, unique secrets
✓ Short access token expiry (15m)
✓ Store refresh tokens securely
✓ Implement token rotation
Storage:
✓ httpOnly cookies for refresh tokens
✓ Memory for access tokens
✓ Never localStorage for tokens
✓ Use secure cookie flags
Validation:
✓ Verify signature
✓ Check expiration
✓ Validate issuer and audience
✓ Use explicit algorithm
Revocation:
✓ Implement token blocklist
✓ Store refresh tokens in DB
✓ Support forced logout
✓ Handle token rotation
Conclusion#
JWTs enable scalable authentication when implemented securely. Use short-lived access tokens with refresh token rotation, store tokens appropriately, and implement proper revocation. Never store sensitive data in tokens and always validate on the server side.