Aller au contenu

Endpoints d'authentification et jetons

Les routes montées par le bloc auth, et les jetons qu’elles renvoient.

Tous les endpoints d’authentification sont montés sur /api/auth/ :

Méthode Chemin Description
POST /api/auth/register Créer un nouveau compte
POST /api/auth/login Connexion avec e-mail/mot de passe
POST /api/auth/refresh Rafraîchir le jeton d’accès
POST /api/auth/<provider> Connexion OAuth (ex. : /api/auth/google, /api/auth/linkedin)
POST /api/auth/link/<provider> Lier un fournisseur OAuth au compte authentifié. Sur un invité, c’est une inscription : elle exige allowRegistration, prend l’adresse du fournisseur quand celui-ci s’en porte garant, et répond avec une session pour le compte que l’invité est devenu
POST /api/auth/logout Révoquer le jeton de rafraîchissement
POST /api/auth/forgot-password Envoyer un e-mail de réinitialisation de mot de passe
POST /api/auth/reset-password Réinitialiser le mot de passe avec un jeton
POST /api/auth/find-user Résoudre une adresse e-mail en un profil public minimal (activation facultative — AUTH_ALLOW_USER_LOOKUP)
POST /api/auth/change-password Modifier le mot de passe de l’appelant (authentifié). Met fin à toute autre session et répond avec une nouvelle pour l’appelant
GET /api/auth/me Le profil de l’appelant
PATCH /api/auth/me Mettre à jour le profil de l’appelant
POST /api/auth/change-email { newEmail } : déplacer le propre compte de l’appelant vers une autre adresse. Envoie un lien à la nouvelle adresse et un avis à l’ancienne ; rien ne change avant que le lien ne soit suivi. Exige aal2 sur un compte avec un second facteur. 409 EMAIL_EXISTS ou UNDELIVERABLE_ADDRESS, 400 EMAIL_UNCHANGED, 403 ANONYMOUS_USER pour un invité — voir Changer d’adresse e-mail
POST /api/auth/confirm-email-change { token } provenant du lien. Aucune session requise. Déplace le compte vers la nouvelle adresse, vérifiée ; 400 INVALID_TOKEN pour un lien consommé, remplacé ou expiré, 409 EMAIL_EXISTS quand l’adresse a été prise pendant l’attente
GET /api/auth/config Ce que ce backend propose à un écran de connexion — needsSetup, registrationEnabled, passwordReset, emailVerification, magicLink, anonymousLogin, adminPasswordReset, enabledProviders, et storage, qui indique si le backend sert le stockage de fichiers. Non authentifié et calculé à partir des mêmes prédicats que ceux appliqués par les routes, de sorte que ce que l’écran affiche ne peut pas dévier de ce qu’il peut faire
POST /api/auth/send-verification Envoyer à l’appelant un lien de vérification d’e-mail
GET /api/auth/verify-email Valider un lien de vérification (l’URL présente dans cet e-mail). Conserve ce qu’une session active du compte prouve et retire ce que personne n’a prouvé — voir Vérification de l’e-mail
POST /api/auth/verify-email La même chose avec { token, password?, removeUnproven? } : le mot de passe le conserve et connecte ; sans aucune des deux preuves, un compte qui en détient une répond 409 PROOF_REQUIRED
POST /api/auth/magic-link Envoyer par e-mail un lien de connexion à usage unique. 503 EMAIL_NOT_CONFIGURED sans SMTP
POST /api/auth/magic-link/verify Échanger un jeton de magic link contre une session
POST /api/auth/otp Envoyer par e-mail un code de connexion à six chiffres. Répond de la même manière que l’adresse possède un compte ou non
POST /api/auth/otp/verify Échanger { email, code } contre une session
POST /api/auth/anonymous Créer une session anonyme (activation facultative — ALLOW_ANONYMOUS)
POST /api/auth/anonymous/link Associer des identifiants réels au compte anonyme déjà connecté. Nécessite à la fois allowAnonymous et l’inscription ouverte : 403 ANONYMOUS_AUTH_DISABLED dès que la connexion anonyme est désactivée, invités déjà connectés compris, et 403 REGISTRATION_DISABLED tant que l’inscription est fermée
GET /api/auth/sessions Lister les sessions actives de l’appelant, une par connexion. La propre session de l’appelant est marquée isCurrentSession
DELETE /api/auth/sessions Révoquer toutes les sessions, y compris celle-ci — déconnexion à distance sur chaque appareil
DELETE /api/auth/sessions/:id Révoquer une session : son jeton de rafraîchissement, et le jeton d’accès que détient cet appareil
GET /api/auth/scopes Toutes les portées que ce backend connaît, et celles que détient l’appelant
GET /api/auth/keys Les propres clés API personnelles de l’appelant
POST /api/auth/keys Créer une clé personnelle. 403 PERSONAL_KEYS_DISABLED sauf si la collection des utilisateurs définit auth.personalKeys
DELETE /api/auth/keys/:id Révoquer l’une des propres clés de l’appelant
GET /.well-known/jwks.json Le JWKS public — monté à la racine, et non sous basePath, car c’est là qu’un vérificateur regarde. Présent lorsque la signature asymétrique est configurée
POST /api/auth/mfa/enroll Démarrer l’enrôlement TOTP (renvoie le secret et les codes de récupération)
POST /api/auth/mfa/verify Confirmer un enrôlement avec un code provenant de l’application d’authentification
GET /api/auth/mfa/factors Lister les facteurs enregistrés de l’appelant
POST /api/auth/mfa/challenge Initier un challenge pour un facteur vérifié
POST /api/auth/mfa/challenge/verify Répondre à un challenge — c’est ce qui émet la session
DELETE /api/auth/mfa/unenroll Supprimer un facteur (nécessite une session aal2 dès que le compte a un facteur vérifié)
POST /api/auth/mfa/recovery-codes Remplacer les codes de récupération de l’appelant par dix nouveaux (nécessite une session aal2)

