We use cookies to enhance your experience on the site
CodeWorlds

JWT Tokens - Roman passes

Pass maker! Consul Caesar.js has a new task for you. Our legion is growing, legionaries are constantly traveling between forts, and the password system is becoming inconvenient. It's time to learn JWT (JSON Web Tokens) - magical passes that allow legionaries to move freely across the entire empire!

What are JWTs in the world of legionaries?

Imagine you are the chief document forger in the Roman Empire. Instead of forcing every legionary to remember a password for each fort, you create special passes:

  • Self-verifying - no need to check in a central database
  • Contain information about the legionary - name, rank, permissions
  • Have an expiration date - cannot be used indefinitely
  • Digitally signed - impossible to forge
  • Portable - work in all cohorts of the legion

JWT is a token standard that consists of three parts:

  1. Header - token type and encryption algorithm
  2. Payload - user data (claims)
  3. Signature - digital signature ensuring authenticity

Installation and JWT configuration

1# Package installation
2npm install @nestjs/jwt @nestjs/passport passport passport-jwt
3npm install --save-dev @types/passport-jwt

JWT Module Configuration

1// auth/jwt.config.ts
2import { ConfigService } from '@nestjs/config';
3import { JwtModuleAsyncOptions } from '@nestjs/jwt';
4
5export const jwtConfig: JwtModuleAsyncOptions = {
6 inject: [ConfigService],
7 useFactory: (configService: ConfigService) => ({
8 secret: configService.get<string>('JWT_SECRET') || 'legionary-legion-secret-key-2024',
9 signOptions: {
10 expiresIn: configService.get<string>('JWT_EXPIRES_IN') || '24h',
11 issuer: 'Legionary',
12 audience: 'Legionarys',
13 },
14 }),
15};

Environment Variables

1# .env
2JWT_SECRET=super-secret-legionary-key-that-should-be-very-long-and-random
3JWT_EXPIRES_IN=24h
4JWT_REFRESH_EXPIRES_IN=7d

JWT Strategy with Passport

1// auth/strategies/jwt.strategy.ts
2import { Injectable, UnauthorizedException } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { ExtractJwt, Strategy } from 'passport-jwt';
5import { ConfigService } from '@nestjs/config';
6import { UserService } from '../services/user.service';
7
8export interface JwtPayload {
9 sub: number; // user ID
10 username: string;
11 email: string;
12 role: string;
13 cohortName?: string;
14 iat: number; // issued at
15 exp: number; // expires at
16}
17
18@Injectable()
19export class JwtStrategy extends PassportStrategy(Strategy) {
20 constructor(
21 private userService: UserService,
22 private configService: ConfigService,
23 ) {
24 super({
25 jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
26 ignoreExpiration: false,
27 secretOrKey: configService.get<string>('JWT_SECRET'),
28 issuer: 'Legionary',
29 audience: 'Legionarys',
30 });
31 }
32
33 async validate(payload: JwtPayload) {
34 // Check if the user still exists and is active
35 const user = await this.userService.findById(payload.sub);
36 
37 if (!user) {
38 throw new UnauthorizedException('Legionary not found in the legion!');
39 }
40
41 if (!user.isActive) {
42 throw new UnauthorizedException('Legionary account has been deactivated!');
43 }
44
45 // Check if the token was issued before the last password change
46 const tokenIssuedAt = new Date(payload.iat * 1000);
47 if (user.lastPasswordChange && tokenIssuedAt < user.lastPasswordChange) {
48 throw new UnauthorizedException('Token expired after password change!');
49 }
50
51 // Return the user object (will be available as req.user)
52 return {
53 id: user.id,
54 username: user.username,
55 email: user.email,
56 role: user.role,
57 cohortName: user.cohortName,
58 };
59 }
60}

JWT Auth Guard

