Access token ngắn hạn (15m) + refresh token dài hạn (7d) là pattern chuẩn.
Flow: login → trả access + refresh token → access token hết hạn → dùng refresh token để lấy access token mới → rotate refresh token.
Refresh Token Rotation: mỗi lần refresh, invalidate token cũ và cấp token mới — detect token theft:
async refresh(refreshToken: string) {
const payload = this.jwtService.verify(refreshToken, { secret: REFRESH_SECRET });
const user = await this.usersService.findOne(payload.sub);
// Validate token khớp với stored (hashed) token
const isValid = await bcrypt.compare(refreshToken, user.hashedRefreshToken);
if (!isValid) throw new ForbiddenException('Token reuse detected');
// Rotate — tạo tokens mới, hash và lưu token mới
const tokens = await this.getTokens(user.id, user.email);
await this.updateRefreshToken(user.id, tokens.refreshToken);
return tokens;
}Revocation: lưu refresh token (bcrypt hash) trong DB, khi logout gọi updateRefreshToken(userId, null).
Lưu ý: không lưu plain refresh token trong DB — luôn hash.
Short-lived access tokens (15m) + long-lived refresh tokens (7d) is the standard pattern.
Refresh Token Rotation: each refresh invalidates old token and issues new one — detects theft:
async refresh(refreshToken: string) {
const payload = this.jwtService.verify(refreshToken, { secret: REFRESH_SECRET });
const user = await this.usersService.findOne(payload.sub);
const isValid = await bcrypt.compare(refreshToken, user.hashedRefreshToken);
if (!isValid) throw new ForbiddenException();
// Rotate tokens, hash and store new one
return this.getTokens(user.id);
}Revocation: store bcrypt hash of refresh token; on logout set to null.
Pitfall: never store plain refresh token in DB.