La gestion administrative des utilisateurs et des rôles est une surface distincte, montée sur /api/admin/ plutôt que sur /api/auth/. La lecture exige la portée users:read et la modification users:write, à l’exception de la lecture restreinte du sélecteur d’utilisateurs ci-dessous. Un administrateur et la clé de service détiennent les deux ; un rôle qui les déclare aussi. Personne ne peut modifier un compte qui détient plus que lui. Voir Rôles et portées.

Méthode Chemin Description
GET /api/admin/users Lister les utilisateurs (paginé). ?search= correspond à une partie, sans tenir compte de la casse, de l’e-mail ou du nom affiché. Sur Postgres, il correspond aussi à n’importe quel rôle que détient l’utilisateur, et à l’uid ; sur MongoDB, il ne correspond toujours qu’à l’e-mail et au nom affiché
GET /api/admin/users/pick La lecture du sélecteur d’utilisateurs : uid, displayName, email et photoURL par utilisateur, rien d’autre. Accepte ?search=, ?limit= (25 par défaut) et ?offset=, ou ?ids=a,b pour les utilisateurs qu’une valeur stockée désigne. Répond à users:read, et aussi à quiconque peut écrire dans une collection ayant un champ userSelect — un éditeur peut ainsi en renseigner un. Une règle écrite en SQL brut (using / withCheck que le modèle de politiques ne sait pas lire) ne peut pas être jugée à l’avance : elle compte donc comme un refus ici, et sur une telle collection seuls les détenteurs de users:read peuvent choisir. Tout autre reçoit 403 SCOPE_MISSING
POST /api/admin/users Créer un utilisateur
GET /api/admin/users/:uid Obtenir un utilisateur
PUT /api/admin/users/:uid Mettre à jour un utilisateur. { disabled: true } désactive le compte sans le supprimer : toute connexion et tout rafraîchissement sont refusés (ACCOUNT_DISABLED), ses sessions prennent fin et chaque jeton qu’il détient est refusé ; false le réactive
DELETE /api/admin/users/:uid Supprimer un utilisateur. Ses sessions prennent fin, et chaque jeton d’accès qu’il détient est refusé à partir de cette requête
POST /api/admin/users/:uid/reset-password Réinitialiser le mot de passe d’un utilisateur sans son mot de passe actuel. rebase auth reset-password l’appelle, et n’écrit directement dans la base de données que lorsque le backend est inatteignable ; dans les deux cas, les sessions du compte prennent fin
DELETE /api/admin/users/:uid/mfa Supprimer les seconds facteurs et les codes de récupération d’un utilisateur, et mettre fin à ses sessions — pour quelqu’un qui a perdu les deux
GET /api/admin/roles admin et les rôles que déclare la collection des utilisateurs, avec leurs portées
POST /api/admin/bootstrap Permettre au premier utilisateur inscrit de revendiquer le rôle admin tant qu’aucun n’existe. Refusé en production — voir Amorçage du premier utilisateur

