Strażniku bram Imperium! Architekt Vitruvius zauważył, że jednorazowe przepustki (access tokeny) wygasają zbyt szybko, zmuszając legionistów do ciągłego logowania. Czas nauczyć się systemu Refresh Tokens - mechanizmu, który pozwala bezpiecznie odnawiać dostęp bez konieczności ponownego podawania danych uwierzytelniających!
Wyobraź sobie system przepustek w obozie legionowym. Legionariusz dostaje dwa dokumenty:
Dlaczego potrzebujemy dwóch tokenów?
Mechanizm refresh tokenów to fundament bezpiecznych systemów uwierzytelniania w produkcyjnych aplikacjach.
1// auth/auth.service.ts
2import { Injectable, UnauthorizedException } from '@nestjs/common';
3import { JwtService } from '@nestjs/jwt';
4import { ConfigService } from '@nestjs/config';
5import * as bcrypt from 'bcrypt';
6
7@Injectable()
8export class AuthService {
9 constructor(
10 private jwtService: JwtService,
11 private configService: ConfigService,
12 private usersService: UsersService,
13 ) {}
14
15 async login(loginDto: LoginDto): Promise<TokenPair> {
16 const user = await this.usersService.validateCredentials(
17 loginDto.username,
18 loginDto.password,
19 );
20
21 if (!user) {
22 throw new UnauthorizedException('Nieprawidłowe dane logowania, legionariuszu!');
23 }
24
25 // Generuj parę tokenów
26 const tokens = await this.generateTokenPair(user);
27
28 // Zapisz zahashowany refresh token w bazie
29 const hashedRefreshToken = await bcrypt.hash(tokens.refreshToken, 10);
30 await this.usersService.updateRefreshToken(user.id, hashedRefreshToken);
31
32 return tokens;
33 }
34
35 async generateTokenPair(user: User): Promise<TokenPair> {
36 const payload = {
37 sub: user.id,
38 username: user.username,
39 role: user.role,
40 cohort: user.cohortName,
41 };
42
43 const [accessToken, refreshToken] = await Promise.all([
44 this.jwtService.signAsync(payload, {
45 secret: this.configService.get('JWT_ACCESS_SECRET'),
46 expiresIn: '15m', // Przepustka dzienna - krótki czas życia
47 }),
48 this.jwtService.signAsync(payload, {
49 secret: this.configService.get('JWT_REFRESH_SECRET'),
50 expiresIn: '7d', // Pieczęć centuriona - dłuższy czas życia
51 }),
52 ]);
53
54 return { accessToken, refreshToken };
55 }
56}1// auth/auth.controller.ts
2@Controller('auth')
3export class AuthController {
4 constructor(private authService: AuthService) {}
5
6 @Post('login')
7 async login(@Body() loginDto: LoginDto) {
8 return this.authService.login(loginDto);
9 }
10
11 @Post('refresh')
12 async refreshTokens(@Body('refreshToken') refreshToken: string) {
13 return this.authService.refreshTokens(refreshToken);
14 }
15
16 @Post('logout')
17 @UseGuards(JwtAuthGuard)
18 async logout(@Req() req, @Body('refreshToken') refreshToken: string) {
19 // Unieważnij refresh token przy wylogowaniu
20 await this.authService.logout(req.user.id);
21 return { message: 'Legionariusz wylogowany pomyślnie' };
22 }
23}1// auth/auth.service.ts (kontynuacja)
2async refreshTokens(refreshToken: string): Promise<TokenPair> {
3 // 1. Zweryfikuj refresh token
4 let payload;
5 try {
6 payload = await this.jwtService.verifyAsync(refreshToken, {
7 secret: this.configService.get('JWT_REFRESH_SECRET'),
8 });
9 } catch (error) {
10 throw new UnauthorizedException('Pieczęć centuriona wygasła - zaloguj się ponownie!');
11 }
12
13 // 2. Znajdź użytkownika i sprawdź zapisany refresh token
14 const user = await this.usersService.findById(payload.sub);
15
16 if (!user || !user.hashedRefreshToken) {
17 throw new UnauthorizedException('Dostęp odrzucony!');
18 }
19
20 // 3. Porównaj tokeny
21 const isTokenValid = await bcrypt.compare(refreshToken, user.hashedRefreshToken);
22
23 if (!isTokenValid) {
24 // Potencjalna kradzież tokenu - unieważnij wszystkie sesje
25 await this.usersService.updateRefreshToken(user.id, null);
26 throw new UnauthorizedException('Token unieważniony - wykryto podejrzaną aktywność!');
27 }
28
29 // 4. Wygeneruj nową parę tokenów (Token Rotation)
30 const newTokens = await this.generateTokenPair(user);
31
32 // 5. Zapisz nowy refresh token
33 const hashedNewRefreshToken = await bcrypt.hash(newTokens.refreshToken, 10);
34 await this.usersService.updateRefreshToken(user.id, hashedNewRefreshToken);
35
36 return newTokens;
37}
38
39async logout(userId: number): Promise<void> {
40 // Unieważnij refresh token
41 await this.usersService.updateRefreshToken(userId, null);
42}Kluczowa technika bezpieczeństwa: przy każdym odświeżeniu generujemy nowy refresh token i unieważniamy stary. Jeśli ktoś ukradnie refresh token i spróbuje go użyć po rotacji - wykryjemy to:
1// Schemat ochrony przed kradzieżą tokenów
2// 1. Legionariusz loguje się → otrzymuje AT1 + RT1
3// 2. AT1 wygasa → wysyła RT1 → otrzymuje AT2 + RT2 (RT1 unieważniony)
4// 3. Złodziej próbuje użyć RT1 → ODMOWA (token już obrócony)
5// 4. System wykrywa użycie starego tokenu → unieważnia WSZYSTKIE sesje użytkownika1// users/user.schema.ts (Mongoose)
2import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
3import { Document } from 'mongoose';
4
5@Schema({ timestamps: true })
6export class User extends Document {
7 @Prop({ required: true, unique: true })
8 username: string;
9
10 @Prop({ required: true })
11 password: string;
12
13 @Prop()
14 email: string;
15
16 @Prop({ default: 'miles' })
17 role: string;
18
19 @Prop({ nullable: true })
20 hashedRefreshToken: string;
21
22 @Prop()
23 cohortName: string;
24}
25
26export const UserSchema = SchemaFactory.createForClass(User);1// auth/guards/refresh-token.guard.ts
2import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
3import { AuthGuard } from '@nestjs/passport';
4
5@Injectable()
6export class RefreshTokenGuard extends AuthGuard('jwt-refresh') {}
7
8// auth/strategies/refresh-token.strategy.ts
9import { Injectable } from '@nestjs/common';
10import { PassportStrategy } from '@nestjs/passport';
11import { ExtractJwt, Strategy } from 'passport-jwt';
12import { ConfigService } from '@nestjs/config';
13import { Request } from 'express';
14
15@Injectable()
16export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
17 constructor(private configService: ConfigService) {
18 super({
19 jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'),
20 secretOrKey: configService.get('JWT_REFRESH_SECRET'),
21 passReqToCallback: true,
22 });
23 }
24
25 validate(req: Request, payload: any) {
26 const refreshToken = req.body.refreshToken;
27 return { ...payload, refreshToken };
28 }
29}Zaimplementuj kompletny system refresh tokenów dla legionu:
1// Twój kod tutaj
2// 1. Skonfiguruj dwa sekrety JWT (access i refresh) w ConfigService
3// 2. Zaimplementuj endpoint POST /auth/refresh z walidacją
4// 3. Dodaj Token Rotation przy każdym odświeżeniu
5// 4. Zaimplementuj wykrywanie ponownego użycia starego tokenu
6// 5. Dodaj czyszczenie wygasłych tokenów (cron job)Zostałeś mianowany strażnikiem bram Imperium! Teraz umiesz:
✓ Generować pary tokenów - access token (krótki) + refresh token (długi) ✓ Implementować Token Rotation - nowy refresh token przy każdym odświeżeniu ✓ Wykrywać kradzież tokenów - unieważnianie sesji przy ponownym użyciu starego tokenu ✓ Bezpiecznie przechowywać refresh tokeny w bazie (zahashowane bcryptem) ✓ Implementować logout - unieważnianie refresh tokenu przy wylogowaniu ✓ Tworzyć Passport Strategies dla refresh tokenów
Teraz Twój legion ma bezpieczny system odnawiania przepustek!