Refresh Tokens
⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 14 min
Refresh Tokens are long-lived tokens used to securely obtain new, short-lived Access Tokens, solving the security vs. convenience dilemma of JWT architecture.
Overview
If you issue an Access Token (JWT) that lasts for 1 year, the user never has to log in again. However, if a hacker steals that token, they have access for a year, and because JWTs are stateless, you cannot easily revoke it.
If you issue a token that lasts for 15 minutes, it’s very secure, but the user will be furiously angry when they are logged out every 15 minutes.
The Solution:
- Issue a short-lived Access Token (e.g., 15 mins). The client uses this for API calls.
- Issue a long-lived Refresh Token (e.g., 7 days). The client stores this securely.
- When the Access Token expires, the client silently sends the Refresh Token to a specific endpoint (
/auth/refresh) to get a new Access Token, without bothering the user.
Key Concepts
- Revocation: Because the client only uses the Refresh Token once every 15 minutes to get a new Access Token, the server can verify the Refresh Token against a database. If the user was banned, the server deletes the Refresh Token from the DB, and the next refresh attempt fails.
- Storage: Access Tokens are often kept in memory (JS variables) or localStorage. Refresh Tokens should ideally be stored in
httpOnlycookies to protect against XSS (Cross-Site Scripting).
Code Examples
1. Generating Both Tokens
Modify your AuthService.login() method to generate two distinct tokens.
async login(user: any) {
const payload = { sub: user.id, email: user.email };
// 1. Short-lived Access Token (15m)
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
// 2. Long-lived Refresh Token (7d)
const refreshToken = this.jwtService.sign(payload, {
secret: 'DIFFERENT_REFRESH_SECRET', // Use a separate secret key!
expiresIn: '7d'
});
// 3. Store a hash of the refresh token in the database for revocation capabilities
await this.usersService.saveRefreshTokenHash(user.id, refreshToken);
return { accessToken, refreshToken };
}
2. The Refresh Strategy
You need a specific Passport strategy just for the /refresh endpoint, pointing to the different secret key.
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor() {
super({
// The client might send the refresh token in the header, or in a cookie
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'DIFFERENT_REFRESH_SECRET',
passReqToCallback: true, // We need the request object to extract the raw token
});
}
async validate(req: Request, payload: any) {
const refreshToken = req.get('Authorization').replace('Bearer', '').trim();
// We return both the decoded payload AND the raw token,
// so the controller can verify the token against the database hash
return { ...payload, refreshToken };
}
}
3. The Refresh Endpoint
@UseGuards(AuthGuard('jwt-refresh'))
@Post('refresh')
async refreshTokens(@Req() req) {
// req.user contains the payload and the raw refreshToken
const userId = req.user.sub;
const refreshToken = req.user.refreshToken;
// Verify the token matches what is in the database (ensuring it wasn't revoked)
const isValid = await this.authService.verifyRefreshTokenHash(userId, refreshToken);
if (!isValid) throw new UnauthorizedException('Token has been revoked');
// Generate a brand new Access Token (and optionally a new Refresh Token - called "Refresh Token Rotation")
return this.authService.generateNewAccessToken(userId);
}
Best Practices
- Refresh Token Rotation: For maximum security, every time a Refresh Token is used, issue a new Access Token AND a new Refresh Token, invalidating the old Refresh Token. If a hacker steals a Refresh Token and uses it, the real user’s next attempt will fail (because the token rotated), alerting the system to a potential breach.