Skip to content

Commit 97a3a1e

Browse files
authored
feat(wakacyje wyzwanie): backend, part 4 docs (#94)
* feat: wakacyje backend, part 4 docs * fix: file rename * fix: mdx CI * feat: added recording link
1 parent 4aaad81 commit 97a3a1e

File tree

6 files changed

+358
-0
lines changed

6 files changed

+358
-0
lines changed

src/assets/nestjs/auth/cors.png

136 KB
Loading

src/assets/nestjs/auth/int.png

44 KB
Loading

src/assets/nestjs/auth/int2.png

53.3 KB
Loading
24.9 KB
Loading
20.4 KB
Loading
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
---
2+
title: 4. Autoryzacja i walidatory
3+
description: Autoryzacja, Walidatory
4+
sidebar:
5+
order: 5
6+
---
7+
8+
## Wstęp
9+
10+
Autoryzacja jest potrzebna w zasadzie w każdej aplikacji. Nawet jeżeli nasze API jest całkowicie publiczne i dostępne dla każdego,
11+
to przydałoby się aby nowe treści można było dodać poprzez pewnego rodzaju panel administratora. W tym rozdziale zostaną wytłomaczone podstawy
12+
przebiegu i metod autoryzacji, opisane kilka dobrych praktyk związanych z nimi oraz, dodatkowo, znajdzie się sekcja o walidacji danych wejściowych.
13+
14+
15+
Kod zawarty w tym poradniki można zobaczyć w całości w [repozytorium z Wakacyjnego Wyzwania 2025](https://github.com/Solvro/backend-wakacyjne-wyzwanie-2025)
16+
17+
----------------
18+
19+
## Autoryzacja
20+
21+
### 1. „Co zrobić z tym hasłem?”
22+
23+
Nieodłącznym elementem autoryzacji jest potrzeba udowodnienia, że ty, jako osoba wysyłająca zapytanie, jesteś tym za kogo się podajesz.
24+
25+
Najprostszym sposobem jest użycie zwykłego hasła dostępu, powiązanego z kontem danego użytkownika. Jednak sama obecność hasła stwarza zagrożenie dla
26+
developera aplikacji - hasła mogą bowiem wyciec, czy zostać ukradzione. Dlatego należy zastanowić się w jaki sposób takie hasło można bezpiecznie
27+
przechowywać. Tutaj pojawiają się dwa słowa: hashowanie oraz enkrypcja. Poniżej definicje z Oxford Languages Dictionary (przetłumaczone):
28+
29+
**Enkrypcja (Encryption)** - *odwracalny proces zamiany informacji lub danych w kod/szyfr, szczególnie aby uniemożliwić nieautoryzowany dostęp.*
30+
31+
**Hashowanie (Hashing)** - *zamiana kawałka danych na numeryczny lub alfanumeryczny łańcuch znaków za pomocą funkcji, której rezultat ma zawsze tą samą długość.*
32+
33+
Do hasła jednak, najlepsze jest hashowanie - wtedy, nawet w przypadku wycieku danych, złodziej nie jest w stanie odzyskać pierwotnego hasła z hashu. Zatem typowy *flow* rejestracji użytkownika oraz
34+
logowania wygląda następująco:
35+
36+
Flow rejestracji:
37+
38+
1. Hasło jako łańcuch znaków (plaintext)
39+
2. Hashowane za pomocą wyznaczonej funkcji
40+
3. Przechowujemy tylko hash
41+
42+
Flow logowania:
43+
44+
1. Hasło jako łańcuch znaków (plaintext)
45+
2. Hashowane za pomocą wyznaczonej funkcji
46+
3. Wyciągamy hash hasła z bazy danych
47+
4. Porównujemy dwie wartości do siebie
48+
49+
Charakterystyka każdej poprawniej funkcji hashującej gwarantuje nam unikalny hash dla każdego ciągu znaków, i zawsze ten sam hash dla tego samego ciągu znaków.
50+
51+
Pozostaje nam pytanie, jaki hash wybrać? Najpopularniejszą opcją dla haseł jest funkcja haszująca “bcrypt” wynaleziony w 1999 roku.
52+
Złożoność hashu powstałego z tej funkcji można modyfikować wybierając liczbę rund hashowania - implementacja w JS (node.bcrypt.js), jako wartość domyślną, wybiera 10 rund,
53+
a wartością maksymalną jest 20. Biblioteka ma bardzo proste API, przykładowo, do sprawdzenia czy hasło zgadza się z hashem wystarczy użycie funkcji `compare`:
54+
55+
```ts
56+
async signIn(email: string, password: string): Promise<LoginResponseDto> {
57+
const user = await this.usersService.findOne(email);
58+
if (user === null || !await compare(password, user.password).catch(() => false)) {
59+
throw new UnauthorizedException();
60+
}
61+
return {token: this.generateToken(user.email)};
62+
}
63+
```
64+
65+
### 2. „Endpoint to koniec drogi zapytania - Interceptors”
66+
67+
Jednak użytkownik może wysyłać wiele zapytań, a przesyłanie i weryfikowanie hasła za każdym razem stwarza zagrożenie bezpieczeństwa -
68+
powinniśmy dążyć do tego, by hasło było przesyłane jak najrzadszej. Tutaj z pomocą przychodzą ciastka i headery, w których możemy zawrzeć
69+
dodatkowe informacje, automatycznie, nie robiąc trudu użytkownikowi. O tym czym są ciastka i headery było opowiedziane, w poprzednich etapach
70+
kursu, dlatego już bez zbędnego przedłużania, zastanówmy się jak można ich użyć do autoryzacji. Najprostszym i najlepszym pomysłem jest następujący
71+
flow zapytania:
72+
73+
Flow każdego zapytania wymagającego autoryzacji:
74+
75+
1. Klient automatycznie dodaje unikalną informację do każdego zapytania
76+
2. Weryfikujemy tą informację na serwerze
77+
3. Zapytanie przechodzi dalej lub zostaje odrzucone
78+
79+
Z pomocą tutaj przychodzą *interceptory*, zwane czasami *filtrami*, czy *guardami*. Idea jest jedna - zareaguj na, przekształć, lub odrzuć zapytanie.
80+
81+
![interceptors](../../../../../assets/nestjs/auth/int.png)
82+
83+
My zajmiemy się tzw. Guards, które działają jak służba celna - mają na celu zbadać, czy zapytanie może wejść do naszego endpoint’u. Powstaje jednak pytanie, co badać i jak badać?
84+
85+
Temat jest bardzo obszerny, ale my skupimy się na jednym z najpopularniejszych i
86+
nie wymagającym utrzymania żadnego stanu na naszym serwerze rozwiązaniu - dzięki temu nie będzie problemów z utrzymaniem sesji.
87+
88+
Podstawowe kryteria:
89+
- unikalna wartość dla każdego użytkownika
90+
- z wartości można uzyskać identyfikator użytkownika
91+
- wartość jest przekazana klientowi podczas logowania
92+
93+
Dodatkowo (dzisiaj się tym nie zajmujemy):
94+
95+
- wartość powinna być unikalna dla każdego logowania
96+
- wartość powinna mieć swój czas ważności
97+
- wartość powinna być zaenkryptowana by uniemożliwić zmianę danych w niej zawartych
98+
99+
Wady rozwiązania:
100+
101+
- ukradziona wartość pozwala się bez żadnych problemów podszyć pod właściciela wartości
102+
- brak wiedzy o zalogowanych urządzeniach (wartość nie jest zależna od urządzenia)
103+
104+
Oczywiście, nawet te problemy mają swoje rozwiązania, ale w tym poradniku nie będziemy się nimi przejmować. Poniżej znajduje się najprostsze rozwiązanie
105+
spełniające trzy podstawowe kryteria (**oczywiście nie jest ono w żadnym wypadku bezpiecznie - jedynie poglądowe**)
106+
107+
```ts
108+
// Walidacja tokenów
109+
async validateToken(token: string): Promise<UserMetadata> {
110+
return token.startsWith(this.tokenPrefix) ?
111+
await this.usersService.findMetadataOrFail(token.slice(this.tokenPrefix.length))
112+
: Promise.reject(new Error("Invalid token"));
113+
}
114+
// Generowanie tokenów
115+
generateToken(email: string): string {
116+
return `${this.tokenPrefix}${email}`;
117+
}
118+
```
119+
120+
W NestJS, każdy *Guard* implementuje metodę `canActivate` zwracającą wartości boolean. Jeżeli metoda zwróci fałsz lub rzuci wyjątek,
121+
zapytanie jest odrzucane. W przypadku wartości `true`, zapytanie przechodzi do następnego interceptora, lub, jeżeli już nie ma żadnego w kolejce, do naszego
122+
endpointu.
123+
124+
```ts
125+
// Autoryzacja zapytania w interceptorze
126+
async canActivate(context: ExecutionContext): Promise<boolean> {
127+
const request: RequestWithUser = context.switchToHttp().getRequest();
128+
const token = this.extractTokenFromHeader(request);
129+
if (token === undefined) {
130+
throw new UnauthorizedException("Missing token");
131+
}
132+
try {
133+
request.user = await this.authService.validateToken(token);
134+
} catch (error) {
135+
throw new UnauthorizedException((error as Error).message);
136+
}
137+
return true;
138+
}
139+
```
140+
141+
### 3. „Nie zawsze wszystko jest dla wszystkich, czyli po co dzielić nasze endpointy?”
142+
143+
Przy podstawowej konfiguracji w NestJS, każdy endpoint z adnotacją `@UseGuards(AuthGuard)` staje się prywatny. Analogicznie, wszystkie bez tej adnotacji są publiczne.
144+
145+
Często wymaganie jest tzw. "Read-Only Public API", czyli API, które jest dostępne publicznie dla każdego do odczytu, ale jakiekolwiek modyfikacje
146+
wymagają dodatkowych uprawinień.
147+
148+
![readonlyAPI](../../../../../assets/nestjs/auth/readonlyAPI.png)
149+
150+
### 4. “Wspólne i nasze – Public & Private API pattern”
151+
152+
Aby ułatwić i rozgraniczyć API, z pomocą przychodzi wzorzec "Public & Private API". Podczas kursu nie będziemy używać tego wzorca, ponieważ łamie on niektóre zasady REST. Wspominam o nim, ponieważ jest czasami używany i osobiście jestem jego fanem 👍
153+
154+
Idea jest prosta:
155+
156+
- wszystkie endpointu zaczynające się z prefixem `/public` lub `/na` (non-authenticated), itp. są dostępne publiczne
157+
- wszystkie inne są prywatne
158+
159+
![readonlyAPI](../../../../../assets/nestjs/auth/readonlyAPI2.png)
160+
161+
Zalety wzorca to m.in. łatwe instalowanie interceptorów oraz wiedza co wymaga zalogowania, a co nie, bez zagłębiania się w dokumentację.
162+
Jako wady można wymienić, obecność tych samych zasobów pod innymi ścieżkami, czy np. dwa razy więcej kontrolerów.
163+
164+
### 5. „Ta część jest tylko dla mnie, czyli RBAC”
165+
166+
Czasami jednak samo zalogowanie się na konto nie wystarczy, albowiem nie każdy użytkownik jest równy. Pojawia się nowe pojęcie, często mylone z tym przez Was już znanym,
167+
czyli Authentication i Authorization.
168+
169+
Authentication, to sprawdzenia kim jest dany użytkownik. Można rozumieć to jako przedstawienie się. Udowodnienie, że nie jest się obcym. Innymi słowy, zalogowanie się.
170+
Authorization, to sprawdzenie jakie uprawnienia ma dany użytkownik. Tutaj metaforom może być sprawdzenie jakie ma wykształcenie, gdzie pracuje, ile ma lat.
171+
172+
W aplikacjach problem uprawnień często się pojawia. Najprostszym przykładem będzie użytkownik zwykły oraz użytkownik Administrator. Najprostsze rozwiązanie (ale nie najlepsze), to **RBAC**, skrót od **Role Based Access Control**.
173+
Każdy użytkownik ma jedną, lub kilka, przypisanych roli, a do niektórych działań wymagane są poszczególne role.
174+
175+
![interceptors2](../../../../../assets/nestjs/auth/int2.png)
176+
177+
Warto też zaznaczyć różnicę pomiędzy kodem błędu 401 i 403. 401 oznacza, że serwer nie wie kim jesteś. 403 natomiast, oznacza, że serwer wie kim jesteś, ale nie ma uprawnień by zrobić to co chciałeś.
178+
179+
Implementacja podstawowego RBAC w NestJS jest prosta. Załóżmy, że każdy użytkownik ma przypisaną jedną rolę.
180+
181+
```ts
182+
// RoleGuard, interceptor, który sprawdza uprawnienia
183+
@Injectable()
184+
export class RoleGuard implements CanActivate {
185+
constructor(private reflector: Reflector) {
186+
}
187+
188+
canActivate(context: ExecutionContext): boolean {
189+
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
190+
context.getHandler(),
191+
context.getClass(),
192+
]);
193+
if (requiredRoles.length === 0) {
194+
return true;
195+
}
196+
const request: RequestWithUser = context.switchToHttp().getRequest();
197+
return request.user !== undefined && requiredRoles.includes(request.user.role);
198+
}
199+
}
200+
// dekorator (adnotacja) do endpointów
201+
export const ROLES_KEY = 'roles';
202+
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
203+
```
204+
205+
Użycie takiego interceptora jest dość intuicyjne i proste. Wystarczy dodać adnotację `@UserGuards` nad endpointem, tak jak w przypadku naszego "AuthGuard",
206+
oraz adnotację `@Roles` z naszego dekoratora z wymienionymi rolami. Przykładowo, endpoint poniżej jest dostępny jedynie dla administratora.
207+
208+
```ts
209+
@UseGuards(AuthGuard, RoleGuard)
210+
@Roles(Role.ADMIN)
211+
async disableUser(@Param('email') email: string) {
212+
return this.userService.disableAccount(email);
213+
}
214+
```
215+
216+
Pozostaje jeszcze odpowiedź na pytanie, czemu takie rozwiązanie nie jest najlepsze. Powodem jest ilość ról, a dokładniej nakładanie się uprawnień.
217+
Przykład poniżej pochodzi z jednego z projektów, które pisałem (kod jest z Javy):
218+
219+
```java
220+
enum Permission {
221+
ADMIN(128),
222+
TEAM_OWNER(64),
223+
DRIVER(32)
224+
RACE(16),
225+
TASK(8),
226+
EVENT_SEND(4),
227+
EVENT_RECEIVE(2),
228+
USER(1),
229+
BLOCKED(0);
230+
}
231+
```
232+
233+
Co jeżeli użytkownik może wysyłać eventy, nie może ich odbierać, jest kierowcą, użytkownikiem, może zarządzać zadaniami, ale nie może zarządzać wyścigiem?
234+
235+
Rozwiązania są dwa:
236+
237+
1. Pozwalamy aby jeden użytkownik miał kilka roli (dodatkowe tabele w bazie danych i overhead na bardzo częstej operacji)
238+
2. Tworzymy hybrydowe role: EventSendDriverTaskUser... (i wszystkie możliwe kombinacje?)
239+
240+
Żadne z nich nie jest dobre - są to limitacje prostego RBAC. Najlepszym rozwiązaniem jest zmiany systemu kontroli na inny (np. ACL).
241+
Czasami jednak trzeba customowy system samemu napisać, bo większość frameworków oferuje jedynie wbudowane implementacje dla RBAC.
242+
243+
### 6. „Nie lubimy obcych - CORS”
244+
245+
Kilka słów o **CORS** (skrót od **Cross-Origin Resource Sharing**).
246+
247+
Jest to mechanizm dla przeglądarek internetowych, który kontroluje czy dana domena może uzyskać zasoby od innej. Przed wysłaniem prawdziwego
248+
zapytania, klient wysyła zapytanie typu OPTIONS, tzw. "preflight request" z danymi klienta, które trafia do pierwszego interceptora na serwerze. Tylko, i tylko wtedy gdy, odpowiedź
249+
na to zapytanie jest pozytywna, to klient wysyła drugie, już właściwe, zapytanie.
250+
251+
![cors](../../../../../assets/nestjs/auth/cors.png)
252+
253+
Konfiguracja CORS w NestJS jest bardzo prosta i wymaga w zasadzie jednej linijki kodu:
254+
255+
```ts
256+
/// main.ts
257+
app.enableCors({
258+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
259+
origin: 'http://localhost:5500',
260+
preflightContinue: false,
261+
});
262+
```
263+
Określamy jakie typy zapytań są dopuszczalne, i jakie źródła akceptujemy.
264+
265+
### 7. „Nasze 500, to dalej 500 – Walidatory”
266+
267+
Nieodłączną częścią każdego API jest walidacja danych wejściowych. Pozwala to nie tylko uniknąć nieprawidłowych danych, ale również
268+
uchronić nas przed nieoczekiwanymi błędami. NestJS bez problemów wspiera różne techniki walidacji danych, a zaczęcie wymaga jedynie instalacji
269+
pakietów: `npm i --save class-validator class-transformer`.
270+
271+
#### Pipes
272+
273+
Pipe (dosłowne, ale raczej nie używane tłumaczenie - "rura"), to element biblioteki `class-transformer`. Celem jest transformacja i jednoczenie
274+
walidacja danych. Spójrz na przykład poniżej używający wbudowanej `ParseIntPipe`:
275+
276+
```ts
277+
@Get(":id")
278+
async findOne(@Param("id", ParseIntPipe) id: number) {
279+
return this.tripService.findOne(id);
280+
}
281+
282+
@Patch(":id")
283+
async update(@Param("id") id: string, @Body() updateTripDto: UpdateTripDto) {
284+
return this.tripService.update(+id, updateTripDto);
285+
}
286+
```
287+
Załóżmy, że klient wyśle zapytanie, gdzie parametr `id` nie będzie liczbą.
288+
289+
Pierwsze rozwiązanie (z `ParseIntPipe`), zwróci do klienta odpowiedź z kodem 400 i informacją o ty, że `id` musi być liczbą całkowitą.
290+
Drugie zaś, wyrzuci kod błędu 500, bo serwer nie może zamienić nie-liczby na liczbę za pomocą sztuczki z `+`.
291+
292+
### Walidacja DTO
293+
294+
Walidacja DTO jest również prosta. Nad każdym z pól wymagających walidacji umieszczamy odpowiednie dekoratory. W przypadku, gdy walidacja zakończy się
295+
błędem, użytkownik dostanie odpowiedź z kodem 400 i, opcjonalnie, mniej lub bardzie szczegółową wiadomością o błędzie - zależy to od nas.
296+
Za pomocą wbudowanych walidatorów można sprawdzić większość potrzebnych kryteriów - nazwa walidatora przekazuje co dany walidator sprawdza.
297+
298+
```ts
299+
export class CreateTripDto {
300+
@IsString()
301+
@Length(3, 255)
302+
name: string;
303+
304+
@IsOptional()
305+
@IsNumber()
306+
@Min(0)
307+
plannedBudget?: number;
308+
}
309+
```
310+
311+
Jeżeli wbudowane walidatory nie wystarczą, możemy zawsze stworzyć swój własny. Załóżmy, że celem jest moderacja danych użytkownika - jego status
312+
nie może zawierać pewnych słów.
313+
314+
```ts
315+
// nice-text.validator.ts
316+
@ValidatorConstraint({name: 'niceText', async: false})
317+
export class NiceText implements ValidatorConstraintInterface {
318+
validate(text: string, _: ValidationArguments) {
319+
return !text.includes("loser");
320+
}
321+
322+
defaultMessage(_: ValidationArguments) {
323+
return "That's not a nice text, is it?";
324+
}
325+
}
326+
```
327+
328+
Nasz walidator zawiera metodę `validate`, która zwraca fałsz jeżeli niedozwolone słowo zostanie wykryte, oraz metodę `defaultMessage` z informacją dla klienta o tym co poszło nie tak.
329+
Sposób użycia customowego walidatora jest taki sam jak tych wbudowanych, poprzez dekorator `@Validate(NazwaKlasyWalidatora):
330+
331+
```ts
332+
export class UserUpdateDto {
333+
@IsOptional()
334+
@IsString()
335+
@MaxLength(30)
336+
@Validate(NiceText)
337+
newAboutMe?: string | null;
338+
@IsOptional()
339+
@MaxLength(15)
340+
@IsOptional()
341+
@IsString()
342+
name?: string | null;
343+
}
344+
```
345+
346+
## Zadanie do wykonania
347+
348+
Zadanie domowe do wykonania znajduje się na [głównych repozytorium](https://github.com/Solvro/backend-wakacyjne-wyzwanie-2025/blob/main/4.%20Autoryzacja%20i%20walidatory/4.md)
349+
350+
## Materiały
351+
352+
- Link do [nagrania prezentacji](https://drive.google.com/file/d/136PmaiBlXc1wRuipbGF2iG7x-D8HI-r4/view?usp=sharing) z wykładu
353+
- Slajdy z prezentacji
354+
- Repozytorium z kodem przedstawionym podczas prezentacji: [link do repozytorium](https://github.com/Solvro/backend-wakacyjne-wyzwanie-2025/blob/main/4.%20Autoryzacja%20i%20walidatory/4.md)
355+
- https://docs.nestjs.com/security
356+
- https://github.com/kelektiv/node.bcrypt.js#readme
357+
- https://github.com/expressjs/cors#configuration-options
358+
- https://github.com/typestack/class-validator (walidatory używane przez Nest'a)

0 commit comments

Comments
 (0)