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!
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:
JWT is a token standard that consists of three parts:
1# Package installation
2npm install @nestjs/jwt @nestjs/passport passport passport-jwt
3npm install --save-dev @types/passport-jwt1// 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};1# .env
2JWT_SECRET=super-secret-legionary-key-that-should-be-very-long-and-random
3JWT_EXPIRES_IN=24h
4JWT_REFRESH_EXPIRES_IN=7d1// 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}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}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);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}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}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);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}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}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});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}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 securityYou 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!