JSON Web Tokens (JWTs) enable stateless authentication. Here's how to implement them securely.
JWT Structure#
Header.Payload.Signature
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "user123",
"iat": 1704067200,
"exp": 1704070800,
"role": "user"
}
Signature:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Token Creation#
1import jwt from 'jsonwebtoken';
2
3interface TokenPayload {
4 sub: string;
5 role: string;
6 email: string;
7}
8
9const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
10const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;
11
12function createAccessToken(user: User): string {
13 const payload: TokenPayload = {
14 sub: user.id,
15 role: user.role,
16 email: user.email,
17 };
18
19 return jwt.sign(payload, ACCESS_TOKEN_SECRET, {
20 expiresIn: '15m',
21 issuer: 'your-app',
22 audience: 'your-app',
23 });
24}
25
26function createRefreshToken(user: User): string {
27 return jwt.sign(
28 { sub: user.id, tokenVersion: user.tokenVersion },
29 REFRESH_TOKEN_SECRET,
30 {
31 expiresIn: '7d',
32 issuer: 'your-app',
33 }
34 );
35}
36
37// Token pair
38function createTokenPair(user: User): TokenPair {
39 return {
40 accessToken: createAccessToken(user),
41 refreshToken: createRefreshToken(user),
42 };
43}Token Validation#
1function verifyAccessToken(token: string): TokenPayload {
2 try {
3 return jwt.verify(token, ACCESS_TOKEN_SECRET, {
4 issuer: 'your-app',
5 audience: 'your-app',
6 }) as TokenPayload;
7 } catch (error) {
8 if (error instanceof jwt.TokenExpiredError) {
9 throw new AuthError('Token expired', 'TOKEN_EXPIRED');
10 }
11 if (error instanceof jwt.JsonWebTokenError) {
12 throw new AuthError('Invalid token', 'INVALID_TOKEN');
13 }
14 throw error;
15 }
16}
17
18// Authentication middleware
19async function authenticate(
20 req: Request,
21 res: Response,
22 next: NextFunction
23): Promise<void> {
24 const authHeader = req.headers.authorization;
25
26 if (!authHeader?.startsWith('Bearer ')) {
27 res.status(401).json({ error: 'No token provided' });
28 return;
29 }
30
31 const token = authHeader.slice(7);
32
33 try {
34 const payload = verifyAccessToken(token);
35
36 // Optionally fetch user from database
37 const user = await prisma.user.findUnique({
38 where: { id: payload.sub },
39 select: { id: true, role: true, email: true },
40 });
41
42 if (!user) {
43 res.status(401).json({ error: 'User not found' });
44 return;
45 }
46
47 req.user = user;
48 next();
49 } catch (error) {
50 if (error instanceof AuthError) {
51 res.status(401).json({ error: error.message, code: error.code });
52 return;
53 }
54 res.status(401).json({ error: 'Invalid token' });
55 }
56}Refresh Token Flow#
1// Store refresh tokens in database
2model RefreshToken {
3 id String @id @default(cuid())
4 token String @unique
5 userId String
6 user User @relation(fields: [userId], references: [id])
7 expiresAt DateTime
8 createdAt DateTime @default(now())
9 revokedAt DateTime?
10
11 @@index([userId])
12 @@index([token])
13}
14
15// Refresh endpoint
16app.post('/auth/refresh', async (req, res) => {
17 const { refreshToken } = req.body;
18
19 if (!refreshToken) {
20 return res.status(400).json({ error: 'Refresh token required' });
21 }
22
23 try {
24 // Verify token signature
25 const payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET) as {
26 sub: string;
27 };
28
29 // Check token in database
30 const storedToken = await prisma.refreshToken.findUnique({
31 where: { token: refreshToken },
32 include: { user: true },
33 });
34
35 if (!storedToken || storedToken.revokedAt || storedToken.expiresAt < new Date()) {
36 return res.status(401).json({ error: 'Invalid refresh token' });
37 }
38
39 // Rotate refresh token
40 await prisma.refreshToken.update({
41 where: { id: storedToken.id },
42 data: { revokedAt: new Date() },
43 });
44
45 // Create new tokens
46 const newRefreshToken = createRefreshToken(storedToken.user);
47
48 await prisma.refreshToken.create({
49 data: {
50 token: newRefreshToken,
51 userId: storedToken.userId,
52 expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
53 },
54 });
55
56 res.json({
57 accessToken: createAccessToken(storedToken.user),
58 refreshToken: newRefreshToken,
59 });
60 } catch (error) {
61 res.status(401).json({ error: 'Invalid refresh token' });
62 }
63});Token Revocation#
1// Logout - revoke refresh token
2app.post('/auth/logout', authenticate, async (req, res) => {
3 const { refreshToken } = req.body;
4
5 if (refreshToken) {
6 await prisma.refreshToken.updateMany({
7 where: {
8 token: refreshToken,
9 userId: req.user.id,
10 },
11 data: { revokedAt: new Date() },
12 });
13 }
14
15 res.status(204).send();
16});
17
18// Logout from all devices
19app.post('/auth/logout-all', authenticate, async (req, res) => {
20 await prisma.refreshToken.updateMany({
21 where: {
22 userId: req.user.id,
23 revokedAt: null,
24 },
25 data: { revokedAt: new Date() },
26 });
27
28 res.status(204).send();
29});
30
31// Password change - invalidate all tokens
32async function changePassword(userId: string, newPassword: string): Promise<void> {
33 const hashedPassword = await bcrypt.hash(newPassword, 12);
34
35 await prisma.$transaction([
36 prisma.user.update({
37 where: { id: userId },
38 data: { password: hashedPassword },
39 }),
40 prisma.refreshToken.updateMany({
41 where: { userId, revokedAt: null },
42 data: { revokedAt: new Date() },
43 }),
44 ]);
45}Cookie-Based Tokens#
1// More secure for web applications
2const COOKIE_OPTIONS: CookieOptions = {
3 httpOnly: true,
4 secure: process.env.NODE_ENV === 'production',
5 sameSite: 'strict',
6 path: '/',
7};
8
9app.post('/auth/login', async (req, res) => {
10 // Validate credentials...
11 const user = await validateCredentials(req.body);
12
13 const tokens = createTokenPair(user);
14
15 // Store refresh token in HTTP-only cookie
16 res.cookie('refreshToken', tokens.refreshToken, {
17 ...COOKIE_OPTIONS,
18 maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
19 });
20
21 // Access token in response body
22 res.json({ accessToken: tokens.accessToken });
23});
24
25// Refresh using cookie
26app.post('/auth/refresh', async (req, res) => {
27 const refreshToken = req.cookies.refreshToken;
28
29 if (!refreshToken) {
30 return res.status(401).json({ error: 'No refresh token' });
31 }
32
33 // Validate and rotate...
34 const tokens = await rotateTokens(refreshToken);
35
36 res.cookie('refreshToken', tokens.refreshToken, {
37 ...COOKIE_OPTIONS,
38 maxAge: 7 * 24 * 60 * 60 * 1000,
39 });
40
41 res.json({ accessToken: tokens.accessToken });
42});
43
44// Logout - clear cookie
45app.post('/auth/logout', (req, res) => {
46 res.clearCookie('refreshToken', COOKIE_OPTIONS);
47 res.status(204).send();
48});Client-Side Token Management#
1// Token storage and refresh
2class AuthClient {
3 private accessToken: string | null = null;
4 private refreshPromise: Promise<string> | null = null;
5
6 async getAccessToken(): Promise<string> {
7 if (this.accessToken && !this.isTokenExpired(this.accessToken)) {
8 return this.accessToken;
9 }
10
11 return this.refresh();
12 }
13
14 private async refresh(): Promise<string> {
15 // Prevent multiple simultaneous refresh requests
16 if (this.refreshPromise) {
17 return this.refreshPromise;
18 }
19
20 this.refreshPromise = this.doRefresh();
21
22 try {
23 return await this.refreshPromise;
24 } finally {
25 this.refreshPromise = null;
26 }
27 }
28
29 private async doRefresh(): Promise<string> {
30 const response = await fetch('/auth/refresh', {
31 method: 'POST',
32 credentials: 'include', // Include cookies
33 });
34
35 if (!response.ok) {
36 this.accessToken = null;
37 throw new Error('Refresh failed');
38 }
39
40 const { accessToken } = await response.json();
41 this.accessToken = accessToken;
42 return accessToken;
43 }
44
45 private isTokenExpired(token: string): boolean {
46 try {
47 const payload = JSON.parse(atob(token.split('.')[1]));
48 // Add 30 second buffer
49 return payload.exp * 1000 < Date.now() + 30000;
50 } catch {
51 return true;
52 }
53 }
54}
55
56// Axios interceptor
57axios.interceptors.request.use(async (config) => {
58 const token = await authClient.getAccessToken();
59 config.headers.Authorization = `Bearer ${token}`;
60 return config;
61});Security Best Practices#
Tokens:
✓ Short-lived access tokens (15 minutes)
✓ Longer-lived refresh tokens (7 days)
✓ Rotate refresh tokens on use
✓ Store refresh tokens in database
Storage:
✓ HttpOnly cookies for refresh tokens
✓ Memory for access tokens (not localStorage)
✓ Secure flag in production
✓ SameSite=Strict
Validation:
✓ Verify signature, issuer, audience
✓ Check expiration
✓ Validate user still exists
✓ Handle token revocation
General:
✓ Use strong secrets (256+ bits)
✓ Different secrets for access/refresh
✓ Implement logout from all devices
✓ Rotate tokens on password change
Conclusion#
JWT authentication requires careful implementation. Use short-lived access tokens with longer-lived refresh tokens. Store refresh tokens in HTTP-only cookies, rotate them on use, and implement proper revocation. Never store JWTs in localStorage—use memory or secure cookies.