1// auth/guards/jwt-auth.guard.ts
2import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
3import { AuthGuard } from '@nestjs/passport';
4import { Reflector } from '@nestjs/core';
5import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
6
7@Injectable()
8export class JwtAuthGuard extends AuthGuard('jwt') {
9 constructor(private reflector: Reflector) {
10 super();
11 }
12
13 canActivate(context: ExecutionContext) {
14 // Check if the endpoint is public
15 const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
16 context.getHandler(),
17 context.getClass(),
18 ]);
19 
20 if (isPublic) {
21 return true;
22 }
23 
24 return super.canActivate(context);
25 }
26
27 handleRequest(err, user, info, context) {
28 if (err || !user) {
29 const request = context.switchToHttp().getRequest();
30 const message = this.getErrorMessage(info);
31 
32 throw new UnauthorizedException(message);
33 }
34 return user;
35 }
36
37 private getErrorMessage(info: any): string {
38 if (info?.name === 'TokenExpiredError') {
39 return 'Pass has expired! Please log in again.';
40 }
41 if (info?.name === 'JsonWebTokenError') {
42 return 'Invalid pass!';
43 }
44 if (info?.name === 'NotBeforeError') {
45 return 'Pass is not yet valid!';
46 }
47 return 'No authorization - you need a valid pass!';
48 }
49}

Public Decorator

1// auth/decorators/public.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4export const IS_PUBLIC_KEY = 'isPublic';
5export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

Auth Service with JWT

1// auth/services/auth.service.ts
2import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
3import { JwtService } from '@nestjs/jwt';
4import { ConfigService } from '@nestjs/config';
5import { UserService } from './user.service';
6import { CryptoService } from './crypto.service';
7import { LoginDto } from '../dto/auth.dto';
8import { JwtPayload } from '../strategies/jwt.strategy';
9
10export interface AuthResult {
11 access_token: string;
12 refresh_token: string;
13 token_type: string;
14 expires_in: number;
15 user: {
16 id: number;
17 username: string;
18 email: string;
19 role: string;
20 cohortName?: string;
21 };
22}
23
24@Injectable()
25export class AuthService {
26 constructor(
27 private userService: UserService,
28 private cryptoService: CryptoService,
29 private jwtService: JwtService,
30 private configService: ConfigService,
31 ) {}
32
33 async login(loginDto: LoginDto, ip: string): Promise<AuthResult> {
34 // Validate the user
35 const user = await this.userService.validateUser(loginDto.username, loginDto.password);
36 
37 if (!user) {
38 throw new UnauthorizedException('Invalid login credentials!');
39 }
40
41 // Update login information
42 await this.userService.updateLastLogin(user.id, ip);
43
44 // Generate tokens
45 const tokens = await this.generateTokens(user);
46 
47 return {
48 ...tokens,
49 user: {
50 id: user.id,
51 username: user.username,
52 email: user.email,
53 role: user.role,
54 cohortName: user.cohortName,
55 },
56 };
57 }
58
59 async generateTokens(user: any): Promise<{
60 access_token: string;
61 refresh_token: string;
62 token_type: string;
63 expires_in: number;
64 }> {
65 const payload: Omit<JwtPayload, 'iat' | 'exp'> = {
66 sub: user.id,
67 username: user.username,
68 email: user.email,
69 role: user.role,
70 cohortName: user.cohortName,
71 };
72
73 const [accessToken, refreshToken] = await Promise.all([
74 this.jwtService.signAsync(payload),
75 this.jwtService.signAsync(
76 { ...payload, type: 'refresh' },
77 {
78 expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d',
79 },
80 ),
81 ]);
82
83 return {
84 access_token: accessToken,
85 refresh_token: refreshToken,
86 token_type: 'Bearer',
87 expires_in: 24 * 60 * 60, // 24 hours in seconds
88 };
89 }
90
91 async refreshToken(refreshToken: string): Promise<AuthResult> {
92 try {
93 const payload = await this.jwtService.verifyAsync(refreshToken);
94 
95 if (payload.type !== 'refresh') {
96 throw new UnauthorizedException('Invalid token type!');
97 }
98
99 // Check if the user still exists
100 const user = await this.userService.findById(payload.sub);
101 if (!user || !user.isActive) {
102 throw new UnauthorizedException('User does not exist or is inactive!');
103 }
104
105 // Generate new tokens
106 const tokens = await this.generateTokens(user);
107 
108 return {
109 ...tokens,
110 user: {
111 id: user.id,
112 username: user.username,
113 email: user.email,
114 role: user.role,
115 cohortName: user.cohortName,
116 },
117 };
118 } catch (error) {
119 throw new UnauthorizedException('Invalid refresh token!');
120 }
121 }
122
123 async validateToken(token: string): Promise<JwtPayload> {
124 try {
125 return await this.jwtService.verifyAsync(token);
126 } catch (error) {
127 throw new UnauthorizedException('Invalid token!');
128 }
129 }
130
131 async revokeAllTokens(userId: number): Promise<void> {
132 // In a real application, we would save this to a token blacklist
133 // or update the last password change timestamp
134 await this.userService.updatePasswordChangeTimestamp(userId);
135 }
136
137 // Decode token without verification (for debugging purposes)
138 decodeToken(token: string): any {
139 return this.jwtService.decode(token);
140 }
141}

