Aller au contenu principal

Authentification

Base URL : /api/v1/auth

Tous les endpoints de ce module sont publics (aucun middleware d'authentification). Deux d'entre eux (/change-password et /me) exigent néanmoins un en-tête Authorization: Bearer vérifié manuellement dans le handler.


POST /auth/login​

Authentifie un utilisateur et retourne un couple access/refresh token. Endpoint unifié qui gère les trois types d'utilisateurs : plateforme, organisation et porteur de carte.

POST /api/v1/auth/login
Content-Type: application/json

{
"login": "jean.mbeki",
"password": "••••••••",
"tenant_slug": "cours-lumiere"
}

Corps de la requête​

ChampTypeRequisDescription
loginstringOuiEmail (plateforme), login (organisation) ou code public (ex: CL-EL-0008)
passwordstringOuiMot de passe en clair
tenant_slugstringNonSlug de l'organisation (requis pour un login organisation non-code)

Comportement​

  1. Le contexte tenant est déterminé par la présence de tenant_slug ou si login ressemble à un code (looks_like_code).
  2. Si contexte tenant → tente d'abord un agent/tenant, puis un porteur de carte.
  3. Sinon → authentification plateforme uniquement.
  4. Les erreurs 401 sont normalisées en AUTH_INVALID_CREDENTIALS pour faciliter la gestion frontend.

Réponse (200)​

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 43200,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"display_name": "Jean Mbeki",
"kind": "tenant",
"permissions": ["card.read", "card.recharge.cash"],
"organization_id": "550e8400-e29b-41d4-a716-446655440001",
"organization_slug": "cours-lumiere",
"organization_name": "Cours Lumière",
"password_change_required": false
}
}
ChampTypeDescription
access_tokenstringJWT d'accès (12h)
refresh_tokenstringJWT de rafraîchissement (30j)
token_typestringToujours "Bearer"
expires_inintegerDurée de vie de l'access token en secondes
user.kindstring"platform", "tenant" ou "card_holder"
user.permissionsstring[]Permissions résolues
user.password_change_requiredbooleanForce le changement de mot de passe

Erreurs​

CodeDescription
401 (AUTH_INVALID_CREDENTIALS)Login ou mot de passe incorrect
403Compte désactivé ou organisation suspendue

POST /auth/cardholder-login​

Endpoint dédié à l'authentification des porteurs de carte. Rétro-compatible : contrairement à /login, les erreurs 401 ne sont pas normalisées (l'erreur du service est renvoyée telle quelle).

POST /api/v1/auth/cardholder-login
Content-Type: application/json

{
"login": "CL-EL-0008",
"password": "••••"
}

Corps de la requête​

ChampTypeRequisDescription
loginstringOuiCode, login, téléphone ou email du porteur
passwordstringOuiPIN / mot de passe
tenant_slugstringNonOptionnel si login est un code public

Comportement​

  1. Résout l'organisation via tenant_slug ou par le préfixe du code public.
  2. Résout le login en cascade : code → login → téléphone → email.
  3. Vérifie l'organisation (active et opérationnelle pour les porteurs).
  4. Émet un JWT kind = card_holder avec card_id et card_number.

Réponse (200)​

Identique à /login (même structure LoginResponse).


POST /auth/refresh​

Renouvelle l'access token à partir d'un refresh token. Les permissions sont re-résolues depuis la base.

POST /api/v1/auth/refresh
Content-Type: application/json

{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Réponse (200)​

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "uuid",
"display_name": "Jean Mbeki",
"kind": "tenant",
"permissions": ["card.read", "card.recharge.cash"],
"organization_id": "uuid",
"organization_slug": "cours-lumiere",
"organization_name": "Cours Lumière",
"password_change_required": false
}
}
Rotation et détection de vol
  • À chaque utilisation, l'ancien refresh token est supprimé de la base et un nouveau est émis.
  • Si l'identifiant du token est introuvable → toutes les sessions et tokens de l'utilisateur sont révoqués (détection de vol).
  • Si le token a déjà été révoqué → erreur session_replaced.
  • En cas de mismatch token/session → la session est révoquée.

Erreurs​

CodeDescription
401Refresh token invalide ou expiré

POST /auth/logout​

Révoque un refresh token côté serveur.

POST /api/v1/auth/logout
Content-Type: application/json

