Authentification
Connexion, double authentification, jeton d’accès, cookie de renouvellement, sessions et mot de passe oublié.
L’API authentifie des utilisateurs. Une intégration se connecte avec l’e-mail et le mot de passe d’un compte, reçoit un jeton d’accès de courte durée et le renouvelle grâce à un cookie. Il n’existe ni clé d’API, ni compte de service, ni jeton personnel de longue durée (ticket #37).
Pour une intégration, créez un compte dédié et donnez-lui uniquement les rôles nécessaires, sur les seules entités concernées (voir Organisation, entités et permissions).
Se connecter
POST /api/v1/auth/login
{ "email": "[email protected]", "password": "••••••••••••" }
Le mot de passe est obligatoire et limité à 200 caractères (contrôle de format de la requête, réponse 400). Aucune longueur minimale n’est exigée à la connexion : un mot de passe trop court reçoit la même réponse 401 qu’un mot de passe faux. La règle de choix d’un mot de passe (12 à 200 caractères, avec majuscule, minuscule, chiffre et caractère spécial), complétée par la politique de mot de passe de l’organisation du compte (longueur minimale plus élevée, historique), s’applique à l’inscription, à l’acceptation d’une invitation, au changement de mot de passe (PATCH /api/v1/auth/me/password), à l’étape de changement de la connexion et à la réinitialisation (POST /api/v1/auth/reset-password). Un bon mot de passe qui a expiré ou ne respecte plus cette politique ouvre la connexion, mais mène à l’étape password_change au lieu d’une session (voir Étapes de connexion).
Réponse 200 :
{
"accessToken": "eyJhbGciOiJSUzI1NiIs…",
"expiresIn": 900,
"session": {
"id": "6f1c…",
"expiresAt": "2026-10-15T16:00:00Z",
"idleExpiresAt": "2026-10-15T10:00:00Z",
"maxDurationHours": 8,
"inactivityTimeoutMinutes": 120
},
"requiresTwoFactor": false,
"twoFactorToken": null
}
accessToken: le jeton à envoyer dans l’en-têteAuthorization: Bearer <accessToken>.expiresIn: sa durée de validité, en secondes : 15 minutes dans la configuration par défaut (jamais plus d’une heure, quelle que soit la configuration du serveur), moins si la session atteint sa durée maximale avant. Fiez-vous à la valeur reçue plutôt qu’à ce chiffre.session: la session ouverte et ses deux échéances (voir Sessions).- La réponse dépose aussi le cookie de renouvellement (voir plus bas).
En cas d’échec, la réponse est 401 avec le même message, que l’e-mail soit inconnu, le mot de passe faux ou le compte désactivé :
{ "type": "authentication-error", "title": "Email ou mot de passe incorrect.", "status": 401 }
Verrouillage : après 5 échecs, le compte est verrouillé 15 minutes ; pendant ce délai, même le bon mot de passe est refusé (401, « Compte temporairement verrouillé suite à trop de tentatives. »). Les codes de double authentification erronés comptent aussi, comme un mot de passe actuel erroné lors d’un changement de mot de passe. Par ailleurs, la connexion est limitée à 5 tentatives par minute et par adresse IP (Limites de débit).
Double authentification
Si le compte a activé la double authentification (TOTP), la connexion se fait en deux temps.
-
POST /api/v1/auth/loginrépond200sans jeton ni cookie :{ "accessToken": "", "expiresIn": 0, "requiredStep": "two_factor", "challengeToken": "…", "requiresTwoFactor": true, "twoFactorToken": "…" }requiredStepdésigne l’étape attendue par un code stable ;challengeTokenest le jeton de cette étape.requiresTwoFactorettwoFactorToken(même valeur quechallengeToken) sont conservés pour compatibilité. -
POST /api/v1/auth/login/2faavec cetwoFactorTokenet le code :{ "twoFactorToken": "…", "code": "123456" }Le code est celui à 6 chiffres de l’application d’authentification (période de 30 secondes, une période de tolérance de part et d’autre) ou l’un des codes de secours à usage unique. La réponse est alors identique à une connexion simple.
Le twoFactorToken est valable 5 minutes et accepte 5 codes erronés ; une nouvelle connexion par mot de passe l’annule. Un jeton ou un code refusé renvoie 401 avec « Code de vérification invalide ou expiré. », sans préciser lequel des deux est en cause ; un compte verrouillé renvoie 401 avec « Compte temporairement verrouillé suite à trop de tentatives. », et une adresse IP non autorisée 403 (voir plus bas).
Étapes de connexion
Tant que requiredStep n’est pas null, la réponse ne contient ni jeton d’accès ni cookie : il faut franchir l’étape indiquée avec son challengeToken, qui n’est accepté que par le point d’entrée de cette étape. Les étapes s’enchaînent dans cet ordre : mot de passe, code de double authentification, renouvellement de la clé, changement de mot de passe, puis session. Une intégration doit se fonder sur la valeur de requiredStep, jamais sur un message, et refuser une valeur qu’elle ne connaît pas : d’autres étapes pourront s’ajouter.
requiredStep | Point d’entrée | Corps |
|---|---|---|
two_factor | POST /api/v1/auth/login/2fa | { "twoFactorToken": "…", "code": "123456" } |
two_factor_reenrollment | POST /api/v1/auth/login/2fa/enrollment | { "challengeToken": "…", "code": "123456" } |
password_change | POST /api/v1/auth/login/password-change | { "challengeToken": "…", "currentPassword": "…", "newPassword": "…" } |
Renouvellement de la clé (two_factor_reenrollment). Les clés de double authentification activées avant la correction du QR code (transmis à un service tiers, api.qrserver.com) doivent être remplacées une fois. Après un code valide, POST /auth/login/2fa répond alors :
{
"accessToken": "", "expiresIn": 0,
"requiredStep": "two_factor_reenrollment", "challengeToken": "…",
"twoFactorEnrollment": { "secret": "…", "qrCodeUri": "otpauth://…", "qrCodeImage": "data:image/png;base64,…" }
}
Le code envoyé à POST /auth/login/2fa/enrollment doit être généré à partir de la nouvelle clé (un code de l’ancienne ou un code de secours est refusé). En cas de succès, la nouvelle clé remplace l’ancienne et la réponse est celle d’une connexion simple. Ce jeton vaut 10 minutes et accepte 5 codes erronés, avec les mêmes refus que l’étape précédente. Tant que l’étape n’est pas franchie, POST /auth/refresh refuse le cookie existant (401) et l’invalide : aucun chemin ne délivre de session avant le renouvellement. Les codes de secours restent inchangés.
Changement de mot de passe (password_change). Dernière étape, après tous les facteurs : elle est demandée quand le mot de passe a expiré selon la politique de l’organisation du compte, ou quand le mot de passe saisi ne respecte plus sa règle (choisi sous une règle plus ancienne, ou avant que l’organisation n’augmente la longueur minimale). La réponse indique la raison et la règle à respecter :
{
"accessToken": "", "expiresIn": 0,
"requiredStep": "password_change", "challengeToken": "…",
"passwordChange": { "reason": "expired", "minLength": 16, "historyCount": 5 }
}
reason vaut expired ou noncompliant. POST /auth/login/password-change reçoit le mot de passe actuel et le nouveau ; en cas de succès, la réponse est celle d’une connexion simple et toutes les autres sessions du compte sont fermées. Le jeton vaut 10 minutes et accepte 5 tentatives : un mot de passe actuel erroné compte comme un échec (et pour le verrouillage du compte) : 401 avec « Mot de passe actuel incorrect, ou étape expirée : recommencez la connexion. », la même réponse qu’un jeton inutilisable. Un nouveau mot de passe refusé par la politique (trop court, déjà utilisé récemment…) reçoit 400 validation-error avec le motif, sans consommer de tentative : l’étape reste ouverte.
Une fois le mot de passe expiré, POST /auth/refresh refuse le cookie existant (401, code password_expired, voir Renouveler le jeton) : il faut se reconnecter, ce qui mène à cette étape. GET /api/v1/auth/me renvoie la politique qui s’applique au compte, passwordPolicy : minLength, historyCount, expiresAt (date d’expiration du mot de passe actuel, null s’il n’expire pas) et expiryWarningDays (nombre de jours avant l’expiration à partir duquel l’application prévient). expiresAt vaut le délai compté depuis le dernier changement du mot de passe, ou depuis l’activation de l’expiration par l’organisation si elle est plus récente ; modifier un délai déjà actif ne déplace pas ce point de départ, si bien qu’un délai raccourci peut faire expirer aussitôt un mot de passe.
Une intégration non interactive ne peut pas fournir de code TOTP sans détenir le secret du compte. Si la double authentification est activée sur le compte d’intégration, il faut donc que le script génère lui-même les codes à partir du secret, ce qui revient à stocker un second facteur à côté du mot de passe. À évaluer selon votre politique de sécurité.
La gestion de la double authentification du compte connecté passe par POST /api/v1/auth/me/2fa/setup (secret, URI otpauth://, qrCodeImage — le QR code en image PNG data:, générée par l’API — et 10 codes de secours), POST /api/v1/auth/me/2fa/confirm (premier code) et DELETE /api/v1/auth/me/2fa (avec le mot de passe dans le corps).
Accepter une invitation
GET /api/v1/auth/validate-invitation?token=… décrit l’invitation (email, organizationName, roles, isExistingUser, et passwordMinLength, la longueur minimale de la politique de mot de passe de l’organisation invitante). POST /api/v1/auth/accept-invitation l’accepte :
{ "token": "…", "firstName": "Alex", "lastName": "Martin", "password": "••••••••••••" }
Prénom, nom et mot de passe ne servent qu’à créer un nouveau compte. Réponse 200 :
{
"accessToken": "eyJhbGciOiJSUzI1NiIs…",
"expiresIn": 900,
"session": { "id": "…", "expiresAt": "…", "idleExpiresAt": "…", "maxDurationHours": 8, "inactivityTimeoutMinutes": 120 },
"requiresSignIn": false,
"email": "[email protected]",
"organizationName": "Exemple SA"
}
-
Nouveau compte :
requiresSignInvautfalse, la session est ouverte et la réponse dépose le cookie de renouvellement, comme une connexion. -
Compte existant de l’organisation invitante (même e-mail) : les rôles de l’invitation sont ajoutés et l’invitation est consommée, mais aucune session n’est ouverte :
requiresSignInvauttrue,accessTokenest vide,expiresInvaut0,sessionestnullet aucun cookie n’est déposé. Il faut ensuite se connecter avecPOST /api/v1/auth/login(mot de passe, puis double authentification si elle est activée). Le lien ne prouve que l’accès à la boîte mail : il ne remplace ni le mot de passe ni le second facteur. -
Compte d’une autre organisation : refusé, tant qu’un compte ne peut pas passer d’une organisation à l’autre.
validate-invitationcommeaccept-invitationrépondent409avec le codeaccount_in_other_organization, sans rien écrire ; l’invitation reste ouverte, à annuler par l’organisation invitante :{ "type": "invitation-error", "code": "account_in_other_organization", "title": "Cette adresse e-mail est déjà associée à un compte d’une autre organisation. …", "status": 409 }
Un compte désactivé reçoit 400 (« Invitation introuvable ou invalide. ») et l’invitation reste ouverte. L’acceptation partage la limite de débit de l’inscription et vérifie la liste d’adresses IP autorisées de l’organisation invitante.
L’envoi d’une invitation (POST /api/v1/invitations) à une adresse dont le compte appartient à une autre organisation est refusé de la même façon : 409, { "code": "account_in_other_organization", "message": "…" }, et aucun e-mail n’est envoyé.
Le jeton d’accès
C’est un JWT signé en RS256, émis par conformitly pour l’audience conformitly-api. Il contient notamment :
| Revendication | Contenu |
|---|---|
sub | identifiant de l’utilisateur |
email | e-mail du compte |
org_id | identifiant de l’organisation |
roles | chaîne JSON contenant un tableau [{"entity_id":"…","role":"admin"}] : les rôles du compte sur les entités de l’organisation org_id, au moment de l’émission ; rôles possibles : admin, rssi, contributor, auditor |
sid | identifiant de la session à laquelle le jeton appartient (le même que session.id et que dans GET /api/v1/auth/me/sessions) |
jti, exp, iss, aud | identifiant du jeton, expiration, émetteur, audience |
Pour une intégration, le jeton est opaque : il suffit de le renvoyer. La clé publique de signature n’est pas publiée (pas de point d’accès JWKS).
Deux points à connaître :
- Les droits ne sont pas figés dans le jeton. L’API vérifie les rôles en base à chaque requête, pour l’organisation
org_id(cache d’au plus 5 secondes) : un rôle attribué, retiré, ou une nouvelle entité, prend effet sans nouveau jeton. La revendicationrolesetGET /api/v1/auth/me(qui répond pour l’organisation du jeton) sont des instantanés pour l’affichage ; le renouvellement les met à jour. - Un compte désactivé perd l’accès même avec un jeton encore valide : l’état du compte est revérifié à chaque requête, avec un cache d’au plus 30 secondes. La désactivation ferme aussi ses sessions (
account_deactivated) : la réactivation du compte ne les rouvre pas, il faut se reconnecter. - Le jeton ne vaut que tant que sa session est ouverte. À chaque requête, l’API vérifie la session que désigne
sid: un jeton est refusé (401) dès que sa session est fermée (déconnexion, révocation, changement ou réinitialisation du mot de passe, réutilisation d’un jeton de renouvellement) ou qu’elle a dépassé la durée maximale ou le délai d’inactivité de l’organisation, sans attendre son expiration. Le refus est immédiat sur l’instance de l’API qui a fermé la session, et intervient en au plus 30 secondes sur les autres (chaque instance garde en mémoire, pendant 30 secondes au plus, l’état des sessions qu’elle a lues). Un refus pour inactivité ou durée maximale est enregistré dès qu’il est prononcé : assouplir ensuite la politique ne rouvre pas la session. Un jeton sanssid(émis avant cette vérification) est refusé : renouvelez-le.
Sessions
Une connexion ouvre une session, que les renouvellements prolongent jusqu’à ce qu’elle se ferme. L’organisation du compte fixe deux limites (Paramètres > Sécurité ; par défaut 8 heures et 2 heures), qui s’appliquent à tous ses utilisateurs, administrateurs de la plateforme compris :
- Durée maximale : la session se termine à cette durée après la connexion, quelle que soit l’activité (
session.expiresAt). - Délai d’inactivité : la session se termine si aucune activité n’est enregistrée pendant ce délai (
session.idleExpiresAt, jamais aprèsexpiresAt).
Ces limites sont celles en vigueur au moment de la vérification : une politique durcie par l’organisation s’applique aussi aux sessions déjà ouvertes (dans les 30 secondes).
Compte comme activité toute requête authentifiée sur l’API, sauf le renouvellement (POST /auth/refresh) et les connexions au temps réel. Elle est enregistrée au plus une fois par minute. Pour signaler une activité sans autre requête (un utilisateur qui lit ou saisit sans enregistrer), l’application web appelle POST /api/v1/auth/me/activity, au plus une fois par minute ; la réponse 200 donne les échéances de la session après prise en compte :
{ "id": "6f1c…", "expiresAt": "2026-10-15T16:00:00Z", "idleExpiresAt": "2026-10-15T12:31:00Z", "maxDurationHours": 8, "inactivityTimeoutMinutes": 120 }
Pour une intégration non interactive : une session ne dure pas plus que la durée maximale de l’organisation ; au-delà, il faut se reconnecter avec le mot de passe. Entre deux traitements plus espacés que le délai d’inactivité, la session est fermée : reconnectez-vous en début de traitement plutôt que de compter sur le cookie.
Renouveler le jeton
À la connexion, l’API dépose un cookie conformitly_refresh_token :
| Attribut | Valeur |
|---|---|
HttpOnly, Secure | oui |
SameSite | Strict |
Path | /api/v1/auth |
| Durée | jusqu’à la durée maximale de la session (30 jours au plus) |
POST /api/v1/auth/refresh, sans corps, avec ce cookie, renvoie la même réponse que la connexion (accessToken, expiresIn, session) et un nouveau cookie. Le jeton de renouvellement est à usage unique : chaque renouvellement remplace l’ancien, qu’il faut jeter. Le renouvellement prolonge le jeton d’accès, pas la session : il ne compte pas comme une activité, et le nouveau jeton expire au plus tard à la durée maximale de la session. Renouvelez avant l’expiration (expiresIn) ou dès qu’une requête répond 401, puis renvoyez-la une fois.
Hors navigateur, conservez la valeur du cookie reçue dans Set-Cookie et renvoyez-la dans un en-tête Cookie :
curl -s -X POST https://api.conformitly.fr/api/v1/auth/refresh \
-b cookies.txt -c cookies.txt
Refus possibles (401, et le cookie est effacé sauf pour refresh_superseded) : cookie absent (« Refresh token manquant. »), inconnu, compte inactif, session terminée, mot de passe expiré, renouvellement concurrent ou réutilisation d’un jeton déjà remplacé. Les refus qu’une intégration doit distinguer portent un code stable, en plus du message :
code | Signification | Que faire |
|---|---|---|
session_expired | La session est fermée ; reason dit pourquoi : idle (délai d’inactivité), max_duration (durée maximale), token_expired (jeton de renouvellement expiré avant la durée maximale), logout (déconnexion), revoked (révoquée depuis la liste des sessions), password_changed, password_reset, password_expired, account_deactivated (compte désactivé par un administrateur), account_deleted (compte supprimé), token_reuse… | Se reconnecter avec le mot de passe |
password_expired | Le mot de passe a expiré : la session est fermée | Se reconnecter, ce qui mène à l’étape password_change |
refresh_superseded | Le même cookie vient d’être renouvelé par une autre requête du même client (moins de 30 secondes, même adresse IP et même user agent) : rien n’est révoqué, le cookie n’est pas effacé | Renouveler une fois de plus avec le cookie le plus récent ; si la réponse se répète, patienter une à deux secondes avant un dernier essai, sans mettre fin à la session |
{ "type": "session-expired", "code": "session_expired", "reason": "idle", "title": "Votre session a expiré après une période d'inactivité. Reconnectez-vous.", "status": 401 }
{ "type": "authentication-error", "code": "password_expired", "title": "Votre mot de passe a expiré. Reconnectez-vous pour le changer.", "status": 401 }
Un jeton de renouvellement présenté de nouveau plus de 30 secondes après avoir été remplacé, ou depuis une autre adresse IP ou un autre user agent que le renouvellement qui l’a remplacé, même quelques secondes plus tard, est considéré comme volé : l’API révoque la chaîne de jetons qui en descend et ferme la session, dont les jetons d’accès cessent aussitôt d’être acceptés ; il faut se reconnecter avec le mot de passe. Les autres sessions du compte restent ouvertes. Deux processus qui partagent le même cookie doivent donc renouveler l’un après l’autre (l’application web le fait sous un verrou commun à ses onglets) ; un renouvellement simultané du même client reçoit refresh_superseded (et une entrée auth.refresh.superseded dans le journal d’audit) et peut être retenté. L’application web retente aussitôt, puis une dernière fois après 1,5 seconde ; si la réponse persiste, ce renouvellement échoue sans fermer la session, et le suivant réessaie.
Le renouvellement est limité à 60 appels par minute et par adresse IP (limiteur auth-refresh, voir Limites de débit) ; au-delà, 429 sans corps, et le cookie n’est pas touché. Il revérifie la liste d’adresses IP autorisées de l’organisation.
Se déconnecter et gérer les sessions
POST /api/v1/auth/logout(avec le jeton d’accès) ferme la session que désigne le jeton (sid), révoque son jeton de renouvellement et celui du cookie, puis efface le cookie. Réponse204. Les jetons d’accès de la session cessent aussitôt d’être acceptés.GET /api/v1/auth/me/sessionsliste les sessions ouvertes du compte, avec pour chacuneexpiresAt(durée maximale) etidleExpiresAt(fin sans activité) ;isCurrentdésigne celle du jeton de la requête.DELETE /api/v1/auth/me/sessions/{sessionId}en ferme une : ses jetons d’accès sont refusés dès la requête suivante.- Changer de mot de passe (
PATCH /api/v1/auth/me/password) ferme toutes les autres sessions du compte, dont les jetons d’accès sont aussitôt refusés ; seule reste ouverte la session du jeton de la requête. Un mot de passe actuel erroné répond400(« Le mot de passe actuel est incorrect. ») et compte pour le verrouillage (voir Se connecter) ; un compte verrouillé répond400(« Compte temporairement verrouillé suite à trop de tentatives. »), même avec le bon mot de passe. L’opération est limitée à 5 appels par minute et par utilisateur (Limites de débit).
Mot de passe oublié
La réinitialisation se fait en deux appels anonymes, avec un lien envoyé par e-mail entre les deux.
-
POST /api/v1/auth/forgot-password:{ "email": "[email protected]" }La réponse est toujours
204, sans corps et immédiate, que l’adresse corresponde à un compte actif, à un compte désactivé ou à aucun compte : elle ne permet pas de savoir si un compte existe. Seul un compte actif reçoit l’e-mail, quelques secondes plus tard. Une adresse mal formée reçoit400(contrôle de format) et la limite est de 3 demandes par minute et par adresse IP. -
L’e-mail contient un lien vers
https://app.conformitly.fr/reset-password?email=…&token=…, valable 30 minutes et à usage unique. La page envoie ces deux paramètres, tels quels, avec le nouveau mot de passe àPOST /api/v1/auth/reset-password:{ "email": "[email protected]", "token": "…", "newPassword": "••••••••••••" }Réponse
204. La réinitialisation révoque tous les jetons de renouvellement et toutes les sessions du compte, annule toute vérification de second facteur en attente et lève le verrouillage du compte. La double authentification reste activée : la connexion suivante demande toujours le code. Comme à la déconnexion, les jetons d’accès déjà émis sont aussitôt refusés.
Refus de POST /api/v1/auth/reset-password (400) :
-
lien inutilisable, quelle qu’en soit la raison (adresse inconnue, compte désactivé, jeton faux, expiré ou déjà utilisé, ou rendu caduc par une réinitialisation plus récente) : toujours le même message.
{ "type": "password-reset-error", "title": "Ce lien de réinitialisation est invalide ou a expiré. Faites une nouvelle demande.", "status": 400 } -
mot de passe hors règle, y compris la politique de l’organisation du compte (longueur minimale, historique : « Ce mot de passe a déjà été utilisé récemment. ») :
validation-error, ou le format de validation de la requête pour la longueur de 12 caractères. Le lien reste alors utilisable.
La limite est de 5 tentatives par minute et par adresse IP.
Restriction par adresse IP
Une organisation peut restreindre l’accès à une liste d’adresses IP ou de plages (offres Enterprise et On-Premises). Lorsqu’elle est active, la liste s’applique à la connexion, à la double authentification, au renouvellement et à chaque requête authentifiée. Un appel depuis une adresse non autorisée reçoit 403 :
{
"type": "ip-not-allowed",
"title": "Accès refusé : votre adresse IP (203.0.113.7) n'est pas autorisée par votre organisation.",
"status": 403,
"instance": "/api/v1/…"
}
Si vous intégrez depuis un serveur, faites ajouter son adresse de sortie à la liste.
Ce qui n’existe pas
- Il n’y a pas de confirmation d’adresse e-mail, ni à l’inscription (
POST /api/v1/auth/register) ni ailleurs. - Parmi les paramètres de sécurité de l’organisation, la politique de mot de passe et la politique de session sont appliquées. La double authentification obligatoire est enregistrée mais n’est pas appliquée aujourd’hui : elle reste un choix de chaque utilisateur.
Mise à jour : 10 octobre 2026. Signaler une inexactitude