Aller au contenu principal

Sécurité

JWT et rotation des jetons de rafraîchissement : la partie que l'on oublie souvent

Un jeton d'accès court est une bonne pratique. Sans rotation ni détection de réutilisation côté rafraîchissement, un jeton volé reste exploitable silencieusement pendant des semaines.

- 9 min de lecture

Les jetons JWT sont largement utilisés pour protéger les API, avec une règle désormais bien connue : un jeton d'accès de courte durée, et un jeton de rafraîchissement pour en obtenir de nouveaux. La partie la plus souvent négligée se situe dans le second : sa durée de vie, son stockage et son cycle de renouvellement.

Le problème des jetons de rafraîchissement longue durée

Un jeton de rafraîchissement valable trente jours et réutilisable à volonté est une clé presque permanente. S'il fuite, par un journal, un poste de travail compromis ou un stockage navigateur mal choisi, l'attaquant conserve l'accès jusqu'à l'expiration. Sans journalisation fine, personne ne s'en aperçoit.

Le principe : usage unique et détection de réutilisation

La rotation consiste à émettre un nouveau jeton de rafraîchissement à chaque utilisation et à invalider immédiatement l'ancien. Chaque jeton appartient à une famille, c'est-à-dire à une chaîne de renouvellements issue de la même authentification initiale.

Si un jeton déjà utilisé est présenté à nouveau, deux scénarios sont possibles : soit un rejeu réseau légitime, soit un vol. Traiter le cas comme un vol et révoquer toute la famille est la seule réponse raisonnable : l'utilisateur légitime devra se réauthentifier, l'attaquant perd son accès.

Ne jamais stocker le jeton en clair

Le jeton de rafraîchissement se stocke sous forme d'empreinte, jamais en clair. Une base de données lue par un attaquant ne doit pas permettre de rejouer une session.

create table refresh_token (
    id          uuid primary key,
    family_id   uuid not null,
    user_id     uuid not null,
    token_hash  char(64) not null unique,
    issued_at   timestamptz not null,
    expires_at  timestamptz not null,
    used_at     timestamptz,
    revoked_at  timestamptz
);

create index ix_refresh_token_family on refresh_token (family_id);
create index ix_refresh_token_user on refresh_token (user_id, expires_at);

Implémentation côté serveur

Le service de rafraîchissement vérifie l'empreinte, contrôle l'état du jeton, puis révoque et remplace. Les exceptions sont distinctes, car elles ne se traitent pas de la même façon : un jeton expiré mène à une réauthentification, une réutilisation détectée déclenche une alerte de sécurité.

@Service
class RefreshTokenService {

    private final RefreshTokenRepository repository;
    private final TokenIssuer tokenIssuer;

    RefreshTokenService(RefreshTokenRepository repository, TokenIssuer tokenIssuer) {
        this.repository = repository;
        this.tokenIssuer = tokenIssuer;
    }

    @Transactional
    public IssuedTokens rotate(String presentedToken, String deviceId) {
        RefreshToken current = repository.findByHash(TokenHasher.sha256(presentedToken))
                .orElseThrow(InvalidRefreshTokenException::new);

        if (current.isUsed() || current.isRevoked()) {
            repository.revokeFamily(current.familyId());
            throw new TokenReuseDetectedException(current.userId(), deviceId);
        }
        if (current.isExpired()) {
            throw new ExpiredRefreshTokenException();
        }

        repository.markUsed(current.id());
        return tokenIssuer.issueForFamily(current.familyId(), current.userId());
    }
}

Les opérations sont transactionnelles : marquer un jeton comme utilisé et en émettre un nouveau doit réussir ou échouer ensemble, sans quoi une panne en cours de route laisse deux jetons valides.

RisqueSans rotationAvec rotation et détection
Jeton voléAccès silencieux jusqu'à expirationAccès révoqué à la première réutilisation
Détection de l'incidentAprès coup, rarementÉvénement de sécurité immédiat
Durée d'expositionJusqu'à trente joursQuelques minutes à quelques heures
Révocation globaleDifficile sans stockageRévocation par famille ou par utilisateur
Un jeton de rafraîchissement à usage unique transforme un vol de jeton en incident détectable, au lieu d'un accès silencieux et permanent.

Le cas des navigateurs

Dans une application web, le jeton de rafraîchissement se place dans un cookie portant les attributs HttpOnly, Secure et SameSite=Strict, avec un chemin restreint au point d'entrée de rafraîchissement. Le stockage local du navigateur doit être évité : il est accessible à tout script exécuté sur la page, y compris par une dépendance compromise.

La protection contre la falsification de requête entre sites reste nécessaire, sous forme d'un jeton anti-CSRF lié à la session, vérifié sur le point d'entrée de rafraîchissement uniquement.

Ce qu'il faut vérifier avant la mise en production

Quatre points suffisent à couvrir l'essentiel : la durée de vie du jeton d'accès reste courte ; le jeton de rafraîchissement est à usage unique et stocké sous forme d'empreinte ; toute réutilisation révoque la famille et génère un événement de sécurité ; la déconnexion révoque explicitement la famille côté serveur, sans se contenter de supprimer le jeton du navigateur.

  • JWT
  • OAuth 2.1 et OIDC
  • Sécurité des API
  • Spring Boot

Articles associes