Updated Auth Controller

1// auth/controllers/auth.controller.ts
2import { 
3 Controller, 
4 Post, 
5 Body, 
6 Get, 
7 Put, 
8 UseGuards, 
9 Request,
10 HttpCode,
11 HttpStatus,
12 Ip
13} from '@nestjs/common';
14import { AuthService } from '../services/auth.service';
15import { UserService } from '../services/user.service';
16import { RegisterDto, LoginDto, ChangePasswordDto } from '../dto/auth.dto';
17import { JwtAuthGuard } from '../guards/jwt-auth.guard';
18import { Public } from '../decorators/public.decorator';
19import { CurrentUser } from '../decorators/current-user.decorator';
20
21@Controller('auth')
22export class AuthController {
23 constructor(
24 private authService: AuthService,
25 private userService: UserService,
26 ) {}
27
28 @Public()
29 @Post('register')
30 async register(@Body() registerDto: RegisterDto) {
31 const user = await this.userService.createUser(registerDto);
32 const { password, ...userWithoutPassword } = user;
33 
34 return {
35 message: 'New legionary has been successfully registered in the legion!',
36 user: userWithoutPassword,
37 };
38 }
39
40 @Public()
41 @Post('login')
42 @HttpCode(HttpStatus.OK)
43 async login(@Body() loginDto: LoginDto, @Ip() ip: string) {
44 return await this.authService.login(loginDto, ip);
45 }
46
47 @Public()
48 @Post('refresh')
49 @HttpCode(HttpStatus.OK)
50 async refresh(@Body('refresh_token') refreshToken: string) {
51 return await this.authService.refreshToken(refreshToken);
52 }
53
54 @Post('logout')
55 @UseGuards(JwtAuthGuard)
56 async logout(@CurrentUser() user: any) {
57 await this.authService.revokeAllTokens(user.id);
58 return { message: 'You have been successfully logged out!' };
59 }
60
61 @Get('profile')
62 @UseGuards(JwtAuthGuard)
63 async getProfile(@CurrentUser() user: any) {
64 return await this.userService.getUserProfile(user.id);
65 }
66
67 @Put('change-password')
68 @UseGuards(JwtAuthGuard)
69 async changePassword(
70 @CurrentUser() user: any,
71 @Body() changePasswordDto: ChangePasswordDto,
72 ) {
73 await this.userService.changePassword(user.id, changePasswordDto);
74 
75 // Revoke all existing tokens
76 await this.authService.revokeAllTokens(user.id);
77 
78 return {
79 message: 'Password has been successfully changed! Please log in again.',
80 };
81 }
82
83 @Put('profile')
84 @UseGuards(JwtAuthGuard)
85 async updateProfile(
86 @CurrentUser() user: any,
87 @Body() updateData: any,
88 ) {
89 const updatedUser = await this.userService.updateProfile(user.id, updateData);
90 const { password, ...userWithoutPassword } = updatedUser;
91 
92 return {
93 message: 'Profile has been updated!',
94 user: userWithoutPassword,
95 };
96 }
97
98 @Get('verify-token')
99 @UseGuards(JwtAuthGuard)
100 async verifyToken(@CurrentUser() user: any) {
101 return {
102 valid: true,
103 user,
104 message: 'Token is valid!',
105 };
106 }
107}

