Sessions web (cookie)
Les pages HTML servies par Hydrogen partagent les mêmes lignes user_session que l’API — seul le transport change : Authorization: Bearer <token> pour l’API, cookie pour le web. Pratiquement, ça veut dire :
- Un même utilisateur peut être connecté simultanément côté site et côté app mobile, chaque session apparaîtra dans
GET /api/auth/sessions. - La révocation d’une session web depuis l’app mobile (et inversement) fonctionne sans code spécial.
- Le throttle anti-bruteforce est partagé : 5 échecs sur
/loginouPOST /api/auth/loginferment l’IP/email côté API ET côté web.
Cookies émis
Section titled “Cookies émis”| Cookie | HttpOnly | Secure (prod) | SameSite | Path | Rôle |
|---|---|---|---|---|---|
hydrogen_session* | oui | oui | Lax* | / | Porte le bearer token de session ; sliding Max-Age aligné sur user_session.expires_at (30 jours). |
csrf* | non | oui | Strict | / | Token CSRF (64 hex chars) du double-submit cookie. Lu par le JS pour être renvoyé. |
* noms et politiques configurables : WEB_SESSION_COOKIE_NAME, WEB_COOKIE_SECURE, WEB_COOKIE_SAMESITE, WEB_COOKIE_DOMAIN, CSRF_COOKIE_NAME.
Pipeline middleware
Section titled “Pipeline middleware”request → WebSession → Locale → Csrf → TwigGlobals → action- WebSession : lit le cookie de session, appelle
SessionService::authenticate(), réémet le cookie avec le nouveauexpires_at(sliding). Sur cookie invalide : passe en anonyme et efface le cookie. - Locale : choisit la locale (settings utilisateur si connecté, sinon
Accept-Language). - Csrf : sur
POST/PUT/PATCH/DELETE, exige soit un champ_csrfdans le corps, soit l’en-têteX-CSRF-Token. Comparaisonhash_equals. Rejet403plain text en cas de désaccord ou de cookie absent/malformé. - TwigGlobals : publie
viewer(objetUserounull),csrf_token(string),localedans toutes les templates Twig.
Variables d’environnement
Section titled “Variables d’environnement”| Variable | Défaut | Effet |
|---|---|---|
WEB_SESSION_COOKIE_NAME | hydrogen_session | Nom du cookie de session. |
WEB_COOKIE_SECURE | false | true en prod (HTTPS). Concerne les deux cookies (session + CSRF). |
WEB_COOKIE_SAMESITE | Lax | Politique SameSite du cookie de session. Strict si pas besoin de GET cross-site authentifié. |
WEB_COOKIE_DOMAIN | (vide) | Attribut Domain explicite. Vide = host-only (recommandé). |
CSRF_COOKIE_NAME | csrf | Nom du cookie CSRF. |
Bandeau de consentement cookies
Section titled “Bandeau de consentement cookies”Le partial partials/cookie-consent.twig s’inclut à la fin du <body> de toute page web qui doit afficher le bandeau (home.twig, auth/login.twig, etc.). Il s’appuie sur la lib orestbida/cookieconsent v3 chargée depuis jsdelivr (CSS + UMD pinned au tag v3.0.1).
Catégories :
| Slug | Décochable ? | Couvre |
|---|---|---|
necessary | non | hydrogen_session, csrf, le cookie de mémorisation du consentement lui-même. |
analytics | oui | Google Analytics 4 (_ga, _ga_*) — chargé UNIQUEMENT après acceptation explicite. |
Légalement (RGPD/CNIL), les cookies strictement nécessaires ne requièrent PAS de consentement. La banner reste affichée pour information sur la catégorie
necessaryET pour recueillir le consentement obligatoire de la catégorieanalytics.
Google Analytics 4 : pilote via GOOGLE_ANALYTICS_ID (ex. G-XXXXXXX). Vide ⇒ aucun snippet GA n’est rendu. Sinon, les deux balises <script> (chargement de gtag.js + appel gtag('config', ...)) sont émises avec type="text/plain" et data-category="analytics" ; le browser ne les exécute PAS au parsing. CookieConsent v3 surveille ces nœuds et ne réécrit leur type en text/javascript qu’à partir de l’acceptation explicite de la catégorie — refus / pas de décision = GA ne se charge jamais, aucun cookie posé, aucun hit envoyé. anonymize_ip: true est forcé.
Localisation : les libellés sont stockés dans resources/lang/<locale>/cookies.php (catalogue racine partagé entre toutes les pages — pas sous pages.* puisque la banner traverse toutes les pages). Sections : consent.*, preferences.*, categories.<slug>.*.
Lien de réouverture : tout élément doté de l’attribut data-cc="show-preferencesModal" rouvre la modale (à utiliser dans un futur footer ou une page Confidentialité).
Réémission : la clé revision (entier) côté Twig est à incrémenter dès qu’une catégorie est ajoutée ou retirée — la banner sera alors réaffichée aux visiteurs qui avaient déjà répondu.
Ajouter un script soumis au consentement : le jour où un outil d’analytics est intégré, sa balise <script> doit porter type="text/plain" + data-category="analytics" (convention CookieConsent v3). Le script ne s’exécute alors qu’à partir de l’acceptation explicite.
Protection contre les redirections ouvertes
Section titled “Protection contre les redirections ouvertes”Tous les endpoints qui consomment un paramètre return (?return=... ou champ return dans le corps) le filtrent via ReturnUrlSanitizer::pick(). Sont acceptés uniquement :
- les chemins relatifs commençant par
/(ex :/profile,/media/abc?from=home).
Sont rejetés :
- les URL absolues (
http://...). - les chemins protocol-relative (
//evil.com/...). - les valeurs vides ou non-string.
En cas de rejet, fallback sur le default fourni (typiquement /).
GET /login
Section titled “GET /login”Affiche le formulaire de connexion (Twig). Si l’utilisateur est déjà authentifié, redirige immédiatement vers ?return= (sanitisé) ou /.
- Auth : aucune (mais redirige si déjà connecté)
- Action : ShowLoginAction
- Query params :
error(optionnel) — code d’erreur affiché par la template (missing_fields,invalid_credentials,banned,email_not_confirmed,throttled,unknown).return(optionnel) — chemin où rediriger après login (sanitisé).
- Réponse :
200 OK(HTML) ou302si déjà connecté.
POST /login
Section titled “POST /login”Soumission du formulaire de connexion web. Mêmes règles métier que POST /api/auth/login (throttle, ban, email confirmé). Succès : pose le cookie de session, redirige vers ?return= ou /. Échec : redirige vers /login?error=<code>&return=<return>.
- Auth : aucune
- CSRF : requis (champ
_csrfou en-têteX-CSRF-Token) - Action : SubmitLoginAction
- Corps (form-urlencoded) :
email(string, requis)password(string, requis)_csrf(string, requis)return(string, optionnel)
- Réponse
302:- Succès :
Location: <return>+Set-Cookie: hydrogen_session=... - Échec :
Location: /login?error=<code>[&return=<return>]
- Succès :
- Codes d’erreur (paramètre
errordu redirect) :missing_fields— email ou password vide.invalid_credentials— combinaison invalide.banned— compte banni.email_not_confirmed— email non confirmé.throttled— trop de tentatives.unknown— erreur inattendue (devrait être inatteignable).
- Réponse
403:CSRF token missing or invalid.(en cas d’échec du middleware CSRF).
POST /logout
Section titled “POST /logout”Révoque la session web courante (supprime la ligne user_session) ET efface le cookie. Idempotent — un visiteur anonyme reçoit aussi un 302 /. Les autres sessions du même utilisateur (autres appareils, app mobile, API) ne sont pas affectées : utilisez POST /api/auth/logout-all pour ça.
- Auth : facultative (l’action ne casse pas si la session est déjà absente)
- CSRF : requis
- Action : LogoutAction (web)
- Corps (form-urlencoded) :
_csrf(string, requis)return(string, optionnel)
- Réponse
302:Location: <return>(default/) +Set-Cookie: hydrogen_session=; Max-Age=0.
POST /auth/oauth/google, POST /auth/oauth/apple, POST /auth/oauth/facebook
Section titled “POST /auth/oauth/google, POST /auth/oauth/apple, POST /auth/oauth/facebook”Pendants web des endpoints OAuth de l’API (POST /api/auth/oauth/*). Le navigateur a obtenu un token via le SDK JS du fournisseur (Google Identity Services / Sign in with Apple JS / Facebook JS SDK), puis le poste à l’un de ces trois endpoints. La logique métier est partagée avec l’API (OAuthLoginService) — seul le transport change : pas de JSON envelope, on ouvre une session web (cookie) et on redirige.
Le formulaire HTML est rendu par templates/auth/login.twig ; les boutons fournisseurs ne s’affichent que si l’env correspondante est non vide :
| Provider | Env requise(s) | Bouton si vide ? |
|---|---|---|
GOOGLE_OAUTH_CLIENT_ID | masqué | |
| Apple | APPLE_OAUTH_WEB_CLIENT_ID + APPLE_OAUTH_WEB_REDIRECT_URI | masqué |
FACEBOOK_APP_ID | masqué |
- Auth : aucune
- CSRF : requis (champ
_csrfposé par le formulaire caché correspondant) - Actions :
- Google → WebGoogleOAuthAction
- Apple → WebAppleOAuthAction
- Facebook → WebFacebookOAuthAction
- Corps (form-urlencoded) :
- Google :
idToken(string, requis) +_csrf+return? - Apple :
idToken(string, requis) +_csrf+return? - Facebook (classic) :
flow=classic+accessToken+_csrf+return? - Facebook (limited) :
flow=limited+idToken+_csrf+return?
- Google :
- Réponse
302:- Succès :
Location: <return>+Set-Cookie: hydrogen_session=... - Échec :
Location: /login?error=<code>[&return=<return>]
- Succès :
- Codes d’erreur (paramètre
errordu redirect, mappés surpages.login.error.*) :oauth_missing_token— body sansidToken/accessToken/flowvalide.oauth_token_invalid— vérification JWKS/Graph KO.oauth_link_refused— un compte existe déjà pour cet email mais l’auto-link a été refusé (règles C3 pour Google ; toujours refusé pour Apple/Facebook — E.a strict).oauth_email_missing— Facebook uniquement (D.a) : l’utilisateur a refusé la permissionemail.banned— compte banni.
Les règles d’auto-link, de placeholder username et de bannissement sont strictement identiques à celles des endpoints API correspondants — voir
POST /api/auth/oauth/google,apple,