# Premiers pas avec l’authentification gérée de Relyt One

Un guide pas à pas pour créer une application Next.js multi-utilisateur sur Relyt One. Activez l’authentification gérée sur une branche, connectez vos utilisateurs, vérifiez leurs JWT avec @relyt/auth et laissez la sécurité au niveau des lignes de Postgres séparer les lignes de chaque utilisateur.

**Dans ce guide, nous construisons une petite API de notes multi-utilisateur sur Relyt One. Les utilisateurs s’inscrivent et se connectent sur l’URL d’authentification gérée de la branche, l’application vérifie leurs jetons avec `@relyt/auth`, et la sécurité au niveau des lignes (row-level security) de Postgres garantit que chaque utilisateur ne lit et n’écrit jamais que ses propres notes. Au passage, nous abordons les points qui posent habituellement problème : origines de confiance, émetteurs qui ne correspondent pas, rôles `BYPASSRLS` et connexions poolées.**

Il vous faut un projet Relyt One, Node.js 20 ou version ultérieure, `psql` et une trentaine de minutes. Tout ce qui suit fonctionne aussi avec l’[exemple TODO](https://github.com/relytcloud/relyt-auth-examples/tree/main/todo) open source, qui applique le même modèle avec davantage de fonctionnalités (connexion via un réseau social, équipes et rôles, et une suite de tests). Si vous voulez comprendre pourquoi l’authentification gérée fonctionne ainsi, lisez d’abord [L’authentification gérée de Relyt One, désormais en préversion](/blog/fr/managed-auth-preview).

> **Remarque**
>
> L’authentification gérée est en préversion. Les étapes correspondent à la console actuelle ; les noms et les limites peuvent encore changer.

## Ce que nous construisons

![Le navigateur se connecte sur l’URL d’authentification et obtient un JWT, l’API le vérifie avec JWKS et définit app.user_id, et RLS filtre la table notes](/blog/assets/d2afa7771b84d007.svg)

- Une table `notes` avec un propriétaire par ligne, protégée par la sécurité au niveau des lignes.
- Une route Next.js `/api/notes` qui liste et crée les notes de l’appelant. Elle n’accepte que `Authorization: Bearer <jwt>`.
- Un utilitaire côté navigateur qui connecte un utilisateur sur l’URL d’authentification et appelle l’API avec un JWT récent.

Deux règles tiennent toute la conception, et chaque étape ci-dessous en découle :

1. **L’identifiant de l’utilisateur ne provient que d’un JWT vérifié.** Jamais d’un en-tête, d’une chaîne de requête ou du corps de la requête.
2. **La base de données le vérifie aussi.** L’API filtre par utilisateur, et RLS filtre une seconde fois : un `WHERE` oublié ne peut donc pas laisser fuiter les lignes de quelqu’un d’autre.

## Étape 1 : activer l’authentification gérée sur une branche

Dans la console, ouvrez votre projet et choisissez la branche que votre application utilisera. Pendant le développement, une branche enfant de `main` est un bon choix : ses paramètres d’authentification, ses sessions et ses clés de signature lui sont propres, si bien que vos expériences ne touchent jamais les connexions de production. (Si vous créez la branche enfant après avoir activé l’authentification sur `main`, elle démarre avec une copie des origines de confiance et des utilisateurs finaux de `main` : considérez donc qu’elle contient de vraies données d’utilisateurs.)

Allez dans l’onglet **Auth** (Authentification) et choisissez **Enable auth** (Activer l’authentification). La première configuration réveille l’instance de calcul de la branche et crée les tables d’authentification ; cela prend environ une minute, alors gardez la page ouverte. Seuls les propriétaires et administrateurs de l’organisation peuvent effectuer cette opération.

Lorsque le statut passe à **Ready** (Prêt), la vue d’ensemble affiche deux URL :

- **Auth URL** (URL d’authentification) : l’endroit où vos utilisateurs se connectent, et la valeur que votre backend attend comme émetteur des jetons.
- **JWKS URL** : les clés publiques que votre backend utilise pour vérifier la signature des jetons.

Copiez les deux. Les utilisateurs, les sessions et les organisations sont désormais stockés dans le schéma `relyt_auth` de la base de données de cette branche.

## Étape 2 : activer une méthode de connexion

Dans **Sign-in methods** (Méthodes de connexion), vérifiez que **Email + password** (E-mail et mot de passe) est activé (c’est le cas par défaut) et enregistrez. C’est tout ce dont ce guide a besoin.

GitHub et Google fonctionnent de la même façon une fois que vous avez une application OAuth : copiez l’URL de rappel affichée par la console (elle pointe vers l’URL d’authentification, par exemple `<URL d’authentification>/api/auth/callback/github`) dans les paramètres du fournisseur, puis collez le Client ID et le Client Secret dans la console et choisissez **Save & enable** (Enregistrer et activer). Le secret est en écriture seule. Une application OAuth GitHub ne peut enregistrer qu’une seule URL de rappel : utilisez une application OAuth par branche.

## Étape 3 : faire confiance à l’origine de votre application

L’URL d’authentification est une origine différente de celle de votre application : le navigateur envoie donc une requête CORS chaque fois que votre application connecte quelqu’un. Le service d’authentification ne répond qu’aux origines que vous listez.

Dans **Redirects & origins** (Redirections et origines), ajoutez l’origine exacte de votre application à **Trusted origins** (Origines de confiance) :

- `http://localhost:3000` pendant le développement ;
- `https://app.example.com` (ou l’adresse où vous déployez) par la suite.

Pas de chemin, pas de barre oblique finale, pas de caractères génériques. Si vous ajoutez plus tard la connexion via un réseau social, enregistrez aussi la page qui termine la connexion, par exemple `http://localhost:3000/auth/callback`, dans **Callback URLs** (URL de rappel). Les URL de rappel doivent correspondre à l’identique, et l’origine de chacune doit aussi être une origine de confiance.

> **Astuce**
>
> Si la connexion dans le navigateur échoue avec une erreur réseau générique, la cause se trouve presque toujours dans cette étape.

## Étape 4 : créer la table, la politique et un rôle applicatif

Connectez-vous à la branche en tant que propriétaire de la base de données (`relytdb_owner` sur la base `relytdb`, avec le mot de passe défini lors de la création du projet) et créez la table :

```sql
CREATE TABLE notes (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id    text NOT NULL,
  body       text NOT NULL CHECK (length(body) BETWEEN 1 AND 2000),
  created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX notes_user_id_idx ON notes (user_id, created_at DESC);
```

`user_id` contient le claim `sub` du JWT de l’utilisateur, qui est un identifiant textuel.

Activez maintenant la sécurité au niveau des lignes et ajoutez une politique qui compare le propriétaire de chaque ligne à un paramètre que l’application définira pour chaque transaction :

```sql
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE notes FORCE ROW LEVEL SECURITY;

CREATE POLICY notes_owner ON notes FOR ALL
  USING (user_id = current_setting('app.user_id', true))
  WITH CHECK (user_id = current_setting('app.user_id', true));
```

Le rôle de chaque partie :

- `ENABLE` active RLS ; `FORCE` l’applique aussi au propriétaire de la table, sauf si celui-ci a `BYPASSRLS`. Sur Relyt One, c’est le cas de `relytdb_owner`, qui voit donc toujours toutes les lignes (point suivant).
- `USING` filtre les lignes qu’une requête peut voir, mettre à jour ou supprimer.
- `WITH CHECK` rejette les insertions et mises à jour qui créeraient une ligne appartenant à quelqu’un d’autre.
- `current_setting('app.user_id', true)` renvoie le paramètre, ou rien s’il n’a jamais été défini (le `true` signifie « ne pas lever d’erreur »). Lorsqu’il est absent, aucune ligne ne correspond : un code qui oublie de le définir voit une table vide au lieu des notes de tout le monde.

### Se connecter avec un rôle soumis à RLS

C’est l’étape que l’on saute, et elle désactive silencieusement tout ce qui précède.

Les superutilisateurs et les rôles dotés de `BYPASSRLS` ignorent toutes les politiques. **Les utilisateurs de base de données créés depuis la console Relyt One ont `BYPASSRLS`, tout comme `relytdb_owner`, le rôle propriétaire avec lequel démarre chaque projet.** Utilisez le propriétaire pour créer les tables, jamais pour servir les requêtes. Si votre application se connecte avec l’un de ces rôles, la politique ne s’exécute jamais, et seul votre code applicatif sépare les utilisateurs.

Créez plutôt un rôle dédié à l’application, en SQL :

```sql
CREATE ROLE notes_app LOGIN NOBYPASSRLS NOSUPERUSER NOCREATEROLE NOCREATEDB;
\password notes_app
GRANT USAGE ON SCHEMA public TO notes_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON notes TO notes_app;
```

`\password` est une commande psql qui demande le mot de passe de façon interactive : il ne se retrouve donc jamais dans l’historique de votre shell ni dans les journaux du serveur. Vérifiez le résultat :

```sql
SELECT rolname, rolbypassrls, rolsuper FROM pg_roles WHERE rolname = 'notes_app';
--  rolname   | rolbypassrls | rolsuper
-- -----------+--------------+----------
--  notes_app | f            | f
```

Les deux colonnes doivent valoir `f`. Un rôle créé de cette façon se connecte aussi bien par la chaîne de connexion directe que par la chaîne poolée ; nous avons vérifié l’ensemble de cette étape sur une vraie branche, y compris le fait que la politique tient derrière le pooler.

## Étape 5 : configurer l’application

Créez une application Next.js 15 et installez le SDK et le pilote Postgres (`@relyt/auth/next` prend en charge Next.js 14.2.35+ et 15.5.28+, mais pas encore la 16 : fixez donc la version) :

```bash
npx create-next-app@15 notes --ts --app
cd notes
npm install @relyt/auth pg
npm install -D @types/pg
```

Ajoutez les paramètres dans `.env.local` :

```bash
# L’Auth URL de l’étape 1 : une origine seule, sans barre oblique finale ni chemin.
RELYT_AUTH_ISSUER=https://<votre URL d’authentification>
RELYT_AUTH_JWKS_URL=https://<votre JWKS URL>

# La même URL d’authentification, pour le navigateur.
NEXT_PUBLIC_RELYT_AUTH_URL=https://<votre URL d’authentification>

# Se connecter en tant que notes_app, le rôle de l’étape 4, jamais avec un utilisateur créé depuis la console.
DATABASE_URL=postgresql://notes_app:<mot-de-passe>@<hôte>/relytdb?sslmode=require
```

L’émetteur doit être exactement l’origine de l’URL d’authentification, car les jetons portent cette valeur à la fois dans `iss` et dans `aud`, et le SDK la compare caractère par caractère. `sslmode=require` compte aussi : les connexions à Relyt One utilisent TLS, et node-postgres ne l’active pas si vous ne le demandez pas.

## Étape 6 : un seul accès à la base de données

Placez tout accès à la base de données derrière une fonction unique qui ouvre une transaction, définit `app.user_id` à partir de l’identifiant d’utilisateur vérifié, et seulement ensuite exécute vos requêtes.

```ts
// lib/db.ts
import { Pool, type PoolClient } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });

export async function withUser<T>(userId: string, fn: (db: PoolClient) => Promise<T>): Promise<T> {
  if (!userId) throw new Error("withUser: empty user id");
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    // `true` = local à cette transaction : une connexion poolée ne le transmet jamais à la requête suivante.
    await client.query("SELECT set_config('app.user_id', $1, true)", [userId]);
    const result = await fn(client);
    await client.query("COMMIT");
    client.release();
    return result;
  } catch (e) {
    await client.query("ROLLBACK").catch(() => undefined);
    client.release(true); // abandonner une connexion peut-être cassée au lieu de la rendre au pool
    throw e;
  }
}
```

Le troisième argument de `set_config` compte plus qu’il n’y paraît. Sans lui, le paramètre dure toute la session, et avec un pool de connexions, la requête suivante qui emprunterait cette connexion s’exécuterait en tant qu’utilisateur précédent. Avec `true`, il disparaît au `COMMIT` ou au `ROLLBACK`.

Il en va de même derrière le pooling par transaction de PgBouncer, car le paramètre ne survit jamais à la transaction.

## Étape 7 : vérifier le jeton à chaque requête

`getClaims` de `@relyt/auth/next` lit le jeton bearer, vérifie sa signature EdDSA à l’aide du JWKS, vérifie l’émetteur, l’audience et l’expiration, puis renvoie les claims, ou `null` si le jeton est absent ou non valide.

```ts
// lib/auth.ts
import { getClaims, RelytAuthUnavailableError } from "@relyt/auth/next";

export type Caller = { ok: true; userId: string } | { ok: false; response: Response };

export async function requireUser(req: Request): Promise<Caller> {
  try {
    const claims = await getClaims(
      req,
      { issuer: process.env.RELYT_AUTH_ISSUER!, jwksUrl: process.env.RELYT_AUTH_JWKS_URL },
      { cookie: false }, // les routes d’API n’acceptent que les jetons bearer
    );
    if (!claims?.sub) {
      return { ok: false, response: Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } }) };
    }
    return { ok: true, userId: claims.sub };
  } catch (e) {
    if (e instanceof RelytAuthUnavailableError) {
      // Impossible de récupérer les clés. Ne pas répondre 401 : cela déconnecterait tout le monde lors d’une coupure réseau.
      return { ok: false, response: Response.json({ error: "auth_unavailable" }, { status: 503, headers: { "retry-after": "2" } }) };
    }
    throw e;
  }
}
```

N’accepter que les jetons bearer (`cookie: false`) met l’API à l’abri du CSRF : un navigateur n’ajoute jamais de lui-même un en-tête `Authorization` à une requête intersite.

## Étape 8 : l’API de notes

```ts
// app/api/notes/route.ts
import { requireUser } from "@/lib/auth";
import { withUser } from "@/lib/db";

export async function GET(req: Request) {
  const caller = await requireUser(req);
  if (!caller.ok) return caller.response;

  const notes = await withUser(caller.userId, async (db) => {
    const { rows } = await db.query(
      "SELECT id, body, created_at FROM notes WHERE user_id = $1 ORDER BY created_at DESC LIMIT 100",
      [caller.userId],
    );
    return rows;
  });
  return Response.json(notes);
}

export async function POST(req: Request) {
  const caller = await requireUser(req);
  if (!caller.ok) return caller.response;

  const input = await req.json().catch(() => null);
  const body = typeof input?.body === "string" ? input.body.trim() : "";
  if (body.length < 1 || body.length > 2000) return Response.json({ error: "invalid_body" }, { status: 400 });

  const note = await withUser(caller.userId, async (db) => {
    const { rows } = await db.query(
      "INSERT INTO notes (user_id, body) VALUES ($1, $2) RETURNING id, body, created_at",
      [caller.userId, body],
    );
    return rows[0];
  });
  return Response.json(note, { status: 201 });
}
```

Remarquez que la requête contient toujours `WHERE user_id = $1`, même si RLS filtrerait de toute façon. Garder les deux est délibéré : la requête est rapide et explicite, et la politique est le filet de sécurité pour le jour où quelqu’un en écrira une qui ne l’est pas.

Remarquez aussi ce que le handler ne fait pas : il ne lit jamais d’identifiant d’utilisateur dans le corps ni dans la chaîne de requête. Le `user_id` qu’il écrit est celui du jeton.

## Étape 9 : se connecter depuis le navigateur

Le navigateur parle à deux endroits : l’URL d’authentification, pour se connecter, et votre API, avec un JWT.

```ts
// lib/auth-client.ts
"use client";
import { createAuthClient } from "@relyt/auth";

const authUrl = process.env.NEXT_PUBLIC_RELYT_AUTH_URL!;
const KEY = "notes.session-token";

export const authClient = createAuthClient({
  authUrl,
  sessionToken: () => localStorage.getItem(KEY),
  onSessionToken: (t) => localStorage.setItem(KEY, t),
});

export async function emailAuth(mode: "sign-in" | "sign-up", email: string, password: string) {
  const body = mode === "sign-up" ? { email, password, name: email.split("@")[0] } : { email, password };
  const res = await fetch(`${authUrl}/api/auth/${mode}/email`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
    credentials: "omit",
  });
  if (!res.ok) throw new Error(`${mode} failed (HTTP ${res.status})`);
  const token = res.headers.get("set-auth-token");
  if (!token) throw new Error("No session token returned. Is this origin a trusted origin?");
  localStorage.setItem(KEY, token);
}

export async function api(path: string, init: RequestInit = {}) {
  const jwt = await authClient.getToken(); // en cache ; renouvelé avant son expiration
  const headers = new Headers(init.headers);
  headers.set("authorization", `Bearer ${jwt}`);
  return fetch(path, { ...init, headers });
}

export async function signOut() {
  const token = localStorage.getItem(KEY);
  if (token) {
    // Terminer la session sur l’URL d’authentification, pas seulement dans cet onglet
    await fetch(`${authUrl}/api/auth/sign-out`, { method: "POST", headers: { authorization: `Bearer ${token}` }, credentials: "omit" }).catch(() => undefined);
  }
  localStorage.removeItem(KEY);
  authClient.clear();
}
```

Comment les éléments s’articulent :

- `emailAuth` envoie une requête POST à l’URL d’authentification. La réponse contient un **jeton de session** dans l’en-tête `set-auth-token`. Ce jeton est la session de l’utilisateur : quiconque le détient peut obtenir des JWT et appeler les endpoints de l’URL d’authentification au nom de cet utilisateur jusqu’à la fin de la session ; traitez-le donc comme un mot de passe.
- `authClient.getToken()` échange le jeton de session contre un **JWT** (15 minutes, EdDSA) et le met en cache, en le renouvelant environ deux minutes avant son expiration.
- `api()` envoie le JWT à votre API. Le navigateur ne vérifie jamais les jetons ; c’est le travail du serveur.

Conserver le jeton de session dans `localStorage` permet de garder ce guide court, mais tout ce qui y est stocké peut être lu par n’importe quel script exécuté sur votre page : une faille de cross-site scripting l’exposerait. Tenez les scripts tiers à l’écart des pages qui gèrent la connexion et utilisez une Content Security Policy stricte. L’exemple TODO montre une configuration plus robuste qui place aussi le JWT dans un cookie `HttpOnly` pour la navigation entre les pages.

Une page minimale pour essayer :

```tsx
// app/page.tsx
"use client";
import { useState } from "react";
import { api, emailAuth } from "@/lib/auth-client";

export default function Home() {
  const [notes, setNotes] = useState<{ id: string; body: string }[]>([]);
  const load = async () => setNotes(await (await api("/api/notes")).json());

  return (
    <main>
      <form
        onSubmit={async (e) => {
          e.preventDefault();
          const f = new FormData(e.currentTarget);
          await emailAuth("sign-up", String(f.get("email")), String(f.get("password")));
          await load();
        }}
      >
        <input name="email" type="email" required />
        <input name="password" type="password" required />
        <button>Sign up</button>
      </form>
      <button onClick={async () => { await api("/api/notes", { method: "POST", body: JSON.stringify({ body: "hello" }) }); await load(); }}>
        Add note
      </button>
      <ul>{notes.map((n) => <li key={n.id}>{n.body}</li>)}</ul>
    </main>
  );
}
```

Lancez `npm run dev`, inscrivez deux utilisateurs dans deux profils de navigateur et ajoutez une note avec chacun. Chaque utilisateur ne voit que les siennes.

## Étape 10 : prouver l’isolation depuis le terminal

Le constater dans un navigateur, c’est bien. Le voir refuser l’accès quand quelque chose manque, c’est mieux. Obtenez un JWT pour un utilisateur avec `curl` :

```bash
AUTH=https://<votre URL d’authentification>
ORIGIN=http://localhost:3000

SESSION=$(curl -s -D - -o /dev/null "$AUTH/api/auth/sign-in/email" \
  -H "origin: $ORIGIN" -H "content-type: application/json" \
  -d '{"email":"alice@example.com","password":"<password>"}' \
  | awk 'tolower($1)=="set-auth-token:" {print $2}' | tr -d '\r')

JWT=$(curl -s "$AUTH/api/auth/token" -H "authorization: Bearer $SESSION" | sed 's/.*"token":"\([^"]*\)".*/\1/')

curl -s http://localhost:3000/api/notes -H "authorization: Bearer $JWT"
```

Essayez ensuite ce qui doit échouer :

```bash
# Sans jeton : 401
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/notes

# Un jeton altéré : 401
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/notes -H "authorization: Bearer ${JWT}x"
```

Et vérifiez directement la part du contrat qui revient à la base de données. Connecté en tant que `notes_app`, sans définir `app.user_id`, la table est vide, quel que soit le nombre de notes existantes :

```sql
SELECT count(*) FROM notes;            -- 0
BEGIN;
SELECT set_config('app.user_id', '<sub d’Alice>', true);
SELECT count(*) FROM notes;            -- uniquement les notes d’Alice
COMMIT;
```

Si la première requête renvoie toutes les notes, votre application est connectée avec un rôle qui contourne RLS. Revenez à l’étape 4.

## Aller plus loin

### Connexion via un réseau social

`startSocialSignIn` envoie l’utilisateur vers GitHub ou Google via l’URL d’authentification, et `completeSocialSignIn` termine la connexion sur votre page de rappel à l’aide d’un code de transfert à usage unique et de PKCE, ce qui fonctionne sans cookies tiers. Le panneau **Integration code** (Code d’intégration) de la console contient les deux extraits, préremplis avec les URL de votre branche. Le README de l’exemple TODO liste les règles applicables à la page de rappel (elle doit exécuter `completeSocialSignIn` avant toute autre chose, et la connexion doit commencer et se terminer sur la même origine).

### Équipes, rôles et autorisations

Lorsqu’un utilisateur a une organisation active, son JWT contient un claim `o` :

```json
"o": { "id": "…", "slug": "acme", "role": "member", "perms": ["todo:create", "todo:read"] }
```

Définissez les ressources et les actions dans **Roles & permissions** (Rôles et autorisations) dans la console, vérifiez `o.perms` dans votre API avant chaque opération, et ajoutez une seconde politique RLS fondée sur une colonne `org_id` (définie par un autre `set_config` dans `withUser`) pour les lignes partagées. L’exemple TODO implémente exactement cela, avec toutes les décisions d’autorisation dans un seul module.

## Dépannage

| Symptôme                                                                                             | Cause probable                                                                                                                                                          |
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| La connexion dans le navigateur échoue avec une erreur réseau                                        | L’origine de l’application ne figure pas dans **Trusted origins** (Origines de confiance), ou elle comporte un chemin ou une barre oblique finale                       |
| Chaque appel d’API renvoie 500, et le journal du serveur montre une erreur `config` de `@relyt/auth` | `RELYT_AUTH_ISSUER` n’est pas une origine seule : elle a une barre oblique finale ou un chemin, ou utilise `http` pour un hôte autre que localhost                      |
| Chaque appel d’API renvoie 401 avec un jeton qui semble valide                                       | `RELYT_AUTH_ISSUER` est une origine différente de celle qui a émis le jeton, par exemple l’URL d’authentification d’une autre branche                                   |
| L’API renvoie 503                                                                                    | Le serveur n’a pas pu récupérer le JWKS. Vérifiez l’accès réseau sortant du serveur vers la JWKS URL                                                                    |
| La connexion à la base de données échoue ou reste bloquée                                            | Il manque `?sslmode=require` dans la chaîne de connexion                                                                                                                |
| Les utilisateurs voient les lignes des autres lorsqu’une requête oublie son filtre                   | L’application se connecte avec un rôle doté de `BYPASSRLS` (c’est le cas des utilisateurs créés depuis la console et de `relytdb_owner`). Utilisez le rôle de l’étape 4 |
| Les requêtes ne renvoient aucune ligne                                                               | `app.user_id` n’a pas été défini dans la même transaction que la requête, ou la requête s’est exécutée en dehors de `withUser`                                          |
| Une requête voit, de façon intermittente, les lignes d’un autre utilisateur                          | `set_config` a été appelé avec `false` (pour toute la session) sur une connexion poolée. Utilisez `true`                                                                |

## Avant la mise en production

- [ ] L’application se connecte avec un rôle `NOBYPASSRLS` dédié (pas `relytdb_owner`), et `rolbypassrls` vaut `f`.
- [ ] Chaque table contenant des données d’utilisateurs a RLS activée **et forcée**, avec `USING` et `WITH CHECK`.
- [ ] Chaque route handler appelle `requireUser`, même derrière un middleware.
- [ ] Les identifiants d’utilisateur ne proviennent que de `claims.sub`.
- [ ] La déconnexion appelle `/api/auth/sign-out` sur l’URL d’authentification, et aucun script tiers ne s’exécute sur les pages qui détiennent le jeton de session.
- [ ] Une panne du JWKS renvoie 503, pas 401.
- [ ] Les origines de production figurent dans **Trusted origins** (Origines de confiance), et les URL de rappel de production sont enregistrées à l’identique.
- [ ] Si vous mettez vous-même le JWKS en cache, le cache dure moins de 960 secondes et vous le récupérez à nouveau lorsqu’un jeton porte un identifiant de clé inconnu, pour que les rotations de clés fonctionnent. (`@relyt/auth` fait déjà les deux.)
- [ ] Vous savez que révoquer les sessions d’un utilisateur dans la console laisse les JWT déjà émis valides jusqu’à 15 minutes, et votre application gère les cas où cela compte.

## Prochaines étapes

- Clonez l’[exemple TODO](https://github.com/relytcloud/relyt-auth-examples/tree/main/todo) pour la connexion via un réseau social, les équipes, les rôles et une suite de tests qui vérifie tout ce qui précède.
- Lisez le [README de `@relyt/auth`](https://github.com/relytcloud/relyt-auth-js#readme) pour toutes les options de `createAuthClient`, `getClaims` et `createVerifier`.
- Lisez [L’authentification gérée de Relyt One, désormais en préversion](/blog/fr/managed-auth-preview) pour la conception et le modèle de sécurité.

[Créer un projet gratuit](/auth/signin?ref=blog-managed-auth-getting-started)
