Toate ghidurile

Autentificarea pe web: sesiuni, JWT, OAuth 2.0 și passkeys

Cum funcționează sesiunile, JWT, OAuth 2.0, OpenID Connect și passkeys, și când îl alegi pe fiecare.


Aproape orice aplicație web are un formular de login, iar în spatele lui se află una dintre cele mai sensibile părți ale codului. Sesiuni, JWT, OAuth 2.0, OpenID Connect, passkeys: termenii sunt mulți și adesea folosiți greșit, chiar și de developeri cu experiență. Ghidul de față explică ce face fiecare, cum funcționează concret și când îl alegi.

Autentificare vs. autorizare

  • Autentificarea răspunde la întrebarea „cine ești?”: parolă, cod, cheie criptografică, cont Google.
  • Autorizarea răspunde la întrebarea „ce ai voie să faci?”: roluri, permisiuni, proprietatea asupra resurselor.

Ordinea este mereu aceeași: întâi identifici utilizatorul, apoi verifici ce poate face. Codurile HTTP reflectă distincția: 401 Unauthorized, în ciuda numelui, înseamnă că lipsește autentificarea, iar 403 Forbidden înseamnă că lipsește dreptul.

Parolele: cum le stochezi corect

Chiar dacă adaugi passkeys sau login cu Google, cel mai probabil vei avea și parole. Regula de bază: nu stochezi niciodată parola, ci un hash al ei, calculat cu un algoritm conceput special pentru parole, intenționat lent și cu salt unic pentru fiecare utilizator.

AlgoritmVerdict
Argon2idPrima opțiune recomandată de OWASP
scryptBun
bcryptAcceptabil și foarte răspândit; atenție la limita de 72 de octeți pentru parolă
PBKDF2Acceptabil când ai nevoie de conformitate FIPS
MD5, SHA-1, SHA-256 simpluNiciodată pentru parole: sunt prea rapide

Funcțiile de hash obișnuite sunt proiectate să fie rapide, ceea ce ajută un atacator care a furat baza de date să încerce miliarde de parole pe GPU. Argon2id și ceilalți algoritmi din tabel sunt intenționat costisitori în timp și memorie.

În Node.js, cu pachetul argon2:

import * as argon2 from 'argon2';

// La înregistrare: Argon2id, cu salt aleator inclus în hash
const passwordHash = await argon2.hash(password);
await db.users.insert({ email, passwordHash });

// La login
const ok = await argon2.verify(user.passwordHash, password);

Alte reguli pentru parole:

  • Permite parole lungi (cel puțin 64 de caractere) și nu impune reguli artificiale de tipul „o majusculă, o cifră, un simbol”. Lungimea contează mai mult.
  • Verifică parolele noi față de liste de parole compromise.
  • Limitează încercările de login, per cont și per IP, ca să frânezi atacurile de tip brute force și credential stuffing.
  • Returnează același mesaj pentru „utilizator inexistent” și „parolă greșită”, ca să nu dezvălui ce conturi există.

Toate detaliile sunt în OWASP Password Storage Cheat Sheet.

Sesiuni pe server, cu cookie-uri

Modelul clasic, care rămâne o alegere excelentă pentru aplicațiile web:

  1. Utilizatorul trimite e-mailul și parola.
  2. Serverul verifică hash-ul, generează un ID de sesiune lung și aleator și salvează sesiunea, în baza de date sau în Redis, împreună cu userId, data expirării și alte informații.
  3. ID-ul este trimis browserului într-un cookie.
  4. La fiecare cerere, browserul trimite automat cookie-ul, serverul caută sesiunea și știe cine ești.
HTTP/1.1 200 OK
Set-Cookie: __Host-session=k3J9xQ...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1209600

Fiecare atribut are un rol:

  • HttpOnly: JavaScript nu poate citi cookie-ul, deci un atac XSS nu îl poate fura direct.
  • Secure: cookie-ul circulă doar pe HTTPS.
  • SameSite=Lax: cookie-ul nu este trimis la majoritatea cererilor inițiate de alte site-uri, ceea ce reduce mult riscul de CSRF. Strict este și mai restrictiv.
  • Prefixul __Host-: browserul acceptă cookie-ul doar dacă are Secure și Path=/ și nu are atributul Domain, deci nu poate fi suprascris de pe un subdomeniu.
  • Max-Age=1209600: expiră după 14 zile.

Avantajul major al sesiunilor este controlul. Logout înseamnă ștergerea sesiunii de pe server, iar „deconectează-mă de pe toate dispozitivele” sau blocarea unui cont compromis au efect imediat. Două reguli de implementare: generează un ID de sesiune nou la login (altfel ești vulnerabil la session fixation) și setează atât o expirare la inactivitate, cât și una absolută.

