--- title: Implement Secure JWT Authentication impact: CRITICAL impactDescription: Essential for secure APIs tags: security, jwt, authentication, tokens --- ## Implement Secure JWT Authentication Use `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads. **Incorrect (insecure JWT implementation):** ```typescript // Hardcode secrets @Module({ imports: [ JwtModule.register({ secret: 'my-secret-key', // Exposed in code signOptions: { expiresIn: '7d' }, // Too long }), ], }) export class AuthModule {} // Store sensitive data in JWT async login(user: User): Promise<{ accessToken: string }> { const payload = { sub: user.id, email: user.email, password: user.password, // NEVER include password! ssn: user.ssn, // NEVER include sensitive data! isAdmin: user.isAdmin, // Can be tampered if not verified }; return { accessToken: this.jwtService.sign(payload) }; } // Skip token validation @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: 'my-secret', }); } async validate(payload: any): Promise { return payload; // No validation of user existence } } ``` **Correct (secure JWT with refresh tokens):** ```typescript // Secure JWT configuration @Module({ imports: [ JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => ({ secret: config.get('JWT_SECRET'), signOptions: { expiresIn: '15m', // Short-lived access tokens issuer: config.get('JWT_ISSUER'), audience: config.get('JWT_AUDIENCE'), }, }), }), PassportModule.register({ defaultStrategy: 'jwt' }), ], }) export class AuthModule {} // Minimal JWT payload @Injectable() export class AuthService { async login(user: User): Promise { // Only include necessary, non-sensitive data const payload: JwtPayload = { sub: user.id, email: user.email, roles: user.roles, iat: Math.floor(Date.now() / 1000), }; const accessToken = this.jwtService.sign(payload); const refreshToken = await this.createRefreshToken(user.id); return { accessToken, refreshToken, expiresIn: 900 }; } private async createRefreshToken(userId: string): Promise { const token = randomBytes(32).toString('hex'); const hashedToken = await bcrypt.hash(token, 10); await this.refreshTokenRepo.save({ userId, token: hashedToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days }); return token; } } // Proper JWT strategy with validation @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private config: ConfigService, private usersService: UsersService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: config.get('JWT_SECRET'), ignoreExpiration: false, issuer: config.get('JWT_ISSUER'), audience: config.get('JWT_AUDIENCE'), }); } async validate(payload: JwtPayload): Promise { // Verify user still exists and is active const user = await this.usersService.findById(payload.sub); if (!user || !user.isActive) { throw new UnauthorizedException('User not found or inactive'); } // Verify token wasn't issued before password change if (user.passwordChangedAt) { const tokenIssuedAt = new Date(payload.iat * 1000); if (tokenIssuedAt < user.passwordChangedAt) { throw new UnauthorizedException('Token invalidated by password change'); } } return user; } } ``` Reference: [NestJS Authentication](https://docs.nestjs.com/security/authentication)