Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

JWT Tokens - rzymskie przepustki

wytwórco przepustek! Konsul Caesar.js ma dla Ciebie nowe zadanie. Nasz legion się rozrasta, legioniści ciągle podróżują między fortami, a system haseł staje się niewygodny. Czas nauczyć się JWT (JSON Web Tokens) - magicznych przepustek, które pozwalają legionariuszom swobodnie poruszać się po całym Imperium!

Czym są JWT w świecie legionariuszy?

Wyobraź sobie, że jesteś głównym fałszerzem dokumentów w rzymskim Imperium. Zamiast zmuszać każdego legionariusza do pamiętania hasła do każdego fortu, tworzysz specjalne przepustki:

  • Samo-weryfikujące się - nie trzeba sprawdzać w centralnej bazie
  • Zawierają informacje o legionariuszu - imię, ranga, uprawnienia
  • Mają datę ważności - nie można ich używać w nieskończoność
  • Są podpisane cyfrowo - niemożliwe do podrobienia
  • Przenośne - działają we wszystkich fortach Imperium

JWT to standard tokenu, który składa się z trzech części:

  1. Header - typ tokenu i algorytm szyfrowania
  2. Payload - dane o użytkowniku (claims)
  3. Signature - podpis cyfrowy zapewniający autentyczność

Instalacja i konfiguracja JWT

1# Instalacja pakietów
2npm install @nestjs/jwt @nestjs/passport passport passport-jwt
3npm install --save-dev @types/passport-jwt

Konfiguracja JWT Module

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 z 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 // Sprawdź czy użytkownik nadal istnieje i jest aktywny
35 const user = await this.userService.findById(payload.sub);
36 
37 if (!user) {
38 throw new UnauthorizedException('Legionariusz nie został znaleziony w legionie!');
39 }
40
41 if (!user.isActive) {
42 throw new UnauthorizedException('Konto legionariusza zostało dezaktywowane!');
43 }
44
45 // Sprawdź czy token nie został wydany przed ostatnią zmianą hasła
46 const tokenIssuedAt = new Date(payload.iat * 1000);
47 if (user.lastPasswordChange && tokenIssuedAt < user.lastPasswordChange) {
48 throw new UnauthorizedException('Token wygasł po zmianie hasła!');
49 }
50
51 // Zwróć obiekt użytkownika (będzie dostępny jako 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 // Sprawdź czy endpoint jest publiczny
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 'Przepustka wygasła! Zaloguj się ponownie.';
40 }
41 if (info?.name === 'JsonWebTokenError') {
42 return 'Nieprawidłowa przepustka!';
43 }
44 if (info?.name === 'NotBeforeError') {
45 return 'Przepustka nie jest jeszcze ważna!';
46 }
47 return 'Brak autoryzacji - potrzebujesz ważnej przepustki!';
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 z 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 // Waliduj użytkownika
35 const user = await this.userService.validateUser(loginDto.username, loginDto.password);
36 
37 if (!user) {
38 throw new UnauthorizedException('Nieprawidłowe dane logowania!');
39 }
40
41 // Aktualizuj informacje o logowaniu
42 await this.userService.updateLastLogin(user.id, ip);
43
44 // Generuj tokeny
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 godziny w sekundach
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('Nieprawidłowy typ tokenu!');
97 }
98
99 // Sprawdź czy użytkownik nadal istnieje
100 const user = await this.userService.findById(payload.sub);
101 if (!user || !user.isActive) {
102 throw new UnauthorizedException('Użytkownik nie istnieje lub jest nieaktywny!');
103 }
104
105 // Wygeneruj nowe tokeny
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('Nieprawidłowy 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('Nieprawidłowy token!');
128 }
129 }
130
131 async revokeAllTokens(userId: number): Promise<void> {
132 // W prawdziwej aplikacji, zapisalibyśmy to w blackliście tokenów
133 // lub zaktualizowali timestamp ostatniej zmiany hasła
134 await this.userService.updatePasswordChangeTimestamp(userId);
135 }
136
137 // Decode token bez weryfikacji (do celów debugging)
138 decodeToken(token: string): any {
139 return this.jwtService.decode(token);
140 }
141}

Aktualizacja 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: 'Nowy legionariusz został pomyślnie zarejestrowany w legionie!',
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: 'Zostałeś pomyślnie wylogowany!' };
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 // Anuluj wszystkie istniejące tokeny
76 await this.authService.revokeAllTokens(user.id);
77 
78 return {
79 message: 'Hasło zostało pomyślnie zmienione! Zaloguj się ponownie.',
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: 'Profil został zaktualizowany!',
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 jest ważny!',
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 dla globalnego użycia

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 // Token nieprawidłowy - nie robimy nic, pozwalamy Guards'om to obsłużyć
21 }
22 }
23 
24 next();
25 }
26}

Interceptor do dodawania informacji o tokenie

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 // Dodaj informacje o wygaśnięciu tokenu
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 jeśli < 30 min
34 },
35 };
36 }
37 } catch (error) {
38 // Ignoruj błędy dekodowania
39 }
40 }
41 
42 return data;
43 }),
44 );
45 }
46}

Testowanie 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: 'Ultio Reginae',
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});

Konfiguracja głównego modułu

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}

Praktyczne ćwiczenie

Rozszerz system JWT o dodatkowe funkcje:

1// Twój kod tutaj
2// 1. Token blacklisting (lista zablokowanych tokenów)
3// 2. Multiple device support (różne tokeny na różne urządzenia)
4// 3. Token introspection endpoint
5// 4. Automatic token refresh przed wygaśnięciem
6// 5. Device fingerprinting dla dodatkowego bezpieczeństwa

Podsumowanie

Zostałeś mianowany głównym wytwórcą przepustek! Teraz umiesz:

Konfigurować JWT z odpowiednimi ustawieniami bezpieczeństwa ✓ Generować tokeny z informacjami o użytkowniku ✓ Weryfikować tokeny za pomocą Passport strategy ✓ Implementować refresh tokens dla długoterminowych sesji ✓ Chronić endpointy z JWT Auth Guard ✓ Zarządzać cyklem życia tokenów

Teraz Twoi legioniści mogą swobodnie przemieszczać się po całym Imperium z magicznymi przepustkami!

Przejdź do CodeWorlds