Skip to content

How-to : Pattern de token de rafraîchissement

Référence FT : FT281 (NENE2-FT/refreshlog) — Pattern de token de rafraîchissement : token d'accès de courte durée (JWT 5 min) + token de rafraîchissement de longue durée (7 jours), stockage de hash SHA-256, rotation de token à l'utilisation, détection d'attaque par rejeu (token révoqué → révoquer tout), déconnexion retourne toujours 204, 15 tests / 63 assertions PASS.

Ce guide montre comment implémenter le pattern de token de rafraîchissement — des tokens d'accès de courte durée pour la sécurité, des tokens de rafraîchissement pour la continuité de session.

Pourquoi c'est important

Les JWTs sont sans état. Une fois émis, ils ne peuvent pas être révoqués jusqu'à leur expiration. Une TTL de 5 minutes limite l'exposition si un token est volé. Les tokens de rafraîchissement étendent les sessions sans invites de mot de passe répétées, et peuvent être tournés (révoqués et réémis) à chaque utilisation pour détecter les vols.

Schéma

sql
CREATE TABLE IF NOT EXISTS refresh_tokens (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER NOT NULL REFERENCES users(id),
    token_hash TEXT    NOT NULL UNIQUE,
    expires_at TEXT    NOT NULL,
    revoked    INTEGER NOT NULL DEFAULT 0,
    created_at TEXT    NOT NULL
);

token_hash stocke le SHA-256 du token brut — jamais la valeur brute. revoked est un flag de suppression douce (vs suppression physique pour la détection de rejeu).

Endpoints

MéthodeCheminAuthDescription
POST/auth/loginAucuneEmail + mot de passe → token d'accès + token de rafraîchissement
POST/auth/refreshToken de rafraîchissement dans le bodyTourner le token de rafraîchissement, émettre une nouvelle paire
POST/auth/logoutToken de rafraîchissement dans le bodyRévoquer le token de rafraîchissement
GET/auth/meToken d'accès BearerObtenir les informations de l'utilisateur actuel

Durées de vie des tokens

php
private const int ACCESS_TOKEN_TTL_SECONDS = 300; // 5 minutes — courte pour la sécurité
// Tokens de rafraîchissement : 7 jours (RefreshTokenRepository::TTL_DAYS)

Les tokens d'accès courts limitent l'exposition si volés. Les tokens de rafraîchissement longs permettent aux utilisateurs de rester connectés entre les sessions sans ressaisir les mots de passe.

Émission de la paire de tokens

php
private function issueTokenPair(User $user): array
{
    $now         = time();
    $accessToken = $this->issuer->issue([
        'jti'   => bin2hex(random_bytes(8)),  // ID de token unique — permet le suivi de révocation futur
        'sub'   => $user->id,
        'email' => $user->email,
        'iat'   => $now,
        'exp'   => $now + self::ACCESS_TOKEN_TTL_SECONDS,
    ]);

    $refreshToken = $this->refreshTokens->issue($user->id);

    return [
        'access_token'  => $accessToken,
        'token_type'    => 'Bearer',
        'expires_in'    => self::ACCESS_TOKEN_TTL_SECONDS,
        'refresh_token' => $refreshToken,
    ];
}

Stocker uniquement le hash

php
public function issue(int $userId): string
{
    $raw       = bin2hex(random_bytes(32));  // 64 chars hex = 256 bits d'entropie
    $hash      = hash('sha256', $raw);
    // INSERT token_hash = $hash ...

    return $raw;  // ← retourner la valeur brute au client ; jamais stockée
}

public function findByRaw(string $raw): ?RefreshToken
{
    $hash = hash('sha256', $raw);
    // SELECT WHERE token_hash = $hash
}

Si la DB est compromise, les attaquants obtiennent des hashes — inutiles sans les tokens bruts détenus par les clients.

Rotation de token

php
// Lors d'un /auth/refresh réussi :
$this->refreshTokens->revoke($stored->id);      // révoquer l'ancien
return $this->json->create($this->issueTokenPair($user));  // émettre une nouvelle paire

Chaque rafraîchissement tourne le token. L'ancien token devient immédiatement invalide, donc un token de rafraîchissement volé ne peut être utilisé qu'une seule fois avant que la rotation l'invalide.

Détection d'attaque par rejeu

php
$stored = $this->refreshTokens->findByRaw($body['refresh_token']);

if ($stored === null || !$stored->isValid()) {
    // Un token révoqué étant réutilisé → attaque par rejeu potentielle
    if ($stored !== null && $stored->revoked) {
        $this->refreshTokens->revokeAllForUser($stored->userId);
    }
    return $this->problems->create($request, 'invalid-refresh-token', '...', 401);
}

Si un attaquant vole un token de rafraîchissement, l'utilise, et le client légitime essaie ensuite de l'utiliser (maintenant révoqué) — le système détecte cela et révoque toutes les sessions de l'utilisateur, forçant une re-authentification.

La déconnexion retourne toujours 204

php
private function logout(ServerRequestInterface $request): ResponseInterface
{
    $stored = $this->refreshTokens->findByRaw($body['refresh_token']);

    if ($stored !== null && !$stored->revoked) {
        $this->refreshTokens->revoke($stored->id);
    }

    // Toujours 204 — ne jamais révéler si le token était valide
    return $this->json->createEmpty(204);
}

Retourner 401 pour un token déjà révoqué à la déconnexion permettrait à un attaquant de sonder s'il a été déconnecté.

Vérification de validité du token

php
public function isValid(): bool
{
    if ($this->revoked) {
        return false;
    }
    return $this->expiresAt > (new \DateTimeImmutable())->format('Y-m-d\TH:i:s\Z');
}

La révocation et l'expiration sont toutes deux vérifiées. Les tokens expirés mais non révoqués sont également rejetés.

Résumé des propriétés de sécurité

PropriétéImplémentation
TTL token d'accès5 minutes (minimiser l'exposition en cas de vol)
TTL token de rafraîchissement7 jours (continuité de session)
Stockage des tokensHash SHA-256 uniquement ; valeur brute jamais stockée
Rotation de tokenAncien token révoqué à chaque rafraîchissement réussi
Détection de rejeuRéutilisation de token révoqué → révoquer toutes les sessions utilisateur
DéconnexionToujours 204 (ne jamais fuiter la validité du token)
Claim jtiUnique par token (suivi de révocation futur)

Ce qu'il ne faut PAS faire

Anti-patternRisque
Stocker le token de rafraîchissement brut en DBUne fuite DB expose toutes les sessions actives
Utiliser la suppression physique à la révocationImpossible de détecter les attaques par rejeu (besoin de revoked = 1 pour savoir que le token existait)
TTL de token d'accès longue (heures/jours)Un token volé fournit un accès à long terme ; annule l'utilité des tokens de rafraîchissement
Retourner 401 à la déconnexion avec token invalideL'attaquant peut sonder s'il est encore connecté
Pas de jti dans le token d'accèsImpossible de suivre les tokens individuels pour les listes de révocation futures
Token unique (accès seul, pas de rafraîchissement)L'utilisateur doit se re-authentifier toutes les 5 minutes, ou utiliser des TTL dangereusement longues
MD5 ou SHA-1 pour le hash de tokenHash faible ; utiliser SHA-256 ou mieux
Pas d'expiration sur les tokens de rafraîchissementLes tokens de rafraîchissement vivent indéfiniment ; un token volé fournit un accès indefini

Publié sous licence MIT.