JWT: token-uri auto-conținute

Un JSON Web Token are trei părți codificate base64url și separate prin puncte: header.payload.signature. Header-ul spune cum a fost semnat token-ul:

{ "alg": "ES256", "typ": "JWT" }

Payload-ul conține afirmațiile (claims) despre utilizator:

{
  "iss": "https://auth.example.com",
  "sub": "user_1029",
  "aud": "api.example.com",
  "iat": 1792800000,
  "exp": 1792800900,
  "role": "student"
}

Serverul semnează token-ul cu o cheie secretă (HS256) sau cu o cheie privată (RS256, ES256). Oricine are token-ul îi poate citi conținutul, pentru că este doar codificat, nu criptat, dar nimeni nu îl poate modifica fără să invalideze semnătura. De aici o regulă simplă: nu pune în JWT date sensibile.

Avantajul: serverul nu trebuie să caute nimic într-o bază de date, verificarea semnăturii este suficientă. De aceea JWT-urile sunt populare între servicii și pentru API-uri folosite de aplicații mobile.

Dezavantajul principal este revocarea. Un JWT rămâne valid până la exp, chiar dacă utilizatorul a dat logout sau contul i-a fost blocat. Soluția uzuală:

  • un access token cu viață scurtă, de 5-15 minute;
  • un refresh token cu viață mai lungă, ținut în evidență pe server, cu rotație: la fiecare folosire se emite unul nou și se invalidează cel vechi. Dacă un refresh token deja folosit apare din nou, înseamnă că a fost furat și revoci toată familia de token-uri.

Observă că în acest punct ai reintrodus starea pe server. Nu este o problemă, dar arată că JWT nu este automat „mai simplu” decât sesiunile.

Verificarea corectă a unui token, cu biblioteca jose:

import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json')
);

const { payload } = await jwtVerify(token, JWKS, {
  issuer: 'https://auth.example.com',
  audience: 'api.example.com',
  algorithms: ['ES256'],
});

Greșeli clasice: acceptarea algoritmului none, lipsa verificării pentru exp, iss și aud, chei HS256 scurte care pot fi ghicite și stocarea token-ului în localStorage, unde orice script injectat prin XSS îl poate citi. Pentru aplicațiile din browser, un cookie HttpOnly este de obicei locul mai sigur.

Sesiuni sau JWT?

CriteriuSesiuni pe serverJWT
Unde e stareaPe server (bază de date, Redis)În token, la client
RevocareImediatăGrea; necesită expirare scurtă și refresh token
Cost per cerereO căutare în storeVerificarea unei semnături
Potrivit pentruAplicații web, monolit, BFFAPI-uri între servicii, clienți mobili, SSO
Risc tipicCSRF (redus mult de SameSite)Furt de token, revocare întârziată

Pentru o aplicație web obișnuită, cu frontend-ul și backend-ul pe același domeniu, sesiunile cu cookie-uri sunt alegerea mai simplă și mai sigură. Dacă frontend-ul este o aplicație separată, un BFF (Backend for Frontend) poate ține token-urile pe server și poate oferi browserului tot un cookie de sesiune.

OAuth 2.0 și OpenID Connect

OAuth 2.0 este un protocol de autorizare delegată: permite unei aplicații să acceseze resurse în numele unui utilizator fără să-i afle parola. Exemplul clasic este o aplicație care cere acces la calendarul tău Google. OpenID Connect (OIDC) este un strat peste OAuth 2.0 care adaugă autentificarea, printr-un ID token (un JWT) care spune cine este utilizatorul. Butoanele „Continuă cu Google” sau „Continuă cu GitHub” se bazează pe aceste protocoale.

Rolurile din OAuth:

  • Resource owner: utilizatorul.
  • Client: aplicația ta.
  • Authorization server: cel care autentifică utilizatorul și emite token-urile (Google, Microsoft Entra ID, Keycloak, Auth0 și altele).
  • Resource server: API-ul care acceptă access token-ul.

Fluxul recomandat astăzi pentru aproape toate tipurile de aplicații este Authorization Code cu PKCE:

  1. Aplicația generează un code_verifier aleator și calculează code_challenge = BASE64URL(SHA256(code_verifier)).
  2. Redirecționează utilizatorul către authorization server (cererea este ruptă pe mai multe rânduri pentru lizibilitate):
GET /authorize?response_type=code
  &client_id=app-web
  &redirect_uri=https://app.example.com/callback
  &scope=openid%20email%20profile
  &state=af0ifjsldkj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256 HTTP/1.1
