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
| Champ | Type | Requis | Description |
|---|---|---|---|
login | string | Oui | Email (plateforme), login (organisation) ou code public (ex: CL-EL-0008) |
password | string | Oui | Mot de passe en clair |
tenant_slug | string | Non | Slug de l'organisation (requis pour un login organisation non-code) |
Comportement
- Le contexte tenant est déterminé par la présence de
tenant_slugou siloginressemble à un code (looks_like_code). - Si contexte tenant → tente d'abord un agent/tenant, puis un porteur de carte.
- Sinon → authentification plateforme uniquement.
- Les erreurs
401sont normalisées enAUTH_INVALID_CREDENTIALSpour 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
}
}
| Champ | Type | Description |
|---|---|---|
access_token | string | JWT d'accès (12h) |
refresh_token | string | JWT de rafraîchissement (30j) |
token_type | string | Toujours "Bearer" |
expires_in | integer | Durée de vie de l'access token en secondes |
user.kind | string | "platform", "tenant" ou "card_holder" |
user.permissions | string[] | Permissions résolues |
user.password_change_required | boolean | Force le changement de mot de passe |
Erreurs
| Code | Description |
|---|---|
401 (AUTH_INVALID_CREDENTIALS) | Login ou mot de passe incorrect |
403 | Compte 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
| Champ | Type | Requis | Description |
|---|---|---|---|
login | string | Oui | Code, login, téléphone ou email du porteur |
password | string | Oui | PIN / mot de passe |
tenant_slug | string | Non | Optionnel si login est un code public |
Comportement
- Résout l'organisation via
tenant_slugou par le préfixe du code public. - Résout le login en cascade : code → login → téléphone → email.
- Vérifie l'organisation (active et opérationnelle pour les porteurs).
- Émet un JWT
kind = card_holderaveccard_idetcard_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
}
}
- À 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
| Code | Description |
|---|---|
401 | Refresh 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"
}
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
| Code | Description |
|---|---|
401 | Token d'accès manquant ("Token d'accès requis") ou invalide |
401 | Mot 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
| Param | Type | Requis | Description |
|---|---|---|---|
code | string | Oui | Code 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
| Code | Description |
|---|---|
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 :
kind | Champs supplémentaires |
|---|---|
platform | email |
tenant | username, email, phone_number, code |
card_holder | username, 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"]
}
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
| Token | Durée |
|---|---|
| Access token | 12 heures (720 min) |
| Refresh token | 30 jours |
Token temporaire porteur (PartiallyAuthenticated) | 5 minutes |
| Code SMS | 5 minutes |
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.