Kurs NestJS · Moduł 4: Uwierzytelnianie
Passport.js - system kontroli dostępu
W tej lekcji6
Umiesz już wystawić przepustkę JWT i sprawdzić hasło. Ale w prawdziwym Imperium do bram podchodzą różni goście: jeden ma hasło, drugi przepustkę z pieczęcią, trzeci list polecający od sojusznika z Galii, czwarty klucz do bramy handlowej. Pisząc obsługę każdego z osobna, powielisz tę samą logikę cztery razy - a każdy sposób wpuszczania to nowa okazja do pomyłki.
Rzymianie postawili przy bramie strażnicę z jednym regulaminem i wieloma wartownikami: każdy wartownik zna jeden rodzaj dokumentu, ale wszyscy meldują tak samo. W NestJS tą strażnicą jest Passport.js - biblioteka, w której każdy sposób logowania to osobna strategia, a ekosystem oferuje ich ponad 500 gotowych.
Trzy warstwy strażnicy
Zanim napiszemy linijkę kodu, ustalmy, z czego składa się ta strażnica - bo to jest szkielet całej lekcji i wraca przy każdej strategii:
- Passport Module - rejestracja: mówi NestJS, których wartowników w ogóle zatrudniamy.
- Strategy - logika weryfikacji: jeden wartownik sprawdzający jeden rodzaj dokumentu.
- Guard - aktywacja: wskazuje, którego wartownika wołamy na tej konkretnej bramie.
- Dekorator
@UseGuards()- postawienie strażnika przy wejściu do endpointu.
Zapamiętaj tę kolejność od dołu do góry: strategia wie jak sprawdzić, guard wie kogo zawołać, dekorator wie gdzie go postawić.
Strategia lokalna - wartownik od haseł
Zacznijmy od najprostszego wartownika: sprawdza login i hasło.
1@Injectable()
2export class LocalStrategy extends PassportStrategy(Strategy) {
3 constructor(private authService: AuthService) {
4 super({
5 usernameField: 'username',
6 passwordField: 'password',
7 });
8 }
9
10 async validate(username: string, password: string): Promise<any> {
11 const user = await this.authService.validateUser(username, password);
12
13 if (!user) {
14 throw new UnauthorizedException();
15 }
16
17 return user;
18 }
19}Prześledźmy tę klasę, bo każdy jej element powtórzy się w kolejnych strategiach. PassportStrategy(Strategy) to klasa bazowa budowana z importu Strategy - tu z pakietu passport-local. Zmieniając pakiet, zmieniasz rodzaj wartownika, a reszta szkieletu zostaje.
Wywołanie super() w konstruktorze konfiguruje strategię. usernameField i passwordField mówią, z których pól żądania wziąć dane - i to jest częsty punkt zaczepienia, bo jeśli Twój formularz wysyła email zamiast username, właśnie tu to zgłaszasz.
Sercem jest validate(). Passport wywołuje ją sam, podając wyciągnięte pola, a Twoim zadaniem jest odpowiedzieć: kto to jest. Zwróć uwagę na kontrakt tej metody - to najważniejsze zdanie tej lekcji. Zwrócony obiekt trafia do request.user i staje się dostępny w kontrolerze. Gdy dokument jest fałszywy, nie zwracasz null ani false - rzucasz UnauthorizedException, a NestJS zamieni ją na odpowiedź 401.
Guard - wołanie wartownika po imieniu
Strategia sama z siebie nie zadziała. Trzeba ją aktywować, a robi to guard:
1@Injectable()
2export class LocalAuthGuard extends AuthGuard('local') {}Tak, to całe ciało klasy - puste. AuthGuard('local') buduje gotowego guarda, który po nazwie odnajduje zarejestrowaną strategię i uruchamia jej validate(). Nazwa 'local' nie jest przypadkowa: to domyślny identyfikator strategii z pakietu passport-local, tak jak 'jwt' należy do passport-jwt, a 'google' do strategii Google.
Analogicznie wygląda guard dla przepustek JWT, które poznałeś w poprzedniej lekcji:
1@Injectable()
2export class JwtAuthGuard extends AuthGuard('jwt') {}Po co w ogóle własna klasa, skoro można napisać @UseGuards(AuthGuard('local')) wprost? Z dwóch powodów: nazwa LocalAuthGuard czyta się lepiej w kontrolerze, a gdy zechcesz dołożyć własne zachowanie, masz gdzie je wpisać. Właśnie do tego służy metoda handleRequest, którą możesz nadpisać:
1@Injectable()
2export class JwtAuthGuard extends AuthGuard('jwt') {
3 handleRequest(err: any, user: any) {
4 if (err) {
5 throw err;
6 }
7
8 if (!user) {
9 throw new UnauthorizedException('Przepustka nieważna lub wygasła');
10 }
11
12 return user;
13 }
14}Kolejność jest tu logiczna i warto ją zapamiętać: najpierw sprawdzasz błąd, potem obecność użytkownika, dopiero na końcu zwracasz obiekt - a to, co zwrócisz, wyląduje w request.user. Nadpisujemy tę metodę głównie po to, by dać czytelny komunikat zamiast gołego 401.
Strategia zewnętrzna - list polecający z Galii
Skoro szkielet się nie zmienia, dołożenie logowania przez Google sprowadza się do podmiany pakietu i danych konfiguracyjnych. Wartownik jest nowy, regulamin ten sam.
1@Injectable()
2export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
3 constructor(private configService: ConfigService) {
4 super({
5 clientID: configService.get('GOOGLE_CLIENT_ID'),
6 clientSecret: configService.get('GOOGLE_CLIENT_SECRET'),
7 callbackURL: '/auth/google/callback',
8 scope: ['email', 'profile'],
9 });
10 }
11}Tu Strategy pochodzi z pakietu passport-google-oauth20. clientID i clientSecret to dane Twojej aplikacji wydane przez Google - i dlatego czytamy je przez ConfigService ze zmiennych środowiskowych, a nie wpisujemy w kod. Sekret w repozytorium to sekret cudzy; to polecam traktować jako regułę bez wyjątków.
callbackURL to adres, pod który Google odeśle użytkownika po zalogowaniu - Twoja aplikacja musi mieć tam endpoint. scope określa, o jakie dane prosisz.
Zwróć uwagę na drugi argument PassportStrategy(Strategy, 'google'): to jawnie nadana nazwa, po której guard AuthGuard('google') odnajdzie tę strategię. Przy strategii lokalnej mogliśmy go pominąć, bo nazwa domyślna wystarczała.
Postawienie strażnika przy bramie
Ostatni krok to wskazanie, których endpointów strażnik pilnuje:
1@Controller('auth')
2export class AuthController {
3 @UseGuards(LocalAuthGuard)
4 @Post('login')
5 async login(@Request() req) {
6 return this.authService.generateToken(req.user);
7 }
8
9 @UseGuards(JwtAuthGuard)
10 @Get('profile')
11 getProfile(@Request() req) {
12 return req.user;
13 }
14}Tu domyka się cały łańcuch. Guard uruchomił strategię, strategia wykonała validate(), zwrócony obiekt wylądował w request.user - i dopiero teraz metoda kontrolera może po niego sięgnąć. Jeśli weryfikacja się nie powiodła, metoda nie wykona się wcale: guard zatrzymuje żądanie, zanim dojdzie ono do kontrolera.
Podsumowanie
Strażnica stoi, a Ty umiesz zatrudnić w niej dowolnego wartownika:
- Passport.js to framework do uwierzytelniania, w którym każdy sposób logowania jest osobną strategią - ekosystem daje ponad 500 gotowych,
- warstwy układają się od dołu: strategia (jak sprawdzić), guard (kogo zawołać),
@UseGuards()(gdzie postawić), - strategia dziedziczy po
PassportStrategy(Strategy), konfiguruje się przezsuper({...})i implementujevalidate(), validate()zwraca użytkownika, który trafia dorequest.user, a przy odrzuceniu rzucaUnauthorizedException,- guard to zwykle pusta klasa:
extends AuthGuard('local'),AuthGuard('jwt'),AuthGuard('google'), handleRequestnadpisujesz, gdy chcesz własną obsługę: sprawdź błąd, sprawdź użytkownika, zwróć obiekt,- strategie zewnętrzne różnią się tylko pakietem i konfiguracją;
clientIDiclientSecretczytaj zConfigService, nigdy z kodu, - drugi argument
PassportStrategy(Strategy, 'nazwa')nadaje strategii nazwę, po której znajdzie ją guard.
W następnej lekcji zejdziemy poziom niżej - do ról i uprawnień, czyli pytania, co wolno gościowi, którego już wpuściliśmy. A na razie zapamiętaj: strategia wie, jak sprawdzić dokument, guard wie, którego wartownika zawołać, a request.user to meldunek, który zostaje po udanej kontroli.
Kod do tej lekcji: src/auth/passport-strategies.ts
1// Passport.js - Uniwersalny System Kontroli Dostepu
2// Strategie Passport dla roznych metod logowania
3import { Injectable, UnauthorizedException } from '@nestjs/common';
4import { PassportStrategy } from '@nestjs/passport';
5import { Strategy as LocalStrategy } from 'passport-local';
6import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
7
8// ===========================================
9// 1. Local Strategy - logowanie haslem
10// ===========================================
11
12@Injectable()
13export class RomanLocalStrategy extends PassportStrategy(LocalStrategy) {
14 constructor() {
15 super({
16 usernameField: 'username', // Pole z nazwa uzytkownika
17 passwordField: 'password', // Pole z haslem
18 });
19 }
20
21 // Metoda wywolywana przy logowaniu
22 async validate(username: string, password: string) {
23 // Tu wywolaj AuthService.validateUser()
24 console.log('Local Strategy: sprawdzam', username);
25
26 // Symulacja walidacji
27 if (username === 'caesar' && password === 'spqr') {
28 return { id: '1', username: 'caesar', rank: 'consul' };
29 }
30
31 throw new UnauthorizedException('Bledne dane logowania!');
32 }
33}
34
35// ===========================================
36// 2. JWT Strategy - weryfikacja tokenu
37// ===========================================
38
39@Injectable()
40export class RomanJwtStrategy extends PassportStrategy(JwtStrategy) {
41 constructor() {
42 super({
43 // Skad pobierac token
44 jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
45 // Czy ignorowac wygasniecie
46 ignoreExpiration: false,
47 // Sekret do weryfikacji podpisu
48 secretOrKey: 'spqr-secret-key',
49 });
50 }
51
52 // Metoda wywolywana po zdekodowaniu tokenu
53 async validate(payload: any) {
54 console.log('JWT Strategy: token zdekodowany', payload);
55
56 // Payload z tokenu trafia do req.user
57 return {
58 id: payload.sub,
59 username: payload.username,
60 rank: payload.rank,
61 };
62 }
63}
64
65// ===========================================
66// 3. Uzycie strategii w kontrolerze
67// ===========================================
68
69import { Controller, Post, Get, UseGuards, Request } from '@nestjs/common';
70import { AuthGuard } from '@nestjs/passport';
71
72@Controller('auth')
73export class AuthController {
74 // Logowanie - uzywa LocalStrategy
75 @Post('login')
76 @UseGuards(AuthGuard('local'))
77 async login(@Request() req) {
78 console.log('Zalogowany:', req.user);
79 return { message: 'Witaj w Imperium!', user: req.user };
80 }
81
82 // Chroniony endpoint - uzywa JwtStrategy
83 @Get('profile')
84 @UseGuards(AuthGuard('jwt'))
85 getProfile(@Request() req) {
86 console.log('Profil legionariusza:', req.user);
87 return { user: req.user };
88 }
89}
90
91console.log('=== Strategie Passport ===');
92console.log('LocalStrategy: username + password -> validate()');
93console.log('JwtStrategy: Bearer token -> validate(payload)');
94console.log('AuthGuard("local") -> uzywa LocalStrategy');
95console.log('AuthGuard("jwt") -> uzywa JwtStrategy');
96Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym jest Passport.js w kontekście NestJS?
2. Co robi metoda validate() w LocalStrategy?
To 2 z 12 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij klasę LocalStrategy, która dziedziczy po PassportStrategy(Strategy) z passport-local i implementuje validate(username, password) rzucając UnauthorizedException gdy użytkownik nie istnieje
- Układanie w pionie
Ułóż opcje konfiguracji super() w LocalStrategy od lewej do prawej
- Klikanie w kolejności
Ułóż elementy definicji LocalAuthGuard w poprawnej kolejności
- Edytor kodu
Utwórz klasę JwtAuthGuard z dekoratorem @Injectable(), która rozszerza AuthGuard('jwt')
- Układanie w pionie
Uporządkuj warstwy authentication w NestJS od najniższej do najwyższej
- Edytor kodu
Uzupełnij GoogleStrategy z opcjami clientID i clientSecret pobranymi z ConfigService oraz callbackURL ustawionym na '/auth/google/callback'
- Klikanie w kolejności
Ułóż logikę metody handleRequest w AuthGuard od pierwszego kroku do ostatniego
- Układanie w pionie
Uporządkuj kroki procesu OAuth 2.0 (np. logowanie przez Google)
- Edytor kodu
Uzupełnij kontroler AuthController z endpointem @Post('login') chronionym przez @UseGuards(LocalAuthGuard), który zwraca token JWT
- Układanie w poziomie
Ułóż składnię użycia wielu guardów na endpoincie
- Edytor kodu
Uzupełnij metodę generateRefreshToken w AuthService, która tworzy JWT z expiresIn: '7d' i zawiera payload z userId i tokenType: 'refresh'
- Klikanie w kolejności
Ułóż kroki procesu odświeżania tokenu JWT
- Edytor kodu
Uzupełnij kontroler z endpointem @Get('profile') chronionym @UseGuards(JwtAuthGuard), który zwraca dane zalogowanego użytkownika z @Req() req
- Układanie w pionie
Ułóż składnię pobierania użytkownika z request w chronionym endpoincie
- Klikanie w kolejności
Ułóż elementy wywołania jwtService.sign() w poprawnej kolejności
- Edytor kodu
Uzupełnij metodę login(user) w AuthService, która tworzy payload z sub: user.id i username: user.username, a następnie zwraca { access_token: this.jwtService.sign(payload) }
- Układanie w pionie
Uporządkuj elementy konfiguracji AuthModule od importów do eksportu
- Edytor kodu
Uzupełnij metodę validate w AuthService, która sprawdza email i hasło, a w przypadku braku użytkownika rzuca throw new UnauthorizedException('Nieprawidłowe dane logowania')
- Układanie w poziomie
Ułóż składnię importu AuthGuard z @nestjs/passport
- Klikanie w kolejności
Ułóż kolejność wykonania warstw w żądaniu HTTP z authentication
- Edytor kodu
Uzupełnij AuthModule importujący PassportModule, JwtModule.registerAsync(jwtConfig), z providers: [AuthService, JwtStrategy, LocalStrategy] i exports: [AuthService]
- Układanie w poziomie
Ułóż składnię metody validate w JwtStrategy od async do zwrócenia obiektu
- Układanie w pionie
Uporządkuj cykl życia tokenu JWT od utworzenia do wygaśnięcia