Seguretat i Autenticació JWT
Mòdul: Autenticació
Versió: 1.0
Actualitzat: 2024-12
Referència: Veure també Security Playbook
Aquest document descriu com funciona el sistema de seguretat i autenticació amb JWT a la Line Dance Platform (LDP), pensat perquè:
- sigui stateless (sense sessió de servidor),
- admeti diversos clients (web, mòbil, futurs),
- permeti revocar tokens de forma controlada.
1. Visió general
1.1. Per què JWT?
- Tenim backend REST i clients diversos (web, Android, futurs).
- No volem gestionar sessió de servidor (HttpSession).
- JWT permet:
- enviar identitat i rol a cada petició,
- escalar fàcilment el backend,
- evitar dependència d'un "state" centralitzat al servidor.
1.2. Conceptes clau
-
Access Token:
- s'envia a cada petició protegida (header Authorization),
- durada curta (per exemple uns 15 minuts).
-
Refresh Token:
- s'envia només a /auth/refresh,
- es guarda com a cookie HttpOnly (no accessible per JavaScript),
- durada més llarga (per exemple 7 dies).
Els dos tokens contenen un identificador de versió de token (tokenVersion) per poder invalidar tots els tokens d'un usuari.
2. Arquitectura de seguretat
2.1. Components principals
-
Entitat User (BD):
- id
- email (únic, en minúscules)
- passwordHash (BCrypt)
- role (USER, TEACHER, ADMIN)
- tokenVersion (enter)
- created_at, updated_at
-
JWT Utils:
- genera access i refresh tokens,
- valida signatura i expiració,
- llegeix claims (sub, uid, role, tv, exp…).
-
Spring Security:
- SecurityFilterChain:
- defineix rutes públiques i protegides,
- marca la sessió com STATELESS.
- Filtre JwtAuthFilter:
- llegeix el header Authorization,
- valida el token,
- carrega Authentication al SecurityContext.
- SecurityFilterChain:
3. Access Token vs Refresh Token
3.1. Access Token
-
Va al header Authorization, per exemple:
Authorization: Bearer ACCESS_TOKEN
-
Durada curta (minuts).
-
Inclou, com a mínim:
- sub (email)
- uid (id d'usuari)
- role
- tv (tokenVersion)
- exp (caducitat)
Si l'access token caduca, el client ha de demanar un token nou via /auth/refresh.
3.2. Refresh Token
- Es retorna en una cookie HttpOnly, per exemple:
- nom: refreshToken
- path: /auth
- HttpOnly: true
- Secure: true (en producció)
- Durada més llarga (dies).
- Només s'utilitza a:
- POST /auth/refresh
4. Fluxos principals
4.1. Registre d'usuari (POST /auth/register)
- El client envia email i password.
- El backend:
- valida format d'email i longitud de password,
- genera passwordHash amb BCrypt,
- crea usuari amb role = USER i tokenVersion = 0.
- Opcionalment:
- pot retornar access token directament,
- o bé forçar que es faci login en una petició separada.
4.2. Login (POST /auth/login)
- El client envia email i password.
- El backend:
- busca usuari per email,
- compara la password amb passwordHash,
- si és correcte, crea:
- un access token (TTL curt),
- un refresh token (TTL més llarg).
- Resposta:
- JSON amb dades d'usuari i accessToken,
- cookie HttpOnly amb refreshToken.
4.3. Peticions autenticades (/api/**)
El client inclou el header:
Authorization: Bearer ACCESS_TOKEN
El filtre JWT:
- valida el token,
- comprova expiració (exp),
- comprova que tokenVersion del token coincideix amb user.tokenVersion a BD.
Si tot és correcte:
- es carrega Authentication amb el rol corresponent,
- el controlador pot accedir a l'usuari autenticat.
4.4. Refresh (POST /auth/refresh)
- El client fa petició a /auth/refresh amb la cookie refreshToken.
- El backend:
- llegeix i valida el refresh token,
- comprova expiració,
- comprova tokenVersion.
- Si tot és correcte:
- genera un nou access token,
- opcionalment renova també el refresh token.
- Resposta:
- nou accessToken en JSON,
- cookie actualitzada si cal.
4.5. Logout (POST /auth/logout i POST /auth/logoutAll)
-
/auth/logout:
- expira o esborra la cookie de refresh,
- l'access token actual caducarà de forma natural.
-
/auth/logoutAll:
- incrementa user.tokenVersion a BD,
- qualsevol access o refresh token existent amb tv antic queda invalidat.
4.6. Autenticació nativa mòbil (T-187 / T-105)
L'auth web desa el refresh token en una cookie HttpOnly, que un client natiu
(Android/iOS) no pot gestionar. Per això hi ha un canal paral·lel d'auth per a
apps, que no toca el web. Neix directament versionat a /api/v1/auth/...mobile
(sense variant /api legacy) i retorna el refresh token al body.
Diferències clau respecte al web:
| Aspecte | Web | Mòbil |
|---|---|---|
| On viu el refresh | Cookie HttpOnly (path=/auth) | Body de la resposta (Keystore/Keychain al client) |
| TTL del refresh | 7 dies | 90 dies (jwt.refreshTtlMobileSec) |
| Identitat del dispositiu | — | device_id (UUID generat pel client) + taula user_devices |
| Rotació del refresh | opcional | a cada refresh (hash SHA-256 a user_devices) |
| Logout d'un sol dispositiu | no | sí (/logout/mobile) |
Endpoints (records JSON):
POST /api/v1/auth/login/mobile(públic) — credencials + blocdevice. Valida les credencials reutilitzant la mateixa lògica que el web (lockout T-086, email verificat), fa UPSERT auser_devicesi retornaaccessToken+refreshToken(al body). Si eldevice_idés nou, envia l'email "Nou dispositiu connectat".POST /api/v1/auth/refresh/mobile(públic) —{refreshToken, deviceId}. Rota el refresh i aplica reuse detection (RFC 8252 §8.2): si arriba un refresh ja rotat, es revoca tot el dispositiu (COMPROMISED). També comprovatokenVersion(unlogoutAll/canvi de contrasenya revoca el dispositiu amb raóPASSWORD_CHANGE).POST /api/v1/auth/logout/mobile(autenticat) —{deviceId}. Revoca només aquest dispositiu (USER_LOGOUT); no bumpatokenVersion(no afecta web ni altres dispositius).
Codis d'error (ProblemDetail code):
code | HTTP | Quan |
|---|---|---|
AUTH_INVALID_DEVICE_ID | 400 | device_id no és un UUID |
AUTH_INVALID_PLATFORM | 400 | platform no és IOS/ANDROID |
AUTH_INVALID_REFRESH | 401 | refresh absent/expirat/sense fila activa o device_id no coincident |
AUTH_REFRESH_REUSE | 401 | reuse detection (refresh ja rotat) → device revocat |
AUTH_REVOKED | 401 | tokenVersion bumped (logoutAll/canvi password) → device revocat |
Credencials incorrectes, compte bloquejat i email no verificat reutilitzen els codis del
web (BAD_CREDENTIALS, USER_LOCKED, EMAIL_NOT_VERIFIED).
user_devices (migració V109): una fila per parella (user_id, device_id). Guarda
el hash del refresh (mai el token en clar), platform/os_version/app_version/
device_model (per a panell admin i force-update futurs), i l'estat de revocació
(revoked_at/revoked_reason). És la pedra angular del cicle de vida mòbil (push,
device limits per tier, panell admin — tasques futures).
El refresh mòbil no comprova
enabled/lockeddel compte: l'estat actiu s'imposa alJwtAuthenticationFiltera cada petició autenticada, així que un access token refrescat per a un compte inactiu no autoritza res.
5. Control d'accés per rols (RBAC)
5.1. Rols definits
- USER
- pot llegir dades públiques (i el que es defineixi per defecte).
- TEACHER
- pot gestionar determinats recursos (balls, llistes, etc.) segons permisos.
- ADMIN
- pot gestionar usuaris,
- pot aprovar ownership requests,
- pot modificar configuracions sensibles.
5.2. A nivell de codi
Es pot utilitzar, per exemple:
@PreAuthorize("hasRole('ADMIN')")
o bé configurar-ho a la SecurityFilterChain per ruta (/admin/**, etc.).
6. Configuració CORS (resum)
Per permetre que el frontend (dev o producció) cridi l'API:
6.1. Orígens permesos
- http://localhost:5173 (dev)
- domini de producció (quan existeixi)
6.2. Mètodes permesos
- GET, POST, PUT, DELETE, PATCH, OPTIONS
6.3. Headers
- Content-Type
- Authorization
6.4. Cookies / credencials
- allowCredentials = true si s'utilitzen cookies de refresh.
- Al client cal fer servir withCredentials = true (Axios/fetch).
6.5. Errors típics CORS
- Origen del frontend no inclòs a allowedOrigins.
- Falta allowCredentials o no s'està fent servir withCredentials al client.
- Preflight OPTIONS no configurat correctament.
7. Bones pràctiques
7.1. Passwords
- sempre amb BCrypt (o equivalent segur),
- no loguejar mai contrasenyes ni hash complet.
7.2. JWT secret
- guardar-lo com a variable d'entorn (JWT_SECRET),
- no posar-lo mai al repositori,
- utilitzar un valor llarg i difícil d'endevinar.
7.3. HTTPS
- obligatori en entorns públics (producció),
- protegeix tokens, cookies i la resta de trànsit.
7.4. Expiració
- tokens sense expiració són una mala idea,
- accessToken de durada curta,
- refreshToken amb durada moderada.
7.5. Logging
- loguejar errors d'autenticació (per exemple TOKEN_EXPIRED, BAD_CREDENTIALS),
- no loguejar tokens sencers ni dades sensibles.
8. Troubleshooting habitual
8.1. 401 Unauthorized amb token
Possibles causes:
- token caducat,
- header Authorization no s'envia correctament,
- secret JWT incorrecte entre entorns.
Solucions:
- regenerar el token,
- revisar el client (header),
- revisar configuració de secrets a l'entorn.
8.2. 403 Forbidden amb token vàlid
Possibles causes:
- usuari autenticat però sense rol suficient,
- @PreAuthorize massa restrictiu,
- configuració de rutes a SecurityFilterChain massa tancada.
Solucions:
- comprovar rol a BD,
- revisar condicions de seguretat als controllers,
- revisar mapping de rutes i configuració de seguretat.
8.3. Problemes amb refresh
Possibles causes:
- cookie de refreshToken no arriba (path, domini, CORS),
- cookie expirada,
- tokenVersion incrementat (logoutAll, reset…).
Solucions:
- revisar configuració de cookies,
- revisar CORS i withCredentials,
- comprovar si s'ha fet logoutAll o s'ha canviat tokenVersion.