Skip to content

Latest commit

 

History

History
253 lines (190 loc) · 9.82 KB

File metadata and controls

253 lines (190 loc) · 9.82 KB

Gestione dell'Autenticazione

Panoramica

CineBase utilizza un sistema di autenticazione basato su JWT (JSON Web Token) con refresh token. Il sistema ha subito un'evoluzione significativa dalla versione iniziale (token in localStorage) alla versione attuale (cookie HTTP-only per il refresh token).

Evoluzione del sistema

Versione iniziale (deprecata)

Nella prima implementazione, sia l'access token che il refresh token venivano memorizzati in localStorage:

  • localStorage.setItem('cb_access_token', data.accessToken)
  • localStorage.setItem('cb_refresh_token', data.refreshToken)

Problemi: vulnerabilità XSS (qualsiasi script nella pagina poteva leggere i token), nessuna protezione contro il furto di token.

Versione attuale

L'access token è in memoria JavaScript (Auth._accessToken), il refresh token è in un cookie HTTP-only (cinebase_refresh). I riferimenti a localStorage per i token vengono puliti durante login/refresh.

Flusso di autenticazione

Login

sequenceDiagram
    participant Browser
    participant API as FilmAPI
    participant DB as MariaDB

    Browser->>API: POST /api/auth/login
    Note right of Browser: body: email, password, deviceId
    API->>DB: Cerca utente per email normalizzata
    API->>API: BCrypt.Verify della password
    API->>API: Genera JWT access token
    API->>API: Genera refresh token (64 byte random)
    API->>DB: Salva hash SHA256 del refresh token
    API-->>Browser: 200 OK: accessToken + refreshToken + user
    Note right of API: Set-Cookie: cinebase_refresh (HttpOnly, Secure, SameSite=Strict)
    Browser->>Browser: Salva access token in memoria
    Browser->>Browser: Salva user info in localStorage
    Browser->>Browser: Rimuove eventuali token legacy da localStorage
    Browser->>Browser: dispatchEvent auth:changed
Loading

Richiesta API autenticata

sequenceDiagram
    participant Browser
    participant API as FilmAPI
    participant DB as MariaDB

    Browser->>API: GET /api/checkout/orders (Authorization: Bearer token)
    API->>API: Valida firma JWT, expiry, issuer, audience
    API->>API: Estrae claims: sub, email, role
    API-->>Browser: 200 OK

    Note over Browser: Se il token è scaduto...
    Browser->>API: GET /api/checkout/orders (token scaduto)
    API-->>Browser: 401 Unauthorized

    Note over Browser: api.js intercetta il 401
    Browser->>API: POST /api/auth/refresh (cookie + deviceId)
    API->>DB: Cerca refresh token per hash + deviceId
    API->>DB: Revoca il vecchio refresh token
    API->>API: Genera nuovo JWT + nuovo refresh token
    API->>DB: Salva nuovo refresh token hash
    API-->>Browser: 200 OK: nuovi token
    Note right of API: Set-Cookie: nuovo cinebase_refresh
    Browser->>Browser: Aggiorna access token in memoria
    Browser->>API: Ritenta GET /api/checkout/orders
    API-->>Browser: 200 OK
Loading

Refresh automatico all'avvio

sequenceDiagram
    participant Browser
    participant API as FilmAPI

    Note over Browser: Pagina caricata, Auth.getUser() non è null
    Browser->>Browser: Verifica token in memoria (assente o scaduto)
    Browser->>API: POST /api/auth/refresh (cookie auto-incluso + deviceId)
    API-->>Browser: 200 OK: nuovo access token
    Browser->>Browser: Aggiorna access token in memoria
    Browser->>Browser: dispatchEvent auth:ready
    Note over Browser: La pagina può caricare dati autenticati
Loading

Logout

sequenceDiagram
    participant Browser
    participant API as FilmAPI
    participant DB as MariaDB

    Browser->>API: POST /api/auth/logout (cookie + deviceId)
    API->>API: Legge refresh token dal cookie
    API->>DB: Revoca refresh token (RevokedAt = now)
    API-->>Browser: 200 OK: revoked true
    Note right of API: Set-Cookie: cinebase_refresh scaduto
    Browser->>Browser: Auth.clearAuth()
    Browser->>Browser: Rimuove token da memoria
    Browser->>Browser: Rimuove user da localStorage
Loading

Dettagli implementativi

Access Token (JWT)

Generato con HMAC-SHA256, payload:

{
  "sub": "42",
  "email": "utente@email.com",
  "role": "User",
  "nome": "Mario",
  "auth_version": "1",
  "iss": "CineBaseAPI",
  "aud": "CineBaseWeb",
  "exp": 1717000000
}
  • Scadenza: 15 minuti (configurabile via JWT_ACCESS_TOKEN_EXPIRY_MINUTES)
  • Firma: JWT_SECRET (minimo 32 byte)
  • Role mapping: il claim role viene mappato a ClaimsIdentity.Role nell'evento OnTokenValidated

Refresh Token

  • Generazione: 64 byte random (RandomNumberGenerator.GetBytes(64)) codificati Base64
  • Storage nel DB: SHA256 del valore raw (RefreshTokenProtector.HashToken())
  • Rotazione: ad ogni refresh, il vecchio token viene revocato e ne viene creato uno nuovo
  • Unicità: un solo refresh token attivo per coppia (userId, deviceId)
  • Scadenza: 7 giorni (configurabile via JWT_REFRESH_TOKEN_EXPIRY_DAYS)
  • Cleanup: RefreshTokenCleanupService elimina periodicamente i token scaduti/revocati

