Authentification Frontend - Cookies httpOnly
Vue d'ensemble
L'authentification frontend utilise maintenant des cookies httpOnly pour une sécurité renforcée, remplaçant l'ancien système basé sur le localStorage.
Architecture
1. Store d'authentification (src/stores/auth.js)
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
isAuthenticated: false
}),
persist: {
paths: ['isAuthenticated'] // Seul l'état d'authentification est persisté
}
})
Principes :
- ✅ Pas de stockage de tokens : Les JWT sont gérés par les cookies httpOnly
- ✅ État minimal : Seul un flag isAuthenticated et les données utilisateur
- ✅ Persistance sélective : Seul l'état d'authentification est sauvegardé
- ✅ Déconnexion : l’action logout() appelle clearBrowserStorageOnLogout() (src/utils/clearBrowserStorageOnLogout.js) : localStorage et sessionStorage sont entièrement vidés, en complément de la suppression des cookies httpOnly côté API, pour éviter toute donnée personnelle résiduelle (profil, onboarding, préférences, caches liés au compte, flags OAuth de session).
2. Instance Axios (src/api/axios.js)
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL + '/api',
withCredentials: true, // Crucial pour envoyer les cookies httpOnly
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
})
// Intercepteur pour gérer les erreurs 401 et le refresh token automatique
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
// Logique de refresh token automatique
// Voir section "Gestion des erreurs" ci-dessous
}
return Promise.reject(error)
}
)
Configuration :
- ✅ withCredentials: true : Envoie automatiquement les cookies
- ✅ Intercepteur 401 intelligent : Refresh automatique des tokens
- ✅ Pas de headers Authorization : Les cookies sont envoyés automatiquement
- ✅ File d'attente : Gestion des requêtes concurrentes pendant le refresh
3. Service d'authentification (src/api/auth.js)
class AuthAPI {
async login(email, password) {
// Les cookies sont automatiquement reçus et stockés par le navigateur
const response = await axiosRaw.post('/security/login', { email, password })
return response.data
}
async logout() {
// Appel au backend pour supprimer les cookies
await axios.post('/logout', {})
}
async fetchMe() {
// Les cookies sont automatiquement envoyés
const response = await axios.get('/me')
return response.data
}
}
Workflow d'authentification
1. Connexion
// 1. Appel au login
const data = await authAPI.login(credentials)
// 2. Les cookies httpOnly sont automatiquement stockés par le navigateur
// 3. Récupération des informations utilisateur
await auth.fetchMe()
// 4. Mise à jour du store
auth.isAuthenticated = true
auth.user = userData
2. Requêtes authentifiées
// Les cookies sont automatiquement envoyés avec chaque requête
const response = await axios.get('/api/protected-endpoint')
3. Déconnexion
// 1. Appel au backend pour supprimer les cookies
await authAPI.logout()
// 2. Nettoyage du store
auth.user = null
auth.isAuthenticated = false
// 3. Redirection (gérée par les composants)
router.push({ name: 'home' })
Gestion des erreurs et Refresh Token
Intercepteur 401 intelligent
// Variables pour éviter les boucles infinies
let isRefreshing = false
let failedQueue = []
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error)
} else {
prom.resolve(token)
}
})
failedQueue = []
}
// Intercepteur pour gérer les erreurs 401 et le refresh token
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Si un refresh est déjà en cours, mettre en file d'attente
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
}).then(() => {
return api(originalRequest)
}).catch(err => {
return Promise.reject(err)
})
}
originalRequest._retry = true
isRefreshing = true
try {
// Appel automatique au refresh token
await axios.post('/api/token/refresh', {}, {
withCredentials: true
})
// Traiter la file d'attente
processQueue(null, true)
// Réessayer la requête originale
return api(originalRequest)
} catch (refreshError) {
// Si le refresh échoue, déconnecter l'utilisateur
processQueue(refreshError, null)
const auth = useAuthStore()
auth.logout()
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
}
)
Fonctionnalités :
- ✅ Refresh automatique : Appel à /api/token/refresh sur 401
- ✅ Rétry automatique : La requête originale est rejouée
- ✅ File d'attente : Gestion des requêtes concurrentes
- ✅ Protection anti-boucle : Évite les refresh multiples
- ✅ Déconnexion intelligente : Seulement si le refresh échoue
Workflow de refresh token
// Scénario normal :
// 1. Utilisateur fait une requête API
// 2. Token expiré → 401 reçue
// 3. Intercepteur détecte la 401
// 4. Appel automatique à /api/token/refresh
// 5. Nouveaux cookies définis
// 6. Requête originale rejouée
// 7. Utilisateur reçoit sa réponse normalement
// Scénario d'échec :
// 1. Utilisateur fait une requête API
// 2. Token expiré → 401 reçue
// 3. Intercepteur détecte la 401
// 4. Appel à /api/token/refresh échoue
// 5. Utilisateur déconnecté automatiquement
// 6. Redirection vers la page de connexion
Gestion dans les composants
const logout = async () => {
try {
await auth.logout()
router.push({ name: 'home' })
} catch (error) {
// Gestion d'erreur si nécessaire
}
}
Sécurité
Avantages des cookies httpOnly
- Protection XSS : Les tokens ne sont pas accessibles en JavaScript
- Gestion automatique : Le navigateur envoie automatiquement les cookies
- Expiration automatique : Gérée par le navigateur selon les paramètres du cookie
- Secure flag : Possibilité de restreindre aux connexions HTTPS
- SameSite : Protection contre les attaques CSRF
Configuration recommandée côté backend
// Exemple de configuration Symfony
$response->headers->setCookie(
new Cookie(
'JWT_TOKEN',
$token,
time() + 3600, // 1 heure
'/',
null,
true, // secure
true, // httpOnly
false, // raw
'Strict' // sameSite
)
);
Migration depuis l'ancien système
Changements principaux
- Suppression de
axios-auth-refresh: Plus nécessaire - Suppression des headers Authorization : Gérés par les cookies
- Simplification du store : Plus de gestion de tokens
- Redirection via router : Au lieu de
window.location.href
Code avant/après
Avant (localStorage) :
// Stockage manuel des tokens
auth.setToken(data.token)
auth.setRefreshToken(data.refreshToken)
// Headers manuels
axios.get('/api/endpoint', {
headers: { Authorization: `Bearer ${token}` }
})
Après (cookies httpOnly) :
// Pas de stockage de tokens
await auth.fetchMe()
// Headers automatiques
axios.get('/api/endpoint') // Cookies envoyés automatiquement
Tests
Vérification des cookies
- Onglet Application > Cookies : Vérifier la présence des cookies
- Console :
document.cookie(ne montre que les cookies non-httpOnly) - Network tab : Vérifier l'envoi automatique des cookies
Tests d'intégration
// Test de connexion
const loginResponse = await authAPI.login(credentials)
expect(loginResponse).toBeDefined()
// Test de récupération utilisateur
const userData = await authAPI.fetchMe()
expect(userData.email).toBe(credentials.email)
// Test de déconnexion
await authAPI.logout()
// Vérifier que les cookies sont supprimés
Dépannage
Problèmes courants
- Erreur 401 après login
- Vérifier que
withCredentials: trueest configuré -
Vérifier la configuration CORS côté backend
-
Cookies non envoyés
- Vérifier le domaine et le path des cookies
-
Vérifier la configuration
SameSite -
Erreur CORS
- Backend doit autoriser les credentials
- Configuration :
Access-Control-Allow-Credentials: true
Logs de debug
// Temporairement ajouter pour diagnostiquer
console.log('Cookies disponibles:', document.cookie)
console.log('withCredentials:', config.withCredentials)
Conclusion
Cette nouvelle architecture d'authentification offre : - ✅ Sécurité renforcée contre les attaques XSS - ✅ Simplicité : Moins de code à maintenir - ✅ Performance : Gestion automatique par le navigateur - ✅ Maintenabilité : Architecture plus claire
Le frontend est maintenant prêt pour fonctionner avec des cookies httpOnly côté backend.