JWT et Cookies HttpOnly
Cette page documente l'implementation de l'authentification JWT avec cookies HttpOnly.
Vue d'ensemble
Le systeme utilise JWT (JSON Web Tokens) pour l'authentification, avec stockage des tokens dans des cookies HttpOnly pour une securite renforcee.
Pourquoi HttpOnly ?
Risques du stockage localStorage
| Risque | Description |
|---|---|
| XSS | Scripts malveillants peuvent lire localStorage |
| Vol de session | Tokens accessibles via JavaScript |
| Pas d'expiration auto | Tokens persistent meme apres fermeture |
Avantages HttpOnly
| Avantage | Description |
|---|---|
| Protection XSS | Cookies non accessibles via JavaScript |
| Expiration auto | Cookies peuvent expirer automatiquement |
| SameSite | Protection CSRF avec attribut SameSite |
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Frontend │ Login │ Backend │ Store │ Cookie │
│ React │ ------> │ Symfony │ ------> │ HttpOnly │
│ │ │ │ │ │
│ │ <------ │ │ <------ │ │
│ │ Set- │ │ │ │
│ │ Cookie │ │ │ │
└─────────────────┘ └─────────────────┘ └──────────────┘
Configuration
Backend (lexik_jwt_authentication.yaml)
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
token_ttl: 3600 # 1 heure
# Configuration cookies
set_cookies:
access_token:
name: jwt_access
httpOnly: true
secure: true
sameSite: strict
path: /
refresh_token:
name: jwt_refresh
httpOnly: true
secure: true
sameSite: strict
path: /api/token/refresh
Variables d'environnement
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=your_passphrase
JWT_TOKEN_TTL=3600
JWT_REFRESH_TOKEN_TTL=2592000 # 30 jours
Flux d'authentification
Login
POST /api/auth/login
Content-Type: application/json
{
"username": "user@example.com",
"password": "password123"
}
Reponse
HTTP/1.1 200 OK
Set-Cookie: jwt_access=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/
Set-Cookie: jwt_refresh=abc...; HttpOnly; Secure; SameSite=Strict; Path=/api/token/refresh
{
"success": true,
"data": {
"user": {
"id": "...",
"email": "user@example.com",
"nom": "John Doe"
}
}
}
Requetes authentifiees
Les cookies sont automatiquement envoyes avec chaque requete :
fetch('/api/users/me', {
method: 'GET',
credentials: 'include' // Important !
})
Refresh Token
Endpoint
POST /api/token/refresh
Comportement
- Le cookie
jwt_refreshest envoye automatiquement - Le backend valide le refresh token
- Nouveaux cookies access + refresh sont generes
Rotation des tokens
A chaque refresh :
- Nouveau access token genere
- Nouveau refresh token genere (rotation)
- Ancien refresh token invalide
Logout
Endpoint
POST /api/auth/logout
Comportement
- Suppression des cookies (Max-Age=0)
- Invalidation du refresh token en base
- Redirection vers la page de login
Securite
Attributs des cookies
| Attribut | Valeur | Description |
|---|---|---|
| HttpOnly | true | Non accessible via JavaScript |
| Secure | true | Uniquement HTTPS |
| SameSite | Strict | Pas d'envoi cross-origin |
| Path | / | Chemin d'application |
| Max-Age | 3600 | Expiration en secondes |
Protection CSRF
Le SameSite=Strict empeche l'envoi du cookie depuis d'autres domaines.
Pour les actions sensibles, un token CSRF additionnel peut etre utilise :
fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': getCsrfToken()
},
credentials: 'include'
})
Frontend (Axios)
Configuration globale
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
withCredentials: true, // Envoie les cookies
});
// Intercepteur pour refresh automatique
api.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
try {
await api.post('/token/refresh');
return api.request(error.config);
} catch (refreshError) {
// Redirection login
window.location.href = '/#/auth/login';
}
}
return Promise.reject(error);
}
);
Gestion des erreurs
| Erreur | HTTP | Description |
|---|---|---|
| Token expire | 401 | Access token expire, refresh necessaire |
| Token invalide | 401 | Token corrompu ou manipule |
| Refresh expire | 401 | Session terminee, login necessaire |
| CSRF invalide | 403 | Token CSRF manquant ou invalide |
Migration depuis localStorage
Si vous migrez depuis localStorage :
- Phase 1 : Acceptez les deux modes (localStorage + cookies)
- Phase 2 : Migrez les tokens existants vers cookies
- Phase 3 : Supprimez le support localStorage
// Verification migration
if (localStorage.getItem('token')) {
// Ancien mode - forcer re-login
localStorage.removeItem('token');
window.location.href = '/#/auth/login';
}
Bonnes pratiques
:::tip Recommandations
- HTTPS obligatoire : Les cookies Secure ne fonctionnent qu'en HTTPS
- SameSite=Strict : Utilisez Strict sauf besoin specifique
- Duree courte : Access token court (1h), refresh token long (30j)
- Rotation : Rotez les refresh tokens a chaque utilisation
- Logout propre : Invalidez les tokens cote serveur au logout :::