Tuto — token API haché (SHA-512) en Symfony
Public visé : développeur back qui ajoute un accès API par token à une app Symfony
(cf. specs/auth-compte.md).
Objectif : un token opaque (chaîne aléatoire), haché à la persistance, vérifié à chaque
requête sans jamais stocker le clair — le modèle simple et robuste retenu par codexia.
Pourquoi pas un simple champ texte ? Si la base fuite, un token stocké en clair est directement rejouable. En ne stockant que son empreinte SHA-512, une fuite de la table
userne livre aucun token utilisable : on ne peut pas remonter du hash au clair.
Le principe en une image
Génération (une fois) Vérification (chaque requête)
───────────────────── ─────────────────────────────
random_bytes(32) header X-Auth-Token: <clair>
│ bin2hex │ hash('sha512', …)
â–Ľ â–Ľ
<clair> (64 hex) ──affiché 1×──► <hash candidat> (128 hex)
│ hash('sha512', …) │
â–Ľ â–Ľ
<hash> (128 hex) ──stocké────────► comparaison == User.apiToken
Le clair ne vit que le temps de l'afficher à l'utilisateur ; seul le hash est persisté.
Étape 1 — Le champ sur l'entité User
Le hash SHA-512 fait 128 caractères hexadécimaux : la colonne est dimensionnée exactement.
// src/Entity/User.php #[ORM\Column(length: 128, nullable: true)] private ?string $apiToken = null; public function getApiToken(): ?string { return $this->apiToken; } public function setApiToken(?string $apiToken): static { $this->apiToken = $apiToken; return $this; }
Largeur exacte =
VARCHAR(128). Si une migration antérieure a crééVARCHAR(255), alignez-la (sinondoctrine:schema:validateéchoue). Pensez aussi à poser une contrainteuniquesur la colonne — deux comptes ne doivent pas partager une empreinte.
Étape 2 — La commande de génération
Le token clair est affiché une seule fois ; on ne stocke que son hash. Impossible de le réafficher ensuite — l'utilisateur doit le recopier immédiatement (et en régénérer un en cas de perte).
// src/Command/UserGenerateTokenCommand.php #[AsCommand(name: 'app:user:generate-token', description: 'Génère un token API pour un utilisateur.')] final class UserGenerateTokenCommand { public function __construct(private EntityManagerInterface $em, private UserRepository $users) {} public function __invoke(SymfonyStyle $io, string $email): int { $user = $this->users->findOneBy(['email' => $email]); if (!$user) { $io->error("Utilisateur introuvable : {$email}"); return Command::FAILURE; } // 32 octets aléatoires → 64 caractères hex : le token EN CLAIR. $clear = bin2hex(random_bytes(32)); // On ne persiste que l'empreinte SHA-512 (128 hex). $user->setApiToken(hash('sha512', $clear)); $this->em->flush(); $io->success('Token généré. Copiez-le maintenant, il ne sera plus affiché :'); $io->writeln($clear); return Command::SUCCESS; } }
random_bytes(), pasrand().random_bytes()est un générateur cryptographiquement sûr ;rand()/mt_rand()sont prédictibles et n'ont rien à faire ici.
Étape 3 — L'authenticator
À chaque appel API, on lit le token en clair envoyé par le client, on le re-hache en SHA-512 et on compare au hash stocké. Aucune comparaison ne porte sur du clair.
// src/Security/APIAuthenticator.php final class APIAuthenticator extends AbstractAuthenticator { public function __construct(private UserRepository $users) {} public function supports(Request $request): ?bool { return $request->headers->has('X-Auth-Token'); } public function authenticate(Request $request): Passport { $clear = (string) $request->headers->get('X-Auth-Token'); if ($clear === '') { throw new CustomUserMessageAuthenticationException('Token manquant.'); } $hash = hash('sha512', $clear); return new SelfValidatingPassport( new UserBadge($hash, function (string $hash): UserInterface { $user = $this->users->findOneBy(['apiToken' => $hash]); if (!$user) { throw new CustomUserMessageAuthenticationException('Token invalide.'); } return $user; }) ); } public function onAuthenticationFailure(Request $request, AuthenticationException $e): ?Response { return new JsonResponse(['error' => 'Authentification requise.'], Response::HTTP_UNAUTHORIZED); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return null; // laisse la requĂŞte continuer } }
Nom du header — canonique et exclusif. L'implémentation lit
X-Auth-Tokenet uniquement ce header :supports()ne se déclenche que sur sa présence, donc un headerBearerest rejeté (→ 401). C'est l'état figé au canon (auth-compte.md§8, confirmé Lead inbox codexia #47). Un supportBearerserait une évolution future, pas l'actuel.
Branchez l'authenticator sur le firewall api :
# config/packages/security.yaml security: firewalls: api: pattern: ^/api stateless: true custom_authenticators: - App\Security\APIAuthenticator
Étape 4 — Rate limiting
Un endpoint authentifié par token reste exposé au bruteforce et à l'abus : on borne le débit.
# config/packages/rate_limiter.yaml framework: rate_limiter: api: policy: 'sliding_window' limit: 100 interval: '1 hour'
// dans le contrĂ´leur API (ou un EventSubscriber) $limiter = $this->apiLimiter->create($request->getClientIp()); if (!$limiter->consume()->isAccepted()) { throw new TooManyRequestsHttpException(); }
Tester
# 1. Générer un token php bin/console app:user:generate-token admin@example.com # → affiche le clair une seule fois, ex. : 7f3a…(64 hex) # 2. Appeler l'API avec curl -H "X-Auth-Token: 7f3a…" https://telaria.dev/api/me # 3. Un token inconnu → 401 curl -H "X-Auth-Token: nimporte-quoi" https://telaria.dev/api/me
Côté tests automatisés, un test dédié de l'authenticator (token valide → 200, inconnu → 401, absent → 401) est recommandé : il verrouille la non-régression de la comparaison de hash.
Ă€ retenir
- Token opaque aléatoire via
random_bytes(32)→bin2hex(64 hex). - Hash SHA-512 stocké (
VARCHAR(128),unique) ; le clair n'est jamais persisté. - Affichage unique à la génération ; perte ⇒ régénération.
- Vérification = re-hash du header + comparaison ; échec ⇒ 401.
- Rate limiting sur le firewall API.
- Évolutions possibles (non requises) : OAuth 2.1 / JWT — voir
auth-compte.md§8.
Voir aussi
specs/auth-compte.md— contrat compte/auth/token.accessibility/api.md— API accessible (codes, messages).- Tuto 2FA en Symfony avec scheb — second facteur côté connexion web.