Tous les endpoints de l’API de données nécessitent un en-tête Authorization: Bearer <token> valide lorsque requireAuth: true (valeur par défaut).

Un nouveau mot de passe doit comporter au moins 8 caractères, avec une majuscule, une minuscule et un chiffre. L’inscription, la réinitialisation et le changement refusent un mot de passe qui ne respecte pas ces règles avec 400 WEAK_PASSWORD, en listant chaque règle enfreinte, et il en va de même pour un administrateur qui choisit un mot de passe : POST /api/admin/users, une création dans la collection des utilisateurs via l’API de données (POST /api/data/users, son /bulk, une écriture par socket ou MCP), PUT /api/admin/users/:uid et POST /api/admin/users/:uid/reset-password. Sur un /bulk, le message commence par Row <n>: (compté à partir de 0), comme les autres erreurs de ligne du bulk.

Pour créer des comptes dont les mots de passe ont été fixés par un autre système, comme lors d’un import, envoyez skipPasswordPolicy: true dans le corps de la création (sur chaque ligne d’un /bulk). Le serveur ne le respecte que pour un administrateur ou la clé de service ; de la part de tout autre, il ne dispense de rien, et le refus le dit. Il n’est jamais stocké. L’import du panneau d’administration l’envoie pour chaque ligne qui porte un password. La connexion ne vérifie jamais la politique, de sorte qu’un ancien mot de passe continue de fonctionner. Pour définir vos propres règles, passez auth.hooks.validatePasswordStrength depuis un backend éjecté. Le premier administrateur créé à partir de REBASE_ADMIN_PASSWORD est soumis à une autre règle : au moins 12 caractères, et rien d’autre.

Chaque endpoint qui émet une session répond avec la même enveloppe — register, login, chaque fournisseur OAuth, magic-link/verify, otp/verify, anonymous, anonymous/link et mfa/challenge/verify :

{
"user": {
"uid": "8f1c2a6e-…",
"email": "jane@example.com",
"displayName": "Jane Doe",
"photoURL": null,
"providerId": "password",
"isAnonymous": false,
"emailVerified": true,
"roles": ["editor"],
"metadata": {}
},
"tokens": {
"accessToken": "eyJhbGciOi…",
"refreshToken": "9b2e…",
"accessTokenExpiresAt": 1700000000000
}
}

Renvoyez le jeton d’accès sous la forme Authorization: Bearer <accessToken>. accessTokenExpiresAt correspond aux millisecondes de l’époque Unix (epoch).

POST /api/auth/refresh répond avec la même enveloppe, sauf que user est totalement omis lorsque le compte ne peut pas être relu, considérez-le donc comme facultatif ici.

providerId indique comment la session s’est connectée : password, anonymous, magic-link, otp, mfa (une connexion achevée avec un second facteur), ou l’id du fournisseur, tel que google. Il est stocké avec la session à la connexion, si bien que refresh et GET /api/auth/me donnent la même réponse pendant toute la durée de vie de la session. Une session connectée avant la 0.24 lit password.

