Skip to content

Authentification

Crée un compte utilisateur. Déclenche l’envoi d’un email contenant un lien de confirmation. Le compte est créé en base avec status = 1, user_type = 1, confirmed_at = NULL — l’utilisateur ne peut pas se connecter tant qu’il n’a pas confirmé son email (POST /api/auth/confirm-email).

  • Auth : aucune
  • Action : RegisterAction
  • Request body (forme plate ou JSON:API data.attributes) :
{
"username": "havoc",
"email": "user@example.com",
"password": "MySuperPassw0rd!",
"sponsorshipCode": "A3K7QM",
"couponId": "WELCOME2026"
}

sponsorshipCode est optionnel (omission, null ou chaîne vide = pas de parrain). Si fourni, il doit matcher un code existant de la table sponsorship — sinon 422 (cf. codes d’erreur). Voir section Parrainage pour le détail du format.

couponId est optionnel et non bloquant : un coupon inconnu, expiré ou déjà épuisé ne fait JAMAIS échouer l’inscription. Le résultat de la tentative est exposé dans meta.coupon sur la réponse 201. Voir section Coupons pour le détail.

Username (UsernamePolicy) :

  • 3 à 32 caractères
  • ASCII : [a-z0-9._-] uniquement (forcé en minuscules à la persistance)
  • Doit commencer et finir par un caractère alphanumérique
  • Pas de séparateurs consécutifs (.., __, -_, …)
  • Unicité case-insensitive

Password (PasswordPolicy) :

  • 12 à 72 octets (72 = limite bcrypt)
  • Au moins : 1 minuscule, 1 majuscule, 1 chiffre, 1 caractère non alphanumérique
  • Pas de NUL byte, pas d’espace en début/fin
  • Ne doit pas contenir le username ni la partie locale de l’email

Email : filter_var(..., FILTER_VALIDATE_EMAIL) + max 255 caractères + unicité.

  • Réponse 201 :
{
"jsonapi": { "version": "1.1" },
"data": {
"type": "users",
"id": "<user-uuid>",
"attributes": {
"username": "havoc",
"email": "user@example.com",
"userType": 1,
"status": 1,
"joinedAt": "2026-06-06T12:34:56+00:00",
"isConfirmed": false
}
},
"meta": {
"confirmationEmailSent": true,
"coupon": {
"applied": true,
"couponId": "WELCOME2026",
"rewards": [{ "type": "experience", "value": 100 }]
}
}
}

meta.coupon n’est présent que si le client a fourni couponId. Sur échec :

"coupon": {
"applied": false,
"couponId": "WELCOME2026",
"reason": "coupon.expired",
"message": "Ce coupon a expiré."
}
  • Réponse 400 — corps non-JSON / non-objet
  • Réponse 422 — un ou plusieurs errors[] avec :
    • source.pointer = /data/attributes/<field>
    • meta.code typé pour i18n côté frontend
    • Codes possibles :
      • username.tooShort, username.tooLong, username.invalidCharacters, username.invalidBoundary, username.consecutiveSeparators, username.reserved, username.alreadyTaken
      • email.invalidFormat, email.alreadyTaken
      • password.tooShort, password.tooLong, password.missingLowercase, password.missingUppercase, password.missingDigit, password.missingSpecial, password.invalidCharacters, password.invalidBoundary, password.containsUsername, password.containsEmail
      • sponsorship.codeInvalid (forme du code incorrecte : longueur, caractères hors alphabet), sponsorship.codeNotFound (code bien formé mais inconnu de la table sponsorship)

Le code username.reserved est renvoyé quand le username demandé figure dans la blocklist statique config/username_blocklist.php (noms système/rôles, identité de marque, routes techniques), chargée dans UsernameBlocklist. La vérification s’applique partout où UsernamePolicy est utilisée (inscription, complétion de profil, changement de username). Le match porte sur la forme normalisée et sur une forme sans séparateurs (./_/-), de sorte que admin bloque aussi a.d.m.i.n. La liste évolue par PR ; aucune table DB en V1.

Les erreurs username.alreadyTaken et email.alreadyTaken sont explicites — un attaquant peut donc tester l’existence d’un email. Choix assumé : les usernames sont publics dans une app sociale, et un endpoint d’« availability check » sera de toute façon nécessaire pour l’UX du formulaire. À durcir si le produit devient sensible.

Tant qu’un vrai mailer n’est pas branché, l’email est écrit dans var/logs/mail.log (LogMailer). Pour récupérer le token de confirmation pendant les tests :

Terminal window
tail -n 30 var/logs/mail.log

Consomme le token reçu par email et marque l’utilisateur comme confirmé (user.confirmed_at = NOW()). Le token est à usage unique et expire après 24h.

{ "token": "<base64url-token>" }

(également accepté en forme JSON:API : data.attributes.token)

  • Réponse 200 :
{
"jsonapi": { "version": "1.1" },
"data": {
"type": "users",
"id": "<user-uuid>",
"attributes": {
"username": "havoc",
"email": "user@example.com",
"isConfirmed": true,
"confirmedAt": "2026-06-06T13:00:00+00:00"
}
}
}
  • Réponse 400 — token inconnu, expiré, ou déjà utilisé (même message pour les trois cas afin d’éviter un oracle)

  • Réponse 422 — attribut token manquant

  • Notes :

    • Une nouvelle demande de confirmation pour le même user invalide automatiquement tous les tokens précédents non utilisés (invalidatePendingForUser).
    • Le token brut n’est jamais stocké : seul son SHA-256 (32 octets) est en base (table email_confirmation).

Renvoie un email de confirmation pour un compte existant et non encore confirmé.

  • Auth : publique (l’utilisateur n’a pas pu se connecter puisqu’il n’est pas confirmé)
  • Action : ResendConfirmationAction
  • Request body (forme plate ou JSON:API data.attributes) :
{ "email": "user@example.com" }

L’endpoint renvoie toujours la même réponse 202 Accepted, peu importe que :

  • l’email soit inconnu ;
  • le compte soit déjà confirmé (confirmed_at IS NOT NULL) ;
  • une demande précédente ait été faite il y a moins de cooldownSeconds (cooldown actif) ;
  • l’envoi de l’email ait réellement eu lieu.

Cela empêche un attaquant d’inférer l’existence d’un compte (ou son état) à partir du status code ou du corps de la réponse.

  • Cooldown 60 s par compte. Calcul : MAX(email_confirmation.created_at) WHERE user_id = X. Si la dernière demande date d’il y a < 60 s, l’envoi est silencieusement supprimé (toujours 202).

  • Si l’envoi a lieu, toutes les confirmations pending du user sont invalidées (used_at = NOW()) avant l’émission du nouveau token — un seul token actif à la fois (logique de EmailConfirmationService::issueFor()).

  • Réponse 202 :

{
"jsonapi": { "version": "1.1" },
"data": {
"type": "confirmationResends",
"id": "pending",
"attributes": {
"message": "If this email is registered and not yet confirmed, a new confirmation link has been sent."
}
},
"meta": { "cooldownSeconds": 60 }
}
  • Réponse 400 — corps non-JSON

  • Réponse 422email manquant ou vide

  • Notes :

    • Le token est généré comme à l’inscription (32 octets CSPRNG, base64url, hash SHA-256 stocké) et expire après EmailConfirmationService::LIFETIME_HOURS (24h).
    • L’email est envoyé via ConfirmationEmailSender — même template que l’inscription pour cohérence.
    • L’ID retourné est volontairement la chaîne "pending" (et non un UUID) : ne pas exposer d’identifiant de ressource créée afin de ne pas confirmer/infirmer l’existence du compte.

Émet un email de réinitialisation de mot de passe pour un compte existant.

  • Auth : publique
  • Action : ForgotPasswordAction
  • Request body (forme plate ou JSON:API data.attributes) :
{ "email": "user@example.com" }

L’endpoint renvoie toujours 202 Accepted avec le même corps, peu importe que :

  • l’email soit inconnu ;
  • le compte soit OAuth-only (sans mot de passe réel — autorisé : permet d’ajouter un mot de passe) ;
  • le compte ne soit pas encore confirmé (autorisé : la consommation du token confirmera l’email implicitement) ;
  • une demande précédente ait été faite il y a moins de cooldownSeconds (cooldown actif) ;
  • l’envoi de l’email ait réellement eu lieu.
  • Cooldown 60 s par compte. Calcul : MAX(password_reset.created_at) WHERE user_id = X. Si la dernière demande date d’il y a < 60 s, l’envoi est silencieusement supprimé.

  • À chaque envoi réussi, toutes les demandes pending du user sont invalidées (used_at = NOW()) — un seul token actif à la fois.

  • Réponse 202 :

{
"jsonapi": { "version": "1.1" },
"data": {
"type": "passwordResetRequests",
"id": "pending",
"attributes": {
"message": "If this email is registered, a password-reset link has been sent."
}
},
"meta": { "cooldownSeconds": 60 }
}
  • Réponse 400 — corps non-JSON

  • Réponse 422email manquant ou vide

  • Notes :

    • Le token est 32 octets CSPRNG, base64url, hash SHA-256 stocké dans password_reset.token_hash (BINARY(32) UNIQUE).
    • Durée de vie : PasswordResetService::LIFETIME_HOURS = 1 heure.
    • L’email est envoyé via PasswordResetEmailSender qui rend le template Twig password_reset.twig. Le lien pointe vers APP_URL + /reset-password?token=....

Consomme un token de réinitialisation et applique un nouveau mot de passe.

  • Auth : publique (le user est précisément en train de récupérer son accès)
  • Action : ResetPasswordAction
  • Request body (forme plate ou JSON:API data.attributes) :
{
"token": "<plain token reçu par email>",
"newPassword": "MyNewPassw0rd!"
}

Sur succès, PasswordResetService::consume() effectue dans l’ordre :

  1. Remplace le mot de passe (bcrypt cost 12, met à jour password_set_at).
  2. Confirme l’email si le compte ne l’était pas (confirmed_at = NOW()) — la possession du token prouve le contrôle de la boîte. meta.emailConfirmedByReset = true dans ce cas.
  3. Révoque toutes les sessions du user (DELETE FROM user_session WHERE user_id = X). L’utilisateur devra se reconnecter partout. meta.sessionsRevoked donne le compte.
  4. Marque le token consommé (used_at = NOW()) — non rejouable.
  • Réponse 200 :
{
"jsonapi": { "version": "1.1" },
"data": {
"type": "passwordResets",
"id": "<user-uuid>",
"attributes": {
"changedAt": "2026-06-07T11:00:00+00:00"
}
},
"meta": {
"sessionsRevoked": 2,
"emailConfirmedByReset": false
}
}
  • Réponse 400 — corps non-JSON

  • Réponse 401 — token rejeté. meta.code :

    • passwordReset.tokenInvalid — inconnu
    • passwordReset.tokenExpired — au-delà de 1h
    • passwordReset.tokenUsed — déjà consommé
  • Réponse 422token ou newPassword manquant, ou échec de la PasswordPolicy. Codes : password.tooShort, password.tooLong, password.missingLowercase, password.missingUppercase, password.missingDigit, password.missingSpecial, password.invalidCharacters, password.invalidBoundary, password.containsUsername, password.containsEmail

  • Notes :

    • Aucun token de session n’est émis — l’utilisateur doit se reconnecter via /auth/login. C’est volontaire (le reset peut résulter d’un compromis, on force une auth complète).
    • Pour un compte OAuth-only, le reset crée son premier mot de passe — il pourra ensuite se connecter par mot de passe ou par Google (au choix).

Échange un Google ID Token (obtenu côté frontend après le sign-in Google) contre un bearer token de session Hydrogen. Selon l’état du compte, l’endpoint peut connecter un utilisateur existant, lier une identité Google à un compte existant, ou créer un nouveau compte.

  • Auth : aucune
  • Action : GoogleLoginAction
  • Pré-requis serveur : variable d’env GOOGLE_OAUTH_CLIENT_ID (Client ID OAuth récupéré dans la Google Cloud Console).
  • Request body (forme plate ou JSON:API data.attributes) :
{ "idToken": "<google id_token>" }

GoogleIdTokenVerifier :

  1. Récupère les JWKS Google (https://www.googleapis.com/oauth2/v3/certs), avec cache disque TTL 1h sous var/cache/oauth/google_jwks.json.
  2. Vérifie la signature RS256, iss (accounts.google.com ou https://accounts.google.com), aud (= GOOGLE_OAUTH_CLIENT_ID), exp (avec 60s de skew).
  3. Extrait sub, email, email_verified, name, given_name, family_name, picture, locale.
CasConditionRésultatCode HTTPmeta.oauthOutcome
Identité Google déjà liée(provider, sub) existe en user_oauth_identitysign-in du user lié200existingIdentity
Auto-link (C3)email Google = email d’un user existant, et ce user a confirmed_at IS NOT NULL, et email_verified = true côté Googlelien créé puis sign-in200linkedToExistingUser
Auto-link refuséemail match un user, mais une des deux conditions C3 manque409 Conflict, l’utilisateur doit se connecter par mot de passe et lier Google manuellement plus tard409
Nouveau compteaucune correspondancecréation d’un user avec username placeholder (g_XXXXXXXX, 4 octets aléatoires en hex), confirmed_at = NOW(), mot de passe inutilisable (bcrypt aléatoire), profile_completed_at = NULL201registered
Banniuser trouvé mais bannedUntil futurrefus403
  • Réponse 200 / 201 — même forme que POST /api/auth/login plus :
{
"jsonapi": { "version": "1.1" },
"data": {
"type": "users",
"id": "<user-uuid>",
"attributes": {
"username": "g_3f2a91b0",
"email": "user@gmail.com",
"confirmedAt": "2026-06-06T12:34:56+00:00",
"profileCompletedAt": null,
"isConfirmed": true,
"isBanned": false
/* + tous les autres champs habituels */
}
},
"meta": {
"token": "<base64url-token>",
"sessionId": "<session-uuid>",
"expiresAt": "2026-07-06T12:34:56+00:00",
"oauthOutcome": "registered",
"profileComplete": false
}
}
  • Réponse 401Invalid Google ID token (signature invalide, expiré, mauvais iss/aud, JWKS injoignable, etc. — message volontairement non détaillé)
  • Réponse 403Account banned, meta.bannedUntil
  • Réponse 409Account exists with this email, meta.code = "oauth.linkRefused" (un compte existe mais l’auto-link est refusé)
  • Réponse 422idToken manquant ou vide
  • Les comptes créés via Google ont un username placeholder (g_<8-hex>) et profile_completed_at = NULL. Un endpoint dédié (à venir) permettra à l’utilisateur de choisir son vrai username.
  • Le mot de passe stocké est un bcrypt d’octets aléatoires CSPRNG — password_verify échoue toujours, donc impossible de se connecter via /api/auth/login tant que l’utilisateur n’a pas explicitement défini un mot de passe.
  • La table user_oauth_identity (migration) stocke (provider, provider_user_id) UNIQUE — un même compte Google ne peut être lié qu’à un seul user Hydrogen.

Récapitulatif des différences entre les trois providers supportés. Chaque cellule reflète une décision du framework Hydrogen, pas seulement une capacité du provider.

CapacitéGoogleAppleFacebook
Type de jeton acceptéOIDC ID Token (JWT)OIDC ID Token (JWT)Access token opaque OU OIDC ID Token (JWT)
Flux côté frontendun seul (idToken)un seul (idToken)deux : classic (web/Android) ou limited (iOS ≥13)
email fournitoujourstoujoursoptionnel (l’utilisateur peut refuser le scope)
email_verified signal côté provideroui (booléen)oui ("true"/"false" ou booléen, coercé)non
Auto-link sur email existant (C3, sign-in)oui si confirmé + verifiednon, jamais (E.a strict)non, jamais (E.a strict)
Lien manuel depuis le compte connectéouiouioui
email_is_relay possiblenonoui (@privaterelay.appleid.com)non
Prefix du username placeholder à la créationg_<8-hex>a_<8-hex>f_<8-hex>
Variables d’env requisesGOOGLE_OAUTH_CLIENT_IDAPPLE_OAUTH_CLIENT_IDS (liste)FACEBOOK_APP_ID, FACEBOOK_APP_SECRET
JWKSgoogleapis.com/oauth2/v3/certsappleid.apple.com/auth/keysfacebook.com/.well-known/oauth/openid/jwks/

Politique E.a (strict) — Apple et Facebook ne sont jamais auto-liés à un compte Hydrogen existant qui aurait le même email. Raison : Apple Private Relay réduit l’assurance d’identité (l’utilisateur peut relayer un email vers n’importe quelle boîte) et Facebook ne renvoie aucun email_verified. Si un compte existe déjà avec cet email, la réponse est 409 oauth.linkRefused — le client doit demander une connexion par mot de passe puis appeler POST /api/users/me/oauth/<provider> depuis l’espace authentifié.


Échange un Apple ID Token (issu de Sign in with Apple) contre un bearer token de session Hydrogen.

  • Auth : aucune
  • Action : AppleLoginAction
  • Pré-requis serveur : variable d’env APPLE_OAUTH_CLIENT_IDS (liste séparée par des virgules des Services ID / Bundle ID acceptés ; Apple permet à un seul Developer Team de signer pour plusieurs aud, tous valides pour le même sub).
  • Request body (forme plate ou JSON:API data.attributes) :
{ "idToken": "<apple id_token>" }

AppleIdTokenVerifier :

  1. Récupère les JWKS Apple (https://appleid.apple.com/auth/keys), cache disque TTL 1h sous var/cache/oauth/apple_jwks.json.
  2. Vérifie la signature RS256, iss = https://appleid.apple.com, aud ∈ APPLE_OAUTH_CLIENT_IDS, exp (skew 60s).
  3. Extrait sub, email, email_verified (coercé en booléen — Apple envoie parfois "true"/"false" sous forme de string).

Identique à Google sauf que l’auto-link est désactivé (E.a strict) : un user Hydrogen existant avec le même email reçoit toujours 409 oauth.linkRefused, jamais linkedToExistingUser.

CasRésultatCodemeta.oauthOutcome
Identité Apple déjà liéesign-in du user lié200existingIdentity
Email match user existantrefus auto-link (E.a)409 oauth.linkRefused
Nouveau comptecréation (username = a_XXXXXXXX, confirmed_at = NOW(), mot de passe inutilisable)201registered
Bannirefus403
  • Réponse 200 / 201 — même forme que Google plus meta.emailIsRelay: true|false (drapeau Private Relay).
  • Réponse 401 — id_token non vérifiable
  • Réponse 403Account banned
  • Réponse 409oauth.linkRefused
  • Réponse 422idToken manquant

Quand l’utilisateur choisit « Hide My Email » au moment du consentement, Apple émet un alias <random>@privaterelay.appleid.com. Cet alias est traité comme un email normal côté Hydrogen (envois fonctionnent, MX d’Apple), mais :

  • la colonne user_oauth_identity.email_is_relay = 1 est positionnée,
  • la meta de la réponse expose emailIsRelay: true,
  • l’application cliente doit informer l’utilisateur que désactiver le forwarding lui ferait perdre l’accès au compte (clef de traduction auth.oauth.apple.relayNotice).

Échange un jeton Facebook contre un bearer token de session Hydrogen. Deux flux sont supportés, sélectionnés par le champ flow du body.

  • Auth : aucune
  • Action : FacebookLoginAction
  • Pré-requis serveur : FACEBOOK_APP_ID et FACEBOOK_APP_SECRET (le secret reste sur le serveur, utilisé pour construire l’app_token = <id>|<secret> consommé par debug_token).
  • Request body :
// Flux classique (web SDK, Android)
{ "flow": "classic", "accessToken": "<facebook access token>" }
// Flux limited (iOS ≥ 13)
{ "flow": "limited", "idToken": "<facebook OIDC id_token>" }
  • Flux classic : POST https://graph.facebook.com/v19.0/debug_token?input_token=…&access_token=<app_id>|<app_secret> puis GET /v19.0/me?fields=id,email. Validations : is_valid = true, app_id correspond à FACEBOOK_APP_ID, expires_at strictement dans le futur ou égal à 0 (long-lived tokens), profile.id égal au user_id retourné par debug_token.
  • Flux limited : JWKS https://www.facebook.com/.well-known/oauth/openid/jwks/ (cache TTL 1h), iss = https://www.facebook.com, aud = FACEBOOK_APP_ID, skew 60s.

Dans les deux cas, l’email est optionnel — si l’utilisateur a refusé le scope email, la sortie normalisée FacebookProfile a email = null.

CasRésultatCodemeta.oauthOutcome
Pas d’email retourné (email = null)refus, code oauth.facebook.emailMissing422
Identité Facebook déjà liéesign-in200existingIdentity
Email match user existantrefus auto-link (E.a)409 oauth.linkRefused
Nouveau comptecréation (username = f_XXXXXXXX)201registered
Bannirefus403
  • Réponse 401Invalid Facebook token (signature/issuer/audience/expiration, ou debug_token refuse)
  • Réponse 403Account banned
  • Réponse 409oauth.linkRefused
  • Réponse 422flow manquant/invalide, ou accessToken/idToken manquant selon le flow, ou oauth.facebook.emailMissing
  • Facebook ne fournit aucun signal email_verified sur ses APIs publiques. Hydrogen part du principe que l’email Facebook n’est pas attesté — d’où la politique stricte (E.a) et le refus d’auto-link sur email existant.
  • Le name/profile.name de Facebook n’est pas récupéré : à l’inscription, on ne demande que id + email, et l’utilisateur complétera son profil ensuite (politique B.a).

Échange un couple email + password contre un bearer token de session.

  • Auth : aucune
  • Action : LoginAction
  • Request body — deux formes acceptées :

Forme plate :

{
"email": "user@example.com",
"password": "secret"
}

Forme JSON:API :

{
"data": {
"type": "credentials",
"attributes": {
"email": "user@example.com",
"password": "secret"
}
}
}
  • Réponse 200 — session créée :
{
"jsonapi": { "version": "1.1" },
"data": {
"type": "users",
"id": "<user-uuid>",
"attributes": {
"username": "havoc",
"nickname": "Havoc",
"email": "user@example.com",
"displayName": "Havoc",
"userType": "member",
"status": "active",
"isVerified": false,
"experience": 0,
"level": 1,
"levelProgress": 0.00,
"isConfirmed": true,
"isBanned": false
/* + name, firstname, sex, birthdate, bio, joinedAt, ... */
}
},
"meta": {
"token": "<base64url-token>",
"sessionId": "<session-uuid>",
"expiresAt": "2026-07-06T12:34:56+00:00"
}
}
  • Réponse 400 — corps non-JSON ou non-objet

  • Réponse 401Invalid credentials (email inconnu ou mauvais mot de passe — message volontairement ambigu pour ne pas faciliter l’énumération)

  • Réponse 403Account banned, meta.bannedUntil indique la date de fin (ou null si permanent) ou Email not confirmed, meta.code = "email.notConfirmed" (compte créé mais email pas encore validé via POST /api/auth/confirm-email)

  • Réponse 422 — un ou plusieurs errors[] avec source.pointer = /data/attributes/email ou /data/attributes/password

  • Réponse 429Too many login attempts. Header Retry-After: <seconds> + meta.retryAfterSeconds + meta.triggeredBy ("ip" ou "email"). Voir Rate limiting ci-dessous.

  • Notes :

    • Le mot de passe est re-hashé automatiquement si le coût bcrypt actuel (12) ne correspond pas au hash en base.
    • Le token brut n’est jamais stocké en base — seul son SHA-256 (32 octets) l’est.

Sliding window de 15 minutes, deux buckets indépendants évalués en cascade (email d’abord, puis IP) :

BucketSeuilEffet
email5 échecsBloque toute tentative pour cet email pendant le reste de la fenêtre.
ip20 échecsBloque toute tentative depuis cette IP pendant le reste de la fenêtre.
  • Seuls les échecs (401 Invalid credentials) incrémentent les compteurs. 400/422/403 ne comptent pas.
  • Un login réussi vide le bucket email du compte (slate propre pour l’utilisateur légitime) ; le bucket IP reste — protège contre le credential spray multi-comptes.
  • Réponse bloquée : 429 + header HTTP Retry-After: <secondes> + meta.retryAfterSeconds (même valeur) + meta.triggeredBy ("email" ou "ip").
{
"jsonapi": { "version": "1.1" },
"errors": [
{
"status": "429",
"title": "Too many login attempts",
"detail": "Login is temporarily blocked. Try again later.",
"meta": {
"retryAfterSeconds": 612,
"triggeredBy": "email"
}
}
]
}

Limites définies dans LoginThrottle (MAX_PER_EMAIL, MAX_PER_IP, WINDOW_MINUTES). Stockage : table login_attempt (migration).


Retourne l’utilisateur courant à partir du bearer token.

  • Auth : requise (Bearer)
  • Action : MeAction
  • Réponse 200 :
{
"jsonapi": { "version": "1.1" },
"data": {
"type": "users",
"id": "<user-uuid>",
"attributes": {
"username": "havoc",
"nickname": "Havoc",
"email": "user@example.com",
"displayName": "Havoc",
"userType": "member",
"status": "active",
"isVerified": false,
"experience": 0,
"level": 1,
"levelProgress": 0.00,
"isConfirmed": true,
"isBanned": false
}
}
}
  • Réponse 401 — token absent / mal formé / expiré / révoqué
  • Effet de bord : chaque appel rafraîchit last_used_at et fait glisser expires_at de +30 jours.

Liste toutes les sessions actives (non expirées) de l’utilisateur courant, triées par dernier usage décroissant. Sert à alimenter une page « Mes sessions actives » avec un bouton de révocation par session.

{
"jsonapi": { "version": "1.1" },
"data": [
{
"type": "userSessions",
"id": "<session-uuid-1>",
"attributes": {
"createdAt": "2026-06-06T10:00:00+00:00",
"lastUsedAt": "2026-06-06T12:30:00+00:00",
"expiresAt": "2026-07-06T12:30:00+00:00",
"userAgent": "Mozilla/5.0 (...)",
"ipAddress": "127.0.0.1",
"isCurrent": true
}
},
{
"type": "userSessions",
"id": "<session-uuid-2>",
"attributes": {
"createdAt": "2026-06-05T08:15:00+00:00",
"lastUsedAt": "2026-06-05T20:00:00+00:00",
"expiresAt": "2026-07-05T20:00:00+00:00",
"userAgent": "Hydrogen-iOS/1.0",
"ipAddress": "2a01:e35:...",
"isCurrent": false
}
}
],
"meta": { "count": 2 }
}
  • Réponse 401 — token absent / invalide
  • Notes :
    • ipAddress est rendu en format lisible (inet_ntop sur les octets stockés en VARBINARY(16)), IPv4 ou IPv6.
    • isCurrent flag la session associée au bearer token de la requête en cours — utile pour griser un bouton « révoquer » dans l’UI.
    • L’appel lui-même fait glisser expiresAt de la session courante (effet de bord normal du middleware d’auth).

Révoque une session précise par son UUID. La session doit appartenir à l’utilisateur courant — sinon 404 (volontaire : ne fuite pas l’existence des sessions d’autres comptes). Si l’{id} cible la session courante, le token utilisé devient immédiatement invalide.

  • Auth : requise (Bearer)
  • Action : RevokeSessionAction
  • Path params :
    • id (string, UUID) — id de la session à révoquer (typiquement obtenu via GET /api/auth/sessions).
  • Request body : aucun
  • Réponse 204 — révoquée avec succès, pas de contenu
  • Réponse 400{id} n’est pas un UUID valide
  • Réponse 404 — aucune session avec cet id n’appartient au user courant (id inconnu ou appartient à un autre user — même réponse pour ne pas révéler l’existence)
  • Réponse 401 — token absent / invalide
  • Notes :
    • Pour révoquer toutes les autres sessions d’un coup, utiliser plutôt POST /api/auth/logout-all (mais celui-ci tue aussi la session courante).
    • L’UI typique appelle cet endpoint depuis un bouton « Révoquer » sur chaque ligne de la liste rendue par GET /api/auth/sessions.

Révoque uniquement la session courante (celle associée au token utilisé).

  • Auth : requise (Bearer)
  • Action : LogoutAction
  • Request body : aucun
  • Réponse 204 — pas de contenu
  • Réponse 401 — token absent / invalide

Révoque toutes les sessions de l’utilisateur courant (déconnexion de tous les appareils). Le token utilisé pour cet appel est lui-même invalidé.

  • Auth : requise (Bearer)
  • Action : LogoutAllAction
  • Request body : aucun
  • Réponse 200 :
{
"jsonapi": { "version": "1.1" },
"data": {
"type": "sessionRevocations",
"id": "<user-uuid>",
"attributes": {
"revokedCount": 3
}
}
}
  • Réponse 401 — token absent / invalide
  • Cas d’usage typiques : bouton « Se déconnecter de tous les appareils », rotation de mot de passe, suspicion de compromission.