Aller au contenu

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

  1. Protection XSS : Les tokens ne sont pas accessibles en JavaScript
  2. Gestion automatique : Le navigateur envoie automatiquement les cookies
  3. Expiration automatique : Gérée par le navigateur selon les paramètres du cookie
  4. Secure flag : Possibilité de restreindre aux connexions HTTPS
  5. 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

  1. Suppression de axios-auth-refresh : Plus nécessaire
  2. Suppression des headers Authorization : Gérés par les cookies
  3. Simplification du store : Plus de gestion de tokens
  4. 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

  1. Onglet Application > Cookies : Vérifier la présence des cookies
  2. Console : document.cookie (ne montre que les cookies non-httpOnly)
  3. 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

  1. Erreur 401 après login
  2. Vérifier que withCredentials: true est configuré
  3. Vérifier la configuration CORS côté backend

  4. Cookies non envoyés

  5. Vérifier le domaine et le path des cookies
  6. Vérifier la configuration SameSite

  7. Erreur CORS

  8. Backend doit autoriser les credentials
  9. 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.