Une personne connectée déplace son propre compte vers une autre adresse en deux étapes :

  1. POST /api/auth/change-email { newEmail } enregistre le changement et envoie un lien, <frontend>/confirm-email-change?token=…, à la nouvelle adresse, et un avis sans lien à l’adresse actuelle. Le lien vit 24 heures, et une nouvelle demande remplace la précédente. GET /api/auth/me signale l’adresse en attente sous pendingEmail.
  2. POST /api/auth/confirm-email-change { token } déplace le compte : la nouvelle adresse devient son adresse, vérifiée. Chaque identité OAuth dont le fournisseur s’était porté garant de l’ancienne adresse est détachée (removedProviders les nomme), car quiconque contrôle l’ancienne adresse pourrait sinon encore se connecter par ce biais, et tout lien de réinitialisation envoyé à l’ancienne adresse cesse de fonctionner. Les sessions sont conservées.

La nouvelle adresse n’est pas réservée pendant que le lien attend : la réserver permettrait à un compte d’empêcher un inconnu de s’inscrire avec sa propre adresse. Si un autre compte détient l’adresse au moment où le lien est suivi, le lien répond 409 EMAIL_EXISTS et rien ne se déplace ; entre deux comptes demandant la même adresse, le premier à suivre son lien l’obtient. Le hook beforeEmailChange peut refuser une adresse, comme beforeUserCreate le fait à l’inscription.

Dans le CMS, l’adresse se change depuis Paramètres du compte → Profil, et le lien ouvre l’écran propre au CMS, connecté ou non. Un autre frontend sert une page à /confirm-email-change qui appelle la route avec le jeton du lien.

Lorsque cookieAuth est activé, le jeton de rafraîchissement transite dans un cookie httpOnly et tokens.refreshToken est une chaîne vide dans le corps de la réponse. Le jeton d’accès n’est pas affecté.

Un deuxième facteur conditionne la connexion, et pas seulement des opérations individuelles. Dès qu’un compte dispose d’un facteur TOTP vérifié, aucune route ne lui délivre de session tant qu’un code n’a pas été fourni — la connexion par mot de passe, chaque fournisseur OAuth, le lien magique et anonymous-link refusent tous avec une erreur 401 MFA_REQUIRED :

{
"error": {
"code": "MFA_REQUIRED",
"message": "Multi-factor authentication is required to complete sign-in.",
"details": {
"mfaToken": "<short-lived pre-auth token>",
"factors": [{ "id": "…", "factorType": "totp", "friendlyName": "Phone" }]
}
}
}

mfaToken n’est pas une session : il est restreint à cet usage spécifique, expire au bout de cinq minutes et est rejeté par toutes les routes authentifiées. Envoyez-le en tant que jeton porteur (bearer token) à POST /api/auth/mfa/challenge (avec un factorId), puis à POST /api/auth/mfa/challenge/verify (avec le challengeId et le code à six chiffres, ou un code de récupération). C’est ce dernier appel qui génère les jetons d’accès et de rafraîchissement, au niveau aal2 ; ce niveau est stocké dans la session et conservé lors d’un POST /api/auth/refresh.

L’enrôlement est également restreint. Le premier facteur d’un compte peut être enrôlé à partir d’une session ordinaire, mais dès lors qu’un facteur est vérifié, enroll, verify et unenroll requièrent tous une session aal2 — sinon, un mot de passe volé permettrait d’enrôler un nouveau facteur, d’élever les privilèges avec celui-ci et de supprimer le véritable facteur.

La vérification est limitée sur deux axes : un challenge expire après cinq tentatives infructueuses, chaque compte est limité à dix tentatives de vérification par tranche de 15 minutes (comptabilisées par utilisateur, donc changer d’adresse IP n’aide pas), et un code accepté est enregistré pour ce facteur afin qu’il ne puisse pas être rejoué durant le reste de sa fenêtre de validité de ±1 intervalle (step).

Dans la table des utilisateurs, Réinitialiser la vérification en deux étapes (DELETE /api/admin/users/:uid/mfa) et Désactiver ou activer le compte (PUT /api/admin/users/:uid { disabled }) sont proposées à quiconque détient users:write, comme le sont les routes ; le bouton n’est jamais proposé sur votre propre compte, et un compte qui vous dépasse est refusé avec la raison donnée par le serveur.