{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Réponse (200)​

{
"message": "Déconnexion réussie"
}
remarque

L'access token reste valide jusqu'à son expiration naturelle (12h). Pour une déconnexion immédiate, supprimez aussi l'access token côté client. Si une session est présente, toutes ses tokens sont révoquées.


POST /auth/change-password​

Change le mot de passe / PIN de l'utilisateur connecté. Route publique mais exige un Authorization: Bearer.

POST /api/v1/auth/change-password
Authorization: Bearer {access_token}
Content-Type: application/json

{
"current_password": "••••••••",
"new_password": "••••••••"
}

Réponse (200)​

{
"message": "Mot de passe modifié"
}

À la réussite, l'indicateur must_change_password de l'utilisateur est remis à false.

Erreurs​

CodeDescription
401Token d'accès manquant ("Token d'accès requis") ou invalide
401Mot de passe actuel incorrect

GET /auth/resolve-code​

Résout un code public d'identification (CL-EL-0008) en informations d'organisation. Utile pour déterminer à quelle organisation appartient un utilisateur avant le login.

GET /api/v1/auth/resolve-code?code=CL-EL-0008

Paramètres de query​

ParamTypeRequisDescription
codestringOuiCode public d'identification

Réponse (200)​

{
"id": "550e8400-e29b-41d4-a716-446655440001",
"slug": "cours-lumiere",
"short_code": "CL",
"name": "Cours Lumière",
"schema_name": "tenant_cours_lumiere"
}

Erreurs​

CodeDescription
400"Code d'identification invalide"
404"Aucune organisation trouvée pour ce code"

GET /auth/me​

Retourne le profil de l'utilisateur connecté. Route publique avec Authorization: Bearer vérifié dans le handler.

GET /api/v1/auth/me
Authorization: Bearer {access_token}

Réponse (200)​

La forme dépend du kind de l'utilisateur, avec des champs communs toujours présents.

Champs communs :

{
"id": "uuid",
"display_name": "Jean Mbeki",
"kind": "tenant",
"permissions": ["card.read"],
"organization_id": "uuid",
"organization_slug": "cours-lumiere",
"organization_name": "Cours Lumière",
"current_organization_id": "uuid",
"current_organization_name": "Cours Lumière",
"password_change_required": false
}

Champs additionnels par type :

kindChamps supplémentaires
platformemail
tenantusername, email, phone_number, code
card_holderusername, first_name, last_name, email, phone_number, code, establishment_name

Flux d'authentification​

sequenceDiagram
participant Client
participant API

Note over Client: 1. Login
Client->>API: POST /auth/login<br/>{login, password, tenant_slug?}
API->>API: Résout tenant + crédentials<br/>Résout permissions
API->>Client: {access_token, refresh_token, user}

Note over Client: 2. Usage normal
Client->>API: GET /cards/...<br/>Authorization: Bearer {access_token}
API->>Client: {data}

Note over Client: 3. Access expire (12h)
Client->>API: POST /auth/refresh {refresh_token}
API->>API: Rotation + re-résolution permissions
API->>Client: {access_token, refresh_token, user}

Note over Client: 4. Refresh expire (30j)
Client->>API: POST /auth/login

Structure du JWT​

L'access token est un JWT HMAC-SHA256. Les claims varient selon le type d'utilisateur.

Utilisateur plateforme / organisation (kind = Platform | Tenant) :

{
"sub": "uuid-utilisateur",
"exp": 1750000000,
"iat": 1749999100,
"kind": "Tenant",
"display_name": "Jean Mbeki",
"organization_id": "550e8400-e29b-41d4-a716-446655440001",
"organization_slug": "cours-lumiere",
"permissions": ["card.read", "card.recharge.cash"]
}

Porteur de carte (kind = CardHolder) :

{
"sub": "uuid-porteur",
"exp": 1750000000,
"iat": 1749999100,
"kind": "CardHolder",
"display_name": "Fatou Diallo",
"card_id": "550e8400-e29b-41d4-a716-446655440002",
"card_number": "CL-EL-0008",
"organization_id": "550e8400-e29b-41d4-a716-446655440001",
"organization_slug": "cours-lumiere",
"permissions": ["card.read", "card.recharge.mobile_money"]
}
Casse des claims

Dans le JWT, kind est en PascalCase ("Tenant", "Platform", "CardHolder"). Dans les réponses JSON (user.kind), il est en snake_case minuscule ("tenant", "platform", "card_holder").

Durées de vie​

TokenDurée
Access token12 heures (720 min)
Refresh token30 jours
Token temporaire porteur (PartiallyAuthenticated)5 minutes
Code SMS5 minutes
Mode bootstrap

Si aucune organisation ni aucun rôle n'existe dans le système, le premier utilisateur créé reçoit toutes les permissions. Cela permet l'initialisation initiale de la plateforme.