Mécanisme de bascule entre espaces (jeton signé à usage unique)¶
Cette page explique pourquoi la bascule d'un compte mixte entre l'espace utilisateur et l'espace d'administration passe par une reconnexion via un jeton signé plutôt que par un simple changement de rôle, et comment ce jeton à usage unique est généré, transmis et consommé en toute sécurité. Pour le parcours pas-à-pas côté utilisateur, voir le how-to Basculer entre l'espace administrateur et l'espace utilisateur.
Le problème : deux firewalls, deux sessions¶
Kirexo sépare son authentification en deux firewalls Symfony distincts — admin (pattern ^/admin) et user (fallback) — qui partagent le même provider Doctrine mais gèrent des sessions indépendantes. Cette architecture, et ses raisons, sont détaillées dans Architecture des firewalls.
La conséquence directe : être authentifié sur le firewall user ne vous authentifie pas sur le firewall admin, et inversement. Chaque firewall possède son propre token de sécurité, stocké sous sa propre clé de session. Un compte mixte connecté à son espace utilisateur n'a, du point de vue du firewall admin, aucune session active.
On ne peut donc pas faire basculer un compte d'un espace à l'autre en lui « ajoutant un rôle » ou en réutilisant la session courante : il faut véritablement ouvrir une session sur l'autre firewall, c'est-à-dire se reconnecter. Tout l'enjeu est de faire cette reconnexion sans redemander le mot de passe, et sans ouvrir une faille par laquelle on pourrait s'authentifier sur un espace auquel on n'a pas droit.
Pourquoi pas un firewall unique avec des rôles ?
Un firewall unique simplifierait la bascule (un seul token, les rôles suffisent). Mais Kirexo veut au contraire cloisonner les deux espaces : sessions séparées, et surtout masquage 404 de l'admin vis-à-vis des visiteurs de l'espace user. La séparation en deux firewalls est le socle de ce cloisonnement ; la bascule par jeton en est la contrepartie assumée.
La solution : un jeton de relog à usage unique¶
Plutôt que de transporter une identité ou un rôle dans l'URL, Kirexo émet un jeton opaque qui sert de « ticket de reconnexion » vers l'autre espace. Le jeton ne porte aucune donnée métier ; il sert uniquement d'index vers une information stockée côté serveur.
Le déroulé complet¶
-
Clic sur le bouton de bascule — un
POSTest émis vers/switch(depuis l'espace user, controllersrc/Controller/AppSwitchToAdminController.php) ou/admin/switch(depuis l'espace admin, controllersrc/Controller/AppSwitchToUserController.php). La requête est protégée par CSRF (jetonswitch_space) : un POST forgé depuis un autre site est rejeté. -
Autorisation — seul un compte mixte peut déclencher la bascule. Les deux controllers portent
#[IsGranted(CanSwitchSpaceVoter::CAN_SWITCH_SPACE)], et le bouton lui-même n'est rendu dans les templates que sousis_granted('CAN_SWITCH_SPACE'). Le Votersrc/Security/Voter/CanSwitchSpaceVoter.phpn'accorde l'attribut que si le compte porte à la foisROLE_USERetROLE_ADMIN. La règle d'autorisation vit ainsi en un seul endroit, conformément à la règle « aucune décision d'autorisation inline » du projet. -
Génération du jeton — tant que la session courante est encore active (on a besoin de l'identité du compte),
src/Security/Token/SwitchSpaceTokenGenerator.phpproduit un jeton de la formenonce.hmac:- le
nonceest aléatoire (32 octets) ; - le
hmacest une signature HMAC SHA-256 du nonce avec le secret applicatif (%kernel.secret%).
Le générateur stocke côté serveur, en cache Redis (
cache.app), un payload{ identifier, target }— l'email du compte et l'espace de destination ('user'ou'admin') — sous une clé dérivée du jeton par SHA-256, avec un TTL de 60 secondes. - le
-
Invalidation de la session courante — le controller invalide explicitement la session du firewall d'origine (
$request->getSession()->invalidate()) et vide le token de sécurité. On quitte proprement l'espace de départ avant de rejoindre l'autre. -
Redirection vers l'autre page de login — le controller redirige vers la page de login de l'espace cible (
app_admin_loginouapp_login) en ajoutant le jeton en query :?_switch_token=.... Aucune donnée métier ne transite en clair dans l'URL : ni email, ni rôle, ni cible — uniquement le jeton opaque. -
Consommation par l'authenticator —
src/Security/Authenticator/SwitchSpaceAuthenticator.phpest enregistré commecustom_authenticatorsur les deux firewalls (cf.config/packages/security.yaml). Il se déclenche quand une page de login est atteinte avec un_switch_token. Il :- vérifie la signature HMAC du jeton (comparaison à temps constant
hash_equals) ; - récupère le payload depuis le cache, puis supprime immédiatement le jeton du cache — c'est ce qui garantit l'usage unique ;
- vérifie la cohérence espace cible ↔ route : un jeton émis pour
'admin'n'est accepté que surapp_admin_login, jamais surapp_login; - vérifie l'existence du compte (
UserRepositoryInterface::findByEmail) et qu'il porte bien le rôle de l'espace cible (ROLE_ADMINpour l'admin,ROLE_USERpour l'utilisateur) ; - authentifie le compte sur le nouveau firewall et redirige vers l'accueil de son espace (
/adminou/u).
- vérifie la signature HMAC du jeton (comparaison à temps constant
-
En cas d'échec — jeton invalide, expiré, déjà consommé, incohérent, ou compte introuvable : l'authenticator ajoute un message flash et redirige vers l'accueil. L'utilisateur n'est authentifié nulle part et doit se reconnecter normalement.
Les garanties de sécurité¶
Le mécanisme repose sur quatre garanties combinées :
| Garantie | Mécanisme | Ce que ça empêche |
|---|---|---|
| Usage unique | Le jeton est supprimé du cache dès sa consommation | Le rejeu : un jeton intercepté ne peut servir une seconde fois |
| Expiration courte | TTL de 60 s sur l'entrée Redis | Un jeton oublié dans un historique ou un log devient vite inerte |
| Non-forgeabilité | Signature HMAC SHA-256 avec %kernel.secret% |
La fabrication d'un jeton : sans le secret, impossible de produire une signature valide |
| Cloisonnement des espaces | L'espace cible du jeton doit correspondre à la route de login atteinte | Qu'un jeton « admin » présenté sur /login (firewall user) authentifie quoi que ce soit — il est rejeté |
À cela s'ajoutent deux contrôles en amont : la protection CSRF sur le POST de bascule (le déclenchement ne peut venir d'un autre site) et le double garde-fou de rôle — le Voter à l'émission (le compte doit être mixte) et la vérification de rôle à la consommation (le compte doit réellement porter le rôle de l'espace cible).
Pourquoi rien dans l'URL
Faire transiter l'email ou le rôle en clair dans l'URL exposerait ces données dans les logs serveur, l'historique du navigateur et le Referer. En ne transmettant qu'un jeton opaque indexant une donnée éphémère et côté serveur, l'URL ne révèle rien et le jeton n'a aucune valeur passé 60 secondes ou une fois consommé.
Voir aussi¶
- Basculer entre l'espace administrateur et l'espace utilisateur — le guide pratique côté utilisateur.
- Architecture des firewalls — la séparation en deux firewalls qui rend la bascule nécessaire, et le masquage 404 de l'admin.
- Référence
security.yaml— la configuration des firewalls et de leurs authenticators. - Espaces utilisateur et admin — les deux dashboards entre lesquels on bascule.