Définissez MFA_ENCRYPTION_KEY (au moins 32 caractères aléatoires) pour chiffrer les secrets TOTP stockés. Sans cela, le serveur se rabat sur JWT_SECRET et émet un avertissement. Définissez-la avant que quiconque ne s’enrôle : les secrets stockés ne comportent pas d’identifiant de clé, donc modifier la clé ultérieurement rendra les facteurs existants indéchiffrables et empêchera leurs propriétaires de valider un challenge.

Les flux d’invitation doivent convertir une adresse e-mail en identifiant utilisateur, mais la collection users est protégée par RLS vis-à-vis du client. Au lieu de développer manuellement une fonction serveur d’administration, activez la recherche intégrée :

await initializeRebaseBackend({
auth: {
// ...
allowUserLookup: true, // enables POST /api/auth/find-user
},
});

Ensuite, depuis le client :

const profile = await client.auth.findUserByEmail("teammate@example.com");
// → { uid, displayName, photoURL } | null (never email/roles/metadata)
if (profile) {
await client.data.team_members.create({ team_id, user_id: profile.uid });
}

Cet endpoint est réservé aux utilisateurs authentifiés et ne renvoie que uid, displayName et photoURL — jamais l’e-mail, les rôles ou les métadonnées de l’utilisateur recherché. Il est désactivé par défaut car il permet à tout utilisateur connecté de sonder quelles adresses e-mail possèdent un compte ; ne l’activez que si votre expérience d’invitation le nécessite.

Contexte de base de données Row-Level Security (RLS)

Section intitulée « Contexte de base de données Row-Level Security (RLS) »

Rebase transmet directement l’authentification de la requête à la sécurité au niveau des lignes (Row-Level Security ou RLS) de PostgreSQL. Chaque requête de base de données exécutée via un pilote lié à l’utilisateur s’exécute au sein d’une transaction de base de données (db.transaction()) qui configure des paramètres locaux à la transaction :

  • app.user_id — L’identifiant unique (uid) de l’utilisateur authentifié. Vaut 'anon' par défaut pour les requêtes non authentifiées.
  • app.user_roles — Une chaîne de caractères séparée par des virgules listant les rôles attribués à l’utilisateur.
  • app.jwt — Une chaîne JSON contenant l’intégralité du payload des revendications (claims) du JWT ({"sub": "<uid>", "roles": [...]}).

Ces paramètres sont configurés localement pour la durée de la transaction à l’aide de la fonction set_config de Postgres :

SELECT
set_config('app.user_id', $1, true),
set_config('app.user_roles', $2, true),
set_config('app.jwt', $3, true);

Pour simplifier l’écriture des politiques Row-Level Security, Rebase crée des fonctions d’aide sous le schéma auth lors de l’initialisation (bootstrapping) de la base de données :

  • rebase.uid() — Renvoie l’identifiant de l’utilisateur authentifié sous forme de text, ou NULL s’il n’est pas défini :
    CREATE OR REPLACE FUNCTION rebase.uid() RETURNS text AS $$
    SELECT NULLIF(current_setting('app.user_id', true), '');
    $$ LANGUAGE sql STABLE;
  • rebase.roles() — Renvoie la chaîne des rôles séparés par des virgules :
    CREATE OR REPLACE FUNCTION rebase.roles() RETURNS text AS $$
    SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
    $$ LANGUAGE sql STABLE;
  • rebase.jwt() — Renvoie le payload JWT complet sous la forme d’un objet jsonb :
    CREATE OR REPLACE FUNCTION rebase.jwt() RETURNS jsonb AS $$
    SELECT COALESCE(NULLIF(current_setting('app.jwt', true), ''), '{}')::jsonb;
    $$ LANGUAGE sql STABLE;

Vous pouvez utiliser ces fonctions d’aide directement dans vos règles de sécurité personnalisées ou vos migrations de base de données :

