Kurs NestJS · Moduł 4: Uwierzytelnianie

Passport.js - system kontroli dostępu

5 min czytania
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:

  1. Passport Module - rejestracja: mówi NestJS, których wartowników w ogóle zatrudniamy.
  2. Strategy - logika weryfikacji: jeden wartownik sprawdzający jeden rodzaj dokumentu.
  3. Guard - aktywacja: wskazuje, którego wartownika wołamy na tej konkretnej bramie.
  4. 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ę przez super({...}) i implementuje validate(),
  • validate() zwraca użytkownika, który trafia do request.user, a przy odrzuceniu rzuca UnauthorizedException,
  • guard to zwykle pusta klasa: extends AuthGuard('local'), AuthGuard('jwt'), AuthGuard('google'),
  • handleRequest nadpisujesz, gdy chcesz własną obsługę: sprawdź błąd, sprawdź użytkownika, zwróć obiekt,
  • strategie zewnętrzne różnią się tylko pakietem i konfiguracją; clientID i clientSecret czytaj z ConfigService, 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');
96

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Czym jest Passport.js w kontekście NestJS?

  2. 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

Przydatne artykuły