Current User Decorator

1// auth/decorators/current-user.decorator.ts
2import { createParamDecorator, ExecutionContext } from '@nestjs/common';
3
4export const CurrentUser = createParamDecorator(
5 (data: string, ctx: ExecutionContext) => {
6 const request = ctx.switchToHttp().getRequest();
7 const user = request.user;
8 
9 return data ? user?.[data] : user;
10 },
11);

JWT Middleware for global use

1// auth/middleware/jwt.middleware.ts
2import { Injectable, NestMiddleware } from '@nestjs/common';
3import { Request, Response, NextFunction } from 'express';
4import { JwtService } from '@nestjs/jwt';
5
6@Injectable()
7export class JwtMiddleware implements NestMiddleware {
8 constructor(private jwtService: JwtService) {}
9
10 async use(req: Request, res: Response, next: NextFunction) {
11 const authHeader = req.headers.authorization;
12 
13 if (authHeader && authHeader.startsWith('Bearer ')) {
14 const token = authHeader.substring(7);
15 
16 try {
17 const payload = await this.jwtService.verifyAsync(token);
18 req['user'] = payload;
19 } catch (error) {
20 // Invalid token - do nothing, let the Guards handle it
21 }
22 }
23 
24 next();
25 }
26}

Interceptor for adding token information

1// auth/interceptors/token-info.interceptor.ts
2import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
3import { Observable } from 'rxjs';
4import { map } from 'rxjs/operators';
5import { JwtService } from '@nestjs/jwt';
6
7@Injectable()
8export class TokenInfoInterceptor implements NestInterceptor {
9 constructor(private jwtService: JwtService) {}
10
11 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
12 return next.handle().pipe(
13 map(data => {
14 const request = context.switchToHttp().getRequest();
15 const authHeader = request.headers.authorization;
16 
17 if (authHeader && authHeader.startsWith('Bearer ')) {
18 const token = authHeader.substring(7);
19 
20 try {
21 const decoded = this.jwtService.decode(token) as any;
22 
23 // Add token expiration information
24 if (decoded?.exp) {
25 const expiresAt = new Date(decoded.exp * 1000);
26 const timeLeft = expiresAt.getTime() - Date.now();
27 
28 return {
29 ...data,
30 token_info: {
31 expires_at: expiresAt.toISOString(),
32 time_left_seconds: Math.max(0, Math.floor(timeLeft / 1000)),
33 should_refresh: timeLeft < 30 * 60 * 1000, // Refresh if < 30 min
34 },
35 };
36 }
37 } catch (error) {
38 // Ignore decoding errors
39 }
40 }
41 
42 return data;
43 }),
44 );
45 }
46}

Testing JWT