CREATE POLICY owner_access ON posts
FOR ALL
TO public
USING (author_id = rebase.uid() OR string_to_array(rebase.roles(), ',') && ARRAY['admin']);

Par défaut, les jetons d’accès sont signés avec jwtSecret (HS256). Cela fonctionne, mais cela signifie que tout service devant vérifier un jeton doit détenir la clé qui permet d’en créer un — ainsi, une passerelle ou un edge worker vérifiant une session peut également en falsifier une — et modifier le secret déconnecte tous les utilisateurs d’un coup.

Configurez une clé de signature et Rebase signera les jetons d’accès de manière asymétrique, publiant la partie publique sur /.well-known/jwks.json pour que n’importe qui puisse effectuer la vérification :

auth: {
jwtSecret: process.env.JWT_SECRET,
signingKeys: [
{ kid: "2026-08", privateKey: process.env.JWT_PRIVATE_KEY! }
]
}

Ou depuis l’environnement, pour une clé unique :

JWT_PRIVATE_KEY="$(cat jwt-key.pem)"
JWT_KEY_ID=2026-08

Générez une clé avec :

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out jwt-key.pem

Les clés RSA fonctionnent également et signent en RS256 ; les clés EC P-256 signent en ES256. Seule la clé privée est configurée — la partie publique en est dérivée, ce qui empêche toute discordance de paire. jwtSecret reste requis dans tous les cas : il signe toujours les jetons à usage restreint (liens de téléchargement, MFA en attente, réinitialisation de mot de passe) que seul ce serveur lit.

Placez la nouvelle clé en premier et conservez l’ancienne dans la liste. Les nouveaux jetons sont signés par la nouvelle clé ; les jetons déjà en circulation continuent d’être vérifiés avec l’ancienne jusqu’à ce qu’ils expirent, afin que personne ne soit déconnecté.

signingKeys: [
{ kid: "2026-09", privateKey: process.env.JWT_PRIVATE_KEY_NEW! },
{ kid: "2026-08", privateKey: process.env.JWT_PRIVATE_KEY_OLD! }
]

Une fois la durée de vie la plus longue d’un jeton d’accès écoulée, supprimez l’ancienne entrée. Utilisez activeKid si vous souhaitez publier une clé avant de commencer à signer avec celle-ci.

Les jetons portent le kid de la clé de signature dans leur en-tête, ce qui permet à un vérificateur de choisir la bonne clé dans le JWKS et de savoir quand récupérer à nouveau le jeu de clés après une rotation. Toute bibliothèque standard prend cela en charge — par exemple avec jose :

import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL("https://api.example.com/.well-known/jwks.json"));
const { payload } = await jwtVerify(token, jwks);

Pour les communications de serveur à serveur (ex. : tâches cron, services externes), configurez une clé de service statique :

auth: {
serviceKey: process.env.REBASE_SERVICE_KEY,
// ...
}

Les clients s’authentifient avec l’en-tête Authorization: Bearer <service-key>.

Si REBASE_SERVICE_KEY n’est pas fournie dans votre configuration, Rebase génère automatiquement une clé interne aléatoire à chaque démarrage.

Cette clé n’est jamais consignée dans les logs et ne quitte jamais le processus. Elle est utilisée par le singleton rebase pour s’authentifier auprès des API du plan de contrôle internes au serveur (auth, stockage, etc.). Cela garantit que les tâches administratives (comme l’envoi d’un e-mail de bienvenue ou la génération d’une URL de stockage) fonctionnent toujours telles quelles en développement et en production, sans nécessiter de gestion manuelle des clés.

Protection contre les attaques temporelles et exigences pour les clés

Section intitulée « Protection contre les attaques temporelles et exigences pour les clés »

Pour prévenir les attaques temporelles (timing attacks), Rebase valide la clé de service configurée par l’utilisateur ainsi que la clé interne en utilisant une comparaison de chaînes à temps constant (safeCompare). La clé de service configurée par l’utilisateur doit comporter au moins 32 caractères ; si une clé de moins de 32 caractères est configurée, Rebase lèvera une erreur de configuration au démarrage et refusera de se lancer (fail-closed).