Skip to main content

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.

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)

  1. El client envia email i password.
  2. El backend:
    • valida format d'email i longitud de password,
    • genera passwordHash amb BCrypt,
    • crea usuari amb role = USER i tokenVersion = 0.
  3. Opcionalment:
    • pot retornar access token directament,
    • o bé forçar que es faci login en una petició separada.

4.2. Login (POST /auth/login)

  1. El client envia email i password.
  2. 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).
  3. 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)

  1. El client fa petició a /auth/refresh amb la cookie refreshToken.
  2. El backend:
    • llegeix i valida el refresh token,
    • comprova expiració,
    • comprova tokenVersion.
  3. Si tot és correcte:
    • genera un nou access token,
    • opcionalment renova també el refresh token.
  4. 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:

AspecteWebMòbil
On viu el refreshCookie HttpOnly (path=/auth)Body de la resposta (Keystore/Keychain al client)
TTL del refresh7 dies90 dies (jwt.refreshTtlMobileSec)
Identitat del dispositiudevice_id (UUID generat pel client) + taula user_devices
Rotació del refreshopcionala cada refresh (hash SHA-256 a user_devices)
Logout d'un sol dispositiunosí (/logout/mobile)

Endpoints (records JSON):

  • POST /api/v1/auth/login/mobile (públic) — credencials + bloc device. Valida les credencials reutilitzant la mateixa lògica que el web (lockout T-086, email verificat), fa UPSERT a user_devices i retorna accessToken + refreshToken (al body). Si el device_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é comprova tokenVersion (un logoutAll/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 bumpa tokenVersion (no afecta web ni altres dispositius).

Codis d'error (ProblemDetail code):

codeHTTPQuan
AUTH_INVALID_DEVICE_ID400device_id no és un UUID
AUTH_INVALID_PLATFORM400platform no és IOS/ANDROID
AUTH_INVALID_REFRESH401refresh absent/expirat/sense fila activa o device_id no coincident
AUTH_REFRESH_REUSE401reuse detection (refresh ja rotat) → device revocat
AUTH_REVOKED401tokenVersion 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/locked del compte: l'estat actiu s'imposa al JwtAuthenticationFilter a 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

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.

9. Documents relacionats