1// auth/auth.service.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { JwtService } from '@nestjs/jwt';
4import { ConfigService } from '@nestjs/config';
5import { AuthService } from './auth.service';
6import { UserService } from './user.service';
7import { CryptoService } from './crypto.service';
8
9describe('AuthService', () => {
10 let service: AuthService;
11 let jwtService: JwtService;
12 let userService: UserService;
13
14 beforeEach(async () => {
15 const module: TestingModule = await Test.createTestingModule({
16 providers: [
17 AuthService,
18 {
19 provide: JwtService,
20 useValue: {
21 signAsync: jest.fn().mockResolvedValue('mock-jwt-token'),
22 verifyAsync: jest.fn(),
23 decode: jest.fn(),
24 },
25 },
26 {
27 provide: UserService,
28 useValue: {
29 validateUser: jest.fn(),
30 updateLastLogin: jest.fn(),
31 findById: jest.fn(),
32 },
33 },
34 {
35 provide: ConfigService,
36 useValue: {
37 get: jest.fn().mockReturnValue('mock-config'),
38 },
39 },
40 {
41 provide: CryptoService,
42 useValue: {},
43 },
44 ],
45 }).compile();
46
47 service = module.get<AuthService>(AuthService);
48 jwtService = module.get<JwtService>(JwtService);
49 userService = module.get<UserService>(UserService);
50 });
51
52 describe('login', () => {
53 it('should return tokens when credentials are valid', async () => {
54 const mockUser = {
55 id: 1,
56 username: 'caesar',
57 email: 'centurion@legionary.cohort',
58 role: 'CENTURION',
59 cohortName: 'Cohors Praetoria',
60 };
61
62 userService.validateUser = jest.fn().mockResolvedValue(mockUser);
63 userService.updateLastLogin = jest.fn().mockResolvedValue(undefined);
64
65 const result = await service.login(
66 { username: 'caesar', password: 'password' },
67 '127.0.0.1'
68 );
69
70 expect(result).toHaveProperty('access_token');
71 expect(result).toHaveProperty('refresh_token');
72 expect(result).toHaveProperty('user');
73 expect(result.user.username).toBe('caesar');
74 });
75 });
76
77 describe('generateTokens', () => {
78 it('should generate access and refresh tokens', async () => {
79 const mockUser = {
80 id: 1,
81 username: 'caesar',
82 email: 'centurion@legionary.cohort',
83 role: 'CENTURION',
84 };
85
86 const tokens = await service.generateTokens(mockUser);
87
88 expect(tokens).toHaveProperty('access_token');
89 expect(tokens).toHaveProperty('refresh_token');
90 expect(tokens.token_type).toBe('Bearer');
91 expect(tokens.expires_in).toBe(24 * 60 * 60);
92 });
93 });
94});

Main module configuration

1// auth.module.ts
2import { Module, MiddlewareConsumer } from '@nestjs/common';
3import { JwtModule } from '@nestjs/jwt';
4import { PassportModule } from '@nestjs/passport';
5import { TypeOrmModule } from '@nestjs/typeorm';
6import { ConfigModule } from '@nestjs/config';
7import { User } from './entities/user.entity';
8import { AuthService } from './services/auth.service';
9import { UserService } from './services/user.service';
10import { CryptoService } from './services/crypto.service';
11import { AuthController } from './controllers/auth.controller';
12import { JwtStrategy } from './strategies/jwt.strategy';
13import { JwtAuthGuard } from './guards/jwt-auth.guard';
14import { JwtMiddleware } from './middleware/jwt.middleware';
15import { jwtConfig } from './jwt.config';
16
17@Module({
18 imports: [
19 TypeOrmModule.forFeature([User]),
20 PassportModule.register({ defaultStrategy: 'jwt' }),
21 JwtModule.registerAsync(jwtConfig),
22 ConfigModule,
23 ],
24 controllers: [AuthController],
25 providers: [
26 AuthService,
27 UserService,
28 CryptoService,
29 JwtStrategy,
30 JwtAuthGuard,
31 ],
32 exports: [
33 AuthService,
34 UserService,
35 JwtAuthGuard,
36 JwtStrategy,
37 ],
38})
39export class AuthModule {
40 configure(consumer: MiddlewareConsumer) {
41 consumer
42 .apply(JwtMiddleware)
43 .forRoutes('*');
44 }
45}

Practical exercise

Extend the JWT system with additional features:

1// Your code here
2// 1. Token blacklisting (list of blocked tokens)
3// 2. Multiple device support (different tokens on different devices)
4// 3. Token introspection endpoint
5// 4. Automatic token refresh before expiration
6// 5. Device fingerprinting for additional security

Summary

You have been appointed chief pass maker! Now you can:

Configure JWT with appropriate security settings ✓ Generate tokens with user information ✓ Verify tokens using Passport strategy ✓ Implement refresh tokens for long-term sessions ✓ Protect endpoints with JWT Auth Guard ✓ Manage the lifecycle of tokens

Now your legionaries can move freely across the entire empire with magical passes!

Go to CodeWorlds