Host: auth.example.com
  1. Utilizatorul se autentifică pe pagina providerului și își dă consimțământul.
  2. Authorization server-ul redirecționează înapoi la redirect_uri cu un code de unică folosință și cu același state, pe care îl compari cu cel trimis, ca protecție împotriva CSRF.
  3. Aplicația schimbă codul pe token-uri, trimițând și code_verifier:
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&client_id=app-web&code_verifier=...
  1. Primește un access_token, eventual un refresh_token și, cu OIDC, un id_token.

PKCE garantează că doar aplicația care a pornit fluxul poate folosi codul, chiar dacă acesta este interceptat. Fluxurile Implicit și Resource Owner Password Credentials sunt descurajate de bunele practici actuale de securitate și nu mai apar în OAuth 2.1, versiunea consolidată a standardului. O prezentare bună a întregului ecosistem găsești pe oauth.net.

Sfat practic: nu implementa singur un authorization server. Folosește un provider gestionat sau o soluție open source matură, precum Keycloak, iar în aplicație o bibliotecă OIDC certificată.

Passkeys: autentificare fără parole

Passkeys sunt credențiale bazate pe standardele WebAuthn și FIDO2, care înlocuiesc parola cu criptografie cu cheie publică:

  • La înregistrare, dispozitivul (telefonul, laptopul sau o cheie de securitate hardware) generează o pereche de chei dedicată site-ului tău. Cheia privată rămâne pe dispozitiv sau în managerul de parole al utilizatorului; serverul primește și stochează doar cheia publică.
  • La login, serverul trimite un challenge aleator. Dispozitivul îl semnează cu cheia privată după ce utilizatorul se deblochează cu amprenta, fața sau PIN-ul, iar serverul verifică semnătura cu cheia publică.

De ce sunt mai sigure decât parolele:

  • Rezistente la phishing. Credențialul este legat de domeniul site-ului (RP ID), deci un site fals, aflat pe alt domeniu, nu îl poate folosi.
  • Nimic de furat de pe server. O bază de date cu chei publice nu îi folosește la nimic unui atacator.
  • Nimic de reutilizat. Fiecare site are propria pereche de chei.

Sincronizarea passkeys între dispozitive, prin iCloud Keychain, Google Password Manager sau manageri de parole precum 1Password și Bitwarden, a rezolvat în mare parte problema „ce fac dacă îmi pierd telefonul”. Toate browserele majore suportă WebAuthn.

În browser, API-ul de bază arată așa, cu opțiunile generate de server:

// Înregistrare: creează o pereche de chei nouă
const credential = await navigator.credentials.create({ publicKey: creationOptions });

// Autentificare: semnează challenge-ul primit de la server
const assertion = await navigator.credentials.get({ publicKey: requestOptions });

Verificarea pe server implică multe detalii (challenge, origine, contor de semnături, formate de atestare), așa că folosește o bibliotecă testată, de exemplu SimpleWebAuthn pentru Node.js. Documentația API-ului este pe MDN: Web Authentication API. În practică, oferă passkeys ca opțiune alături de metodele existente și păstrează un proces sigur de recuperare a contului.

Autentificarea multi-factor (MFA)

Dacă folosești parole, MFA ar trebui să fie disponibil pentru toți utilizatorii și obligatoriu pentru conturile cu privilegii:

  • TOTP, codurile de 6 cifre din aplicațiile de tip authenticator: o alegere solidă și ieftin de implementat.
  • SMS: mai bine decât nimic, dar vulnerabil la SIM swapping și la phishing.
  • Passkeys și cheile de securitate: cea mai sigură variantă, rezistentă la phishing.

Ce alegi în practică

SituațieRecomandare
Aplicație web, frontend și backend pe același domeniuSesiuni cu cookie HttpOnly, Secure, SameSite
SPA cu API pe alt domeniuBFF cu sesiune, sau OIDC cu Authorization Code + PKCE
Aplicație mobilăOIDC cu Authorization Code + PKCE, token-uri în stocarea securizată a sistemului
Comunicare între serviciiToken-uri cu viață scurtă (OAuth client credentials) sau mTLS
„Login cu Google/GitHub”OpenID Connect, printr-o bibliotecă
Securitate maximă pentru utilizatoriPasskeys, cu parolă + MFA ca alternativă

Pașii următori

  • Implementează de la zero un login cu sesiuni (înregistrare, hash Argon2id, cookie configurat corect, logout), apoi adaugă login cu GitHub prin OIDC folosind o bibliotecă.
  • Aprofundează partea de server în roadmap-ul Backend sau, dacă scrii JavaScript, în roadmap-ul Node.js.
  • Pentru perspectiva atacatorului, adică exact cum sunt exploatate greșelile de mai sus, continuă cu roadmap-ul Cyber Security.