Aller au contenu

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 distinctsadmin (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

  1. Clic sur le bouton de bascule — un POST est émis vers /switch (depuis l'espace user, controller src/Controller/AppSwitchToAdminController.php) ou /admin/switch (depuis l'espace admin, controller src/Controller/AppSwitchToUserController.php). La requête est protégée par CSRF (jeton switch_space) : un POST forgé depuis un autre site est rejeté.

  2. 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 sous is_granted('CAN_SWITCH_SPACE'). Le Voter src/Security/Voter/CanSwitchSpaceVoter.php n'accorde l'attribut que si le compte porte à la fois ROLE_USER et ROLE_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.

  3. 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.php produit un jeton de la forme nonce.hmac :

    • le nonce est aléatoire (32 octets) ;
    • le hmac est 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.

  4. 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.

  5. Redirection vers l'autre page de login — le controller redirige vers la page de login de l'espace cible (app_admin_login ou app_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.

  6. Consommation par l'authenticatorsrc/Security/Authenticator/SwitchSpaceAuthenticator.php est enregistré comme custom_authenticator sur 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 sur app_admin_login, jamais sur app_login ;
    • vérifie l'existence du compte (UserRepositoryInterface::findByEmail) et qu'il porte bien le rôle de l'espace cible (ROLE_ADMIN pour l'admin, ROLE_USER pour l'utilisateur) ;
    • authentifie le compte sur le nouveau firewall et redirige vers l'accueil de son espace (/admin ou /u).
  7. 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