All articles
BackendMarch 28, 202610 min read

JWT Authentication in NestJS: Access Tokens, Refresh Tokens, and Rotation

A single JWT token is not enough for production. Here's how to implement the access token + refresh token pattern with rotation in NestJS.

NestJSJWTAuthenticationTypeScriptSecurity

JWT authentication with a single token works in development but creates a real security problem in production: when the token is stolen, you cannot revoke it until it expires. The access token + refresh token pattern solves this by separating concerns.

Why two tokens?

  • Access token: short-lived (15 min), stateless. The server verifies the signature without a DB lookup — fast on every request.
  • Refresh token: long-lived (7 days), stored in the database. Revocable at any time by deleting the DB record.

The complete flow

  • User logs in → server returns access token + refresh token stored in DB.
  • Client sends access token in the Authorization header on every API call.
  • When access token expires, client sends refresh token to POST /auth/refresh.
  • Server validates, deletes old token, issues new pair (rotation).
  • On logout, server deletes the refresh token — session immediately invalidated.

Passport strategies in NestJS

// jwt.strategy.ts — access token
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: config.get<string>('JWT_ACCESS_SECRET'),
    });
  }
  validate(payload: JwtPayload): JwtPayload {
    return payload;
  }
}

Token rotation on refresh

async refreshTokens(userId: string, refreshToken: string) {
  const stored = await this.prisma.refreshToken.findUnique({
    where: { token: refreshToken, userId },
  });
  if (!stored) throw new ForbiddenException('Invalid refresh token');
  await this.prisma.refreshToken.delete({ where: { id: stored.id } });
  return this.issueTokens(userId);
}

Security checklist

  • Use separate secrets for access and refresh tokens
  • Set aggressive expiry on access tokens (15 min)
  • On logout, always delete the refresh token from the DB
  • For multi-device support, store one refresh token per device
NOTE

Storing the refresh token in an httpOnly cookie instead of in the Authorization header protects it from XSS attacks.