Symptôme
Un client d’un site de vente d’équipements sportifs nous a signalé un comportement étrange : certains visiteurs abandonnaient leur panier après plusieurs minutes passées à comparer des tailles et des coloris, sans message d’erreur visible, sans plantage apparent. Le bouton « Ajouter au panier » restait cliquable, mais plus rien ne se passait après le clic. Aucune erreur dans la console du navigateur ne remontait clairement la cause, seulement un code retour 403 discret dans l’onglet réseau, ignoré silencieusement par le code du front.
Diagnostic
Le front headless en Vue.js authentifiait chaque visiteur via un plugin JWT Authentication pour WordPress, avec un jeton d’une durée de vie de dix minutes, valeur par défaut sur ce projet. Le panier, lui, était géré côté serveur pour permettre une synchronisation entre appareils, ce qui obligeait chaque ajout de produit à passer par un appel authentifié à un point de terminaison personnalisé. Un visiteur qui prenait plus de dix minutes à choisir sa taille voyait son jeton expirer en silence : l’appel suivant recevait une réponse 403 du plugin JWT, que le front se contentait de logguer sans rien afficher ni tenter de corriger.
{
"code": "jwt_auth_invalid_token",
"message": "Le jeton d'authentification a expiré",
"data": { "status": 403 }
}
Le problème n’était donc pas un bug de panier, mais une absence totale de gestion de l’expiration du jeton pendant un parcours volontairement plus long qu’une simple lecture de contenu.

Correctif : un rafraîchissement silencieux avant chaque appel sensible
La solution retenue n’a pas consisté à allonger la durée de vie du jeton, ce qui aurait affaibli la sécurité du compte sans résoudre le problème de fond pour les parcours plus longs. À la place, un intercepteur Axios a été ajouté pour détecter toute réponse 403 liée à un jeton expiré, tenter un rafraîchissement transparent, puis rejouer la requête initiale sans que le visiteur ne perçoive quoi que ce soit :
axios.interceptors.response.use(
(response) => response,
async (error) => {
const original = error.config
const estJetonExpire =
error.response?.status === 403 &&
error.response?.data?.code === 'jwt_auth_invalid_token'
if (estJetonExpire && !original._dejaTente) {
original._dejaTente = true
try {
const nouveauJeton = await rafraichirJeton()
localStorage.setItem('jwt', nouveauJeton)
original.headers.Authorization = `Bearer ${nouveauJeton}`
return axios(original)
} catch (echecRafraichissement) {
redirigerVersConnexion()
}
}
return Promise.reject(error)
}
)
La fonction rafraichirJeton s’appuie sur le point de terminaison /wp-json/jwt-auth/v1/token/validate pour confirmer la validité d’un jeton de rafraîchissement stocké séparément, avant de générer un nouveau jeton d’accès de courte durée. Ce jeton de rafraîchissement, lui, est configuré avec une durée de vie plus longue, un mois dans notre cas, et stocké dans un cookie HttpOnly plutôt que dans le localStorage, pour limiter les risques de vol par script.
Prévention
- Tout appel authentifié critique (panier, commande, compte) passe désormais systématiquement par cet intercepteur, jamais par un appel direct sans filet.
- Un test automatisé simule volontairement l’expiration du jeton en cours de parcours, pour vérifier que le rafraîchissement silencieux fonctionne avant chaque mise en production.
- Un indicateur discret de session, affiché uniquement en développement, permet à l’équipe de repérer immédiatement un jeton sur le point d’expirer pendant les tests manuels.
Ce que ce correctif ne règle pas
Ce mécanisme suppose que le jeton de rafraîchissement lui-même reste valide. Un visiteur qui laisserait un onglet ouvert pendant plusieurs semaines sans revenir finirait par devoir se reconnecter, ce qui reste un compromis raisonnable entre sécurité et confort d’usage. Ce choix entre stocker un jeton côté client et gérer une session serveur avec cookies mérite d’ailleurs un comparatif à part entière, déjà traité dans un autre article de ce blog à propos de Remix.
Un jeton qui expire n’est jamais le vrai problème : le vrai problème, c’est un front qui ne sait pas quoi faire quand ça arrive.
En résumé
Un parcours d’achat headless qui dure plus longtemps qu’une session de lecture classique finit toujours par croiser un jeton expiré. Plutôt que d’allonger artificiellement sa durée de vie, un rafraîchissement silencieux intercepté au bon endroit règle le problème sans compromettre la sécurité, et sans jamais que le visiteur ne s’aperçoive qu’il vient d’être reconnecté sous le capot.