veza/apps/web/src/services/tokenStorage.ts
senke d9b6510802 security: migrate access token to httpOnly cookie (Actions 5.1.1.1-5.1.1.3)
Backend changes (Action 5.1.1.1):
- Set access_token cookie in Login, Register, and Refresh handlers
- Cookie uses same configuration as refresh_token (httpOnly, Secure, SameSite)
- Expiry matches AccessTokenTTL (5 minutes)
- Update logout handler to clear access_token cookie

Backend middleware (Action 5.1.1.1):
- Update auth middleware to read access token from cookie first
- Fallback to Authorization header for backward compatibility
- Update OptionalAuth with same cookie-first logic

Frontend changes (Actions 5.1.1.2 & 5.1.1.3):
- Remove localStorage token storage from TokenStorage service
- TokenStorage now returns null for getAccessToken/getRefreshToken (httpOnly cookies not accessible)
- Remove Authorization header logic from API client
- Remove token expiration checks (can't check httpOnly cookies from JS)
- Update AuthContext to remove localStorage usage
- Update tokenRefresh to work without reading tokens from JS
- Simplify refresh logic: periodic refresh every 4 minutes (no expiration checks)

Security improvements:
- Access tokens no longer exposed to XSS attacks (httpOnly cookies)
- Tokens automatically sent with requests via withCredentials: true
- Backend reads tokens from cookies, not Authorization headers
- All users will need to re-login after deployment (breaking change)

Breaking change: All users must re-login after deployment
2026-01-16 01:03:23 +01:00

107 lines
4 KiB
TypeScript

/**
* TokenStorage - Service de gestion du stockage des tokens
* T0169: Service simple pour stocker, récupérer et supprimer les tokens d'authentification
*
* SECURITY: Migration vers cookies httpOnly COMPLÉTÉE (Action 5.1.1.2)
* - Access Token: Stocké dans un cookie httpOnly par le backend (pas accessible via JavaScript)
* - Refresh Token: Stocké dans un cookie httpOnly par le backend (pas accessible via JavaScript)
*
* IMPORTANT: Les tokens sont maintenant dans des cookies httpOnly set par le backend.
* JavaScript ne peut pas accéder aux cookies httpOnly, donc cette classe est maintenant
* principalement une API de compatibilité (no-op).
*
* NOTE: Les cookies httpOnly sont automatiquement envoyés avec les requêtes via withCredentials: true.
* Le backend lit les tokens depuis les cookies, pas depuis localStorage ou les headers Authorization.
*/
const ACCESS_TOKEN_KEY = 'veza_access_token';
const REFRESH_TOKEN_KEY = 'veza_refresh_token';
/**
* Réinitialise le stockage (utile pour les tests)
* @internal
*/
export function _resetTokenMemory(): void {
// No-op: tokens are in httpOnly cookies, not accessible from JS
}
/**
* Classe TokenStorage pour gérer le stockage des tokens
* T0169: Service de gestion du stockage tokens
*
* SECURITY: Action 5.1.1.2 - Tokens sont maintenant dans des cookies httpOnly
* Cette classe est maintenant principalement une API de compatibilité (no-op)
* car les cookies httpOnly ne sont pas accessibles via JavaScript.
*/
export class TokenStorage {
/**
* Stocke les tokens d'authentification
* SECURITY: Action 5.1.1.2 - No-op car les tokens sont dans des cookies httpOnly
* Le backend sette les cookies httpOnly lors du login/register/refresh.
*
* @param accessToken - Token d'accès JWT (ignoré, dans cookie httpOnly)
* @param refreshToken - Token de rafraîchissement (ignoré, dans cookie httpOnly)
*/
static setTokens(_accessToken: string, _refreshToken: string): void {
// No-op: tokens are set in httpOnly cookies by backend, not accessible to JS
// Clean up any legacy localStorage tokens if they exist
try {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
} catch {
// Ignore errors (e.g., in SSR environment)
}
}
/**
* Récupère le token d'accès
* SECURITY: Action 5.1.1.2 - Retourne null car le token est dans un cookie httpOnly
* Les cookies httpOnly ne sont pas accessibles via JavaScript.
*
* @returns null (token est dans cookie httpOnly, non accessible)
*/
static getAccessToken(): string | null {
// Token is in httpOnly cookie, not accessible from JavaScript
return null;
}
/**
* Récupère le token de rafraîchissement
* SECURITY: Action 5.1.1.2 - Retourne null car le token est dans un cookie httpOnly
* Les cookies httpOnly ne sont pas accessibles via JavaScript.
*
* @returns null (token est dans cookie httpOnly, non accessible)
*/
static getRefreshToken(): string | null {
// Token is in httpOnly cookie, not accessible from JavaScript
return null;
}
/**
* Supprime tous les tokens
* SECURITY: Action 5.1.1.2 - Nettoie seulement localStorage legacy si présent
* Les cookies httpOnly sont supprimés par le backend lors du logout.
*/
static clearTokens(): void {
// Clean up any legacy localStorage tokens if they exist
try {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
} catch {
// Ignore errors (e.g., in SSR environment)
}
// Cookies httpOnly are cleared by backend on logout
}
/**
* Vérifie si des tokens sont présents
* SECURITY: Action 5.1.1.2 - Retourne false car on ne peut pas vérifier les cookies httpOnly
* Les cookies httpOnly ne sont pas accessibles via JavaScript.
*
* @returns false (ne peut pas vérifier les cookies httpOnly depuis JS)
*/
static hasTokens(): boolean {
// Cannot check httpOnly cookies from JavaScript
return false;
}
}