Cookie HTTP-only

// AuthCookieService.cs
new CookieOptions
{
    HttpOnly = true,           // Inaccessibile a JavaScript
    Secure = !isDevelopment,   // HTTPS only in produzione
    SameSite = SameSiteMode.Strict,  // Solo richieste same-site
    Path = "/api/auth",        // Limitato agli endpoint auth
    Expires = expiresAtUtc
};

Il cookie si chiama cinebase_refresh e viene automaticamente incluso dal browser in tutte le richieste a /api/auth/*.

Device ID

Ogni browser ha un device ID univoco generato con crypto.randomUUID() (o fallback deterministico), persistito in localStorage come cb_device_id. Permette di associare i refresh token a un dispositivo specifico e di avere sessioni multiple su dispositivi diversi.

Auth Version

Ogni utente ha un campo AuthVersion incrementato ad ogni cambio password. Il valore è incluso nel JWT. Se un token ha una versione inferiore, viene rigettato — questo invalida tutti i token esistenti dopo un cambio password.

Protezione CSRF

Gli endpoint sensibili (/auth/refresh, /auth/logout) verificano l'header Origin o Referer tramite AuthRequestGuard.HasAllowedOriginOrReferer(). Questo, combinato con SameSite=Strict, protegge contro CSRF.

Rate Limiting

Il rate limiter per gli endpoint di autenticazione usa una partition composita {IP}|{SHA256(email)} per distinguere utenti diversi dietro lo stesso IP (proxy aziendale, NAT). L'email viene estratta dal body JSON da un middleware asincrono prima di UseRateLimiter() e salvata in HttpContext.Items["RateLimitEmail"].

Richiesta con email

Policy Algoritmo Limite Finestra Segmenti
LoginRateLimit SlidingWindow 10 tentativi 60 secondi 4 (15s ciascuno)
ForgotPasswordRateLimit FixedWindow 3 tentativi 300 secondi
  • Login: SlidingWindowLimiter con 4 segmenti — i permessi si rigenerano gradualmente ogni 15s, evitando l'effetto "burst al confine" del FixedWindow.
  • Forgot-password: FixedWindowLimiter — il contatore si azzera completamente allo scadere della finestra.

Richiesta senza email (fallback anonimo)

Se il body non contiene un campo email valido, la partition diventa {IP}|__anon__ con limiti ridotti:

Policy Limite anonimo
LoginRateLimit 5 tentativi
ForgotPasswordRateLimit 1 tentativo

Risoluzione IP reale

L'IP del client viene letto dall'header X-Forwarded-For (primo IP a sinistra, compatibile con reverse proxy). In assenza dell'header, fallback a connection.RemoteIpAddress.

Risposta 429

In caso di superamento del limite, l'API restituisce:

{"message":"Troppi tentativi. Riprova tra {N} secondi."}

con header Retry-After: {N}. Il valore di N è letto dinamicamente dal metadata del lease (MetadataName.RetryAfter).

Configurazione

Le soglie sono configurabili tramite builder.Configuration (che legge da variabili d'ambiente, .env, e override in-memory per i test):

Chiave Default
LOGIN_RATE_LIMIT_PERMITS 10
LOGIN_RATE_LIMIT_WINDOW_SECONDS 60
FORGOT_PASSWORD_RATE_LIMIT_PERMITS 3
FORGOT_PASSWORD_RATE_LIMIT_WINDOW_SECONDS 300

Test

Il file tests/backend/Integration/RateLimiterIntegrationTests.cs contiene 7 test case (RL1-RL7) che verificano tutti gli aspetti del rate limiter: blocco dopo N tentativi, isolamento per email diversa, risposta 429 JSON+Retry-After, forgot-password, header X-Forwarded-For, fallback anonimo. I test usano TightRateLimitWebApplicationFactory, una factory derivata che sovrascrive la configurazione in-memory con limiti bassi (6/60s e 3/60s) senza alterare lo stato globale del processo.

sequenceDiagram
    participant Client
    participant API as FilmAPI
    participant RL as RateLimiter

    Client->>API: POST /api/auth/login
    Note right of Client: body: email, password, deviceId
    API->>API: Middleware: ReadToEndAsync body JSON
    API->>API: Estrae email, salva in HttpContext.Items
    API->>API: Reset stream position a 0
    API->>RL: PartitionKey = {IP}|{SHA256(email)}
    RL->>RL: SlidingWindow: controlla contatore
    alt Contatore < 10 (email valida)
        RL-->>API: Consenti
        API-->>Client: 401/200
    else Contatore >= 10
        RL-->>API: Bloccato
        API-->>Client: 429 + Retry-After + JSON
    end

    Note over Client,RL: Se email assente nel body
    RL->>RL: PartitionKey = {IP}|__anon__
    RL->>RL: Limite ridotto: 5 tentativi

### Sicurezza

- **Nessun token in localStorage** nella versione attuale (pulizia forzata durante login/refresh di eventuali residui legacy)
- **Refresh token hashati** nel database (SHA256) — anche con accesso al DB, i valori raw non sono recuperabili
- **Rotazione automatica** ad ogni refresh (revoca il vecchio, emette il nuovo)
- **Auth version** per invalidazione massiva dopo cambio password
- **Audit log** (`UserSecurityAuditLog`) per tutte le operazioni sensibili
Loading