Mobile Auth Session Contract For Front¶
Ce document formalise le contrat cible entre l'app mobile et le backend pour la nouvelle auth first-party.
Statut: contrat cible V1, pas encore implémentation courante Date de mise à jour: 2026-04-28
Référence backend longue: heymoov-api/reference/auth_target_architecture.md
Référence onboarding transverse: heymoov-docs/reference/contracts/reference-auth-first-party-onboarding.md
Décisions V1¶
- passkeys = méthode de login nominale
- password = fallback optionnel de migration, recovery et support
- magic link login = hors cible
- Hydra navigateur ne fait pas partie du login mobile first-party cible
- pas de JWT mobile comme source de vérité d'auth ou de rôles
- session mobile =
accessTokenopaque court +refreshTokenopaque rotatif sessionIdetdeviceIdsont des identifiants backend non secrets- le contexte UI vient du bootstrap courant
GET /v1/mobile/api/me - pas de
device-bound refreshen V1
SessionEnvelope¶
Tous les flows d'auth réussis renvoient le même envelope.
{
"accessToken": "opaque_random_token",
"accessExpiresAt": "2026-04-27T12:15:00Z",
"refreshToken": "opaque_random_token",
"refreshExpiresAt": "2026-05-27T12:00:00Z",
"sessionId": "mobile_session_id",
"deviceId": "40a86471-a4b1-4105-bf63-5758db93f47b",
"bootstrapVersion": 42
}
Règles client:
- envoyer
Authorization: Bearer <accessToken>sur les routes privées mobile - ne jamais décoder
accessToken - ne jamais déduire
userId,personId,rolesoupermissionsdepuis un token local - stocker
refreshToken,sessionId,deviceIdetbootstrapVersionen secure storage deviceIdest lemobile_devices.idémis par le backend dans leSessionEnvelope- le mobile ne doit jamais générer
deviceIdlui-même, ni y mettre un identifiant d'installation local - garder
accessTokenen mémoire de préférence, ou le stocker en secure storage seulement si le client doit le persister - remplacer l'envelope local complet après chaque refresh réussi
- supprimer tout l'état local auth après logout ou erreur de refresh terminale
Login et deviceId¶
Sur les endpoints qui ouvrent une session mobile (password/login, passkeys/login/verify, onboarding ou recovery):
- au premier login, ou si aucun
deviceIdbackend n'est disponible, omettredeviceId - si un
deviceIdest envoyé, il doit être la valeur retournée précédemment dans unSessionEnvelope - un
deviceIdenvoyé doit être un UUID backend connu commemobile_devices.id deviceIdne participe pas à la vérification passkey/WebAuthn- les métadonnées appareil (
platform,appVersion,deviceName,deviceModel,osVersion) restent les champs à envoyer pour décrire le device courant
Endpoints Mobile¶
Passkeys:
POST /v1/mobile/auth/passkeys/login/optionsPOST /v1/mobile/auth/passkeys/login/verifyPOST /v1/mobile/auth/passkeys/register/optionsPOST /v1/mobile/auth/passkeys/register/verify
Fallbacks:
POST /v1/mobile/auth/password/login
Onboarding et recovery:
POST /v1/public/onboarding/resolvePOST /v1/public/onboarding/passkey/optionsPOST /v1/public/onboarding/passkey/verifyPOST /v1/public/onboarding/password/completePOST /v1/public/account-recovery/requestPOST /v1/public/account-recovery/resolvePOST /v1/public/account-recovery/passkey/optionsPOST /v1/public/account-recovery/passkey/verifyPOST /v1/public/account-recovery/password/complete
Session:
POST /v1/mobile/session/refreshPOST /v1/mobile/session/logoutPOST /v1/mobile/session/logout-allGET /v1/mobile/session/devicesDELETE /v1/mobile/session/devices/{deviceId}
Les endpoints d'auth, de refresh et de bootstrap courant (/v1/mobile/api/me) doivent renvoyer Cache-Control: no-store.
Bootstrap¶
Request:
GET /v1/mobile/api/me
Authorization: Bearer <accessToken>
Response cible:
{
"user": {},
"person": {},
"roles": [],
"permissions": [],
"auth": {
"sessionId": "mobile_session_id",
"deviceId": "40a86471-a4b1-4105-bf63-5758db93f47b",
"authMethod": "passkey",
"authLevel": "aal2",
"passkeyEnrolled": true,
"reauthRequiredAt": "2026-04-27T14:00:00Z"
},
"bootstrapVersion": 42
}
Le bootstrap doit être rechargé:
- au démarrage à froid si une session existe
- après login réussi
- après refresh réussi si
bootstrapVersionchange - après retour au premier plan au-delà d'un seuil défini
- après réception d'un événement d'invalidation de contexte
Le bootstrap ne doit pas être rechargé à chaque navigation ou à chaque requête métier.
Refresh¶
Request:
{
"refreshToken": "opaque_random_token"
}
Response:
SessionEnvelope
Règles client:
- appeler
refreshquand l'API privée renvoie une expiration d'access token ou avant expiration connue - ne pas envoyer
deviceIddans la payload de refresh; lerefreshTokenidentifie déjà la session serveur - remplacer immédiatement l'ancien
refreshTokenpar le nouveau - ne jamais réutiliser volontairement un ancien
refreshToken - si
refreshrenvoie une erreur terminale, supprimer l'état local et retourner au login
Erreurs Contractuelles¶
Les codes exacts peuvent être ajustés, mais le client doit pouvoir distinguer:
invalid_credentialschallenge_expiredsession_expiredrefresh_reusedprofile_bannedreauth_required
Suivi Mobile¶
TODO: remplacer le flow navigateur Hydra par les écrans first-partyTODO: intégrer passkeys iOS/AndroidTODO: ajouter password login fallbackTODO: ajouter onboarding passkey/passwordTODO: ajouter recovery passkey/password sans login direct par lienTODO: stocker et remplacer leSessionEnvelopeselon la politique secure storage/mémoireTODO: ajouter refresh automatique avec sérialisation des appels concurrentsTODO: appeler le bootstrap/v1/mobile/api/meau démarrage, après login et après changement de versionTODO: supprimer la lecture duUI JWTpour le routing et les rôlesTODO: ajouter logout appareil et logout globalTODO: gérer les erreurs terminales de refresh par retour loginTODO: migrer ou purger l'ancien état SecureStoretoken/sid