
# Ajouter la connexion à une application React Router

Ce démarrage rapide ajoute la connexion KeenAuth à une application React Router 8 en mode framework, avec le client OpenID Connect standard `openid-client`. Les loaders et les actions s'exécutent côté serveur : votre application est donc le client OAuth. Elle conserve chaque jeton dans une session côté serveur, et le navigateur ne reçoit que le HTML et un cookie d'identifiant de session signé. Vous ajoutez les routes de connexion, protégez une page, appelez une API protégée avec le jeton d'accès de l'utilisateur, vous déconnectez et testez l'application hors ligne.

Pour une application monopage sans serveur, consultez [Ajouter la connexion à une application React monopage](/docs/quickstarts/react).

L'application expose les routes suivantes :

| Route | Rôle |
|---|---|
| `GET /` | Page d'accueil publique : un bouton de connexion, ou le nom de l'utilisateur et un bouton de déconnexion |
| `GET /dashboard` | Nécessite une session. Affiche l'utilisateur et les messages renvoyés par votre API |
| `POST /login` | Démarre la connexion. Accepté uniquement depuis une page de cette application |
| `GET /auth/callback` | Vérifie la réponse, échange le code et démarre la session |
| `POST /logout` | Termine la session ici et chez KeenAuth. Accepté uniquement depuis une page de cette application |

## Avant de commencer

- Un environnement KeenAuth. Son émetteur ressemble à `https://your-environment.eu.keenauth.app`.
- Une application web dans l'environnement ([Créer une application](/docs/api/management/applications/create)) avec un secret client (`client_secret_basic`). Dans un environnement `development`, enregistrez l'URI de redirection `http://localhost/auth/callback` et l'URI de redirection après déconnexion `http://localhost/` ; une URI de boucle locale est enregistrée sans port et correspond à n'importe quel port, comme `5173`.
- Une API enregistrée dans l'environnement ([Créer une API](/docs/api/management/apis/create)) avec le scope `messages:read`. Son `identifier`, par exemple `https://api.example.com`, est l'audience de ses jetons d'accès. Le [démarrage rapide Express](/docs/quickstarts/express) crée une API qui accepte ces jetons.
- Node.js 22.22 ou une version ultérieure, et une application React Router en mode framework (`npx create-react-router@8.5.0`).

## 1. Installer

```sh
npm install --save-exact openid-client@6.8.8
```

`openid-client` implémente la partie relying party d'OpenID Connect : la découverte, le flux à code d'autorisation avec PKCE, la vérification `iss` de la RFC 9207 et les contrôles de l'ID token. L'exemple épingle chaque paquet à une version exacte et installe depuis `package-lock.json` avec `npm ci --ignore-scripts`, de sorte qu'aucun script de paquet ne s'exécute pendant l'installation.

## 2. Configurer

L'application lit sa configuration uniquement depuis des variables d'environnement. Placez-les dans `.env`, que `npm run dev` et `npm start` chargent avec `node --env-file=.env`, et ne versionnez jamais ce fichier.

| Variable | Obligatoire | Signification |
|---|---|---|
| `KEENAUTH_ISSUER` | oui | L'émetteur de votre environnement, en `https` |
| `KEENAUTH_CLIENT_ID` | oui | Le `client_id` de l'application web |
| `KEENAUTH_CLIENT_SECRET` | oui | Son secret client |
| `APP_BASE_URL` | oui | L'URL publique de l'application. L'URI de redirection est construite à partir d'elle. `http` uniquement pour `localhost`, `127.0.0.1` ou `[::1]` |
| `SESSION_SECRET` | oui | Au moins 32 caractères ; signe le cookie de session. Générez-en un avec `openssl rand -base64 32` |
| `API_URL` | oui | L'API appelée par l'application, par exemple `http://localhost:8080` |
| `KEENAUTH_API_AUDIENCE` | oui | L'identifiant de cette API dans KeenAuth |

`loadConfig` dans `app/lib/config.server.ts` vérifie ces valeurs à la première requête. Une valeur manquante ou inutilisable interrompt la requête, et aucun message d'erreur ne contient de valeur :

```ts
  const issuer = url("KEENAUTH_ISSUER", false);
  const appBaseUrl = url("APP_BASE_URL", true).origin;
  const sessionSecret = need("SESSION_SECRET");
  if (sessionSecret.length < 32) throw new Error("SESSION_SECRET must be at least 32 characters");
```

Les fichiers qui se terminent par `.server.ts` n'atteignent jamais le bundle du navigateur ; React Router interrompt la compilation si une page les importe pour le navigateur.

## 3. Créer le client OpenID Connect

`app/lib/keenauth.server.ts` construit le client une seule fois à partir de la découverte (`<issuer>/.well-known/openid-configuration`) :

```ts
export function keenauth(): Promise<client.Configuration> {
  discovered ??= client
    .discovery(config().issuer, config().clientId, undefined, client.ClientSecretBasic(config().clientSecret), {
      // Also check the ID token signature with your environment's keys (jwks_uri), not only the TLS connection.
      execute: [client.enableNonRepudiationChecks],
    })
```

Par défaut, `openid-client` fait confiance à un ID token qui provient directement du endpoint de jeton via TLS. `enableNonRepudiationChecks` vérifie en plus sa signature avec les clés publiées par votre environnement.

## 4. Conserver la session côté serveur

La session réside dans des fichiers sur le serveur ; le cookie ne contient qu'un identifiant de session signé :

```ts
  storage ??= createFileSessionStorage<SessionData>({
    dir: ".sessions",
    cookie: {
      name: "__keenauth_session",
      httpOnly: true,
      sameSite: "lax",
      // Secure whenever the app is served over https; plain http only for localhost.
      secure: config().redirectUri.startsWith("https:"),
      path: "/",
      maxAge: 8 * 60 * 60,
      secrets: [config().sessionSecret],
    },
  });
```

Un stockage de fichiers ne sert qu'un seul serveur. Avec plusieurs serveurs, écrivez un stockage de session avec `createSessionStorage` et un magasin partagé, comme Redis. N'utilisez pas le stockage de session par cookie pour les jetons : deux jetons peuvent dépasser la limite de 4 Ko d'un cookie, et le navigateur les conserverait.

## 5. Démarrer la connexion

Les routes sont listées dans `app/routes.ts` :

```ts
export default [
  index("routes/home.tsx"),
  route("dashboard", "routes/dashboard.tsx"),
  route("login", "routes/login.ts"),
  route("auth/callback", "routes/callback.ts"),
  route("logout", "routes/logout.ts"),
] satisfies RouteConfig;
```

La connexion est un formulaire qui envoie une requête POST vers `/login` :

```tsx
        <Form method="post" action="/login" reloadDocument>
          <button type="submit">Sign in</button>
        </Form>
```

L'action crée un nouveau `state`, un nouveau `nonce` et un nouveau verifier PKCE pour chaque connexion, et les conserve dans la session :

```ts
export async function action({ request }: Route.ActionArgs) {
  assertSameOrigin(request);
  const session = await getSession(request);
  const transaction = newTransaction();
  session.set("transaction", transaction);
  const url = await authorizationUrl(transaction);
  return redirect(url.href, { headers: { "Set-Cookie": await sessions().commitSession(session) } });
}
```

La requête d'autorisation envoie le challenge S256 du verifier et l'identifiant de votre API comme `resource` (RFC 8707). Sans `resource`, le jeton d'accès n'est pas destiné à votre API, et celle-ci le rejette.

```ts
  return client.buildAuthorizationUrl(await keenauth(), {
    redirect_uri: config().redirectUri,
    // messages:read is a scope of your API; the example API requires it on /api/messages.
    scope: "openid profile email messages:read",
    state: transaction.state,
    nonce: transaction.nonce,
    // PKCE (RFC 7636): S256 of the verifier that only this session knows.
    code_challenge: await client.calculatePKCECodeChallenge(transaction.codeVerifier),
    code_challenge_method: "S256",
    // RFC 8707: an access token for your API.
    resource: config().apiAudience,
  });
```

### N'accepter le POST que depuis vos propres pages

React Router refuse un POST inter-site vers l'action d'une page, mais `/login` et `/logout` sont des routes de ressource (une action sans page), et pour celles-ci la vérification vous incombe. `assertSameOrigin` n'accepte le POST que si son en-tête `Origin` (ou, à défaut, `Referer`) désigne `APP_BASE_URL` :

```ts
  const app = new URL(config().redirectUri).origin;
  const source = request.headers.get("Origin") ?? request.headers.get("Referer");
```

## 6. Traiter le callback

Le callback retire d'abord la transaction de la session, de sorte qu'un `state` ne peut servir qu'une fois :

```ts
  const old = await getSession(request);
  const transaction = old.get("transaction");
  // The transaction works once: it leaves the session before anything else happens.
  old.unset("transaction");
```

`authorizationCodeGrant` vérifie le `state` et l'`iss` que votre environnement envoie dans chaque réponse d'autorisation (RFC 9207), échange le code avec le verifier PKCE et vérifie l'ID token : sa signature, son `iss`, son `aud`, son `exp`, son `iat` et le `nonce` de cette connexion. L'URI de redirection est construite à partir de `APP_BASE_URL`, jamais à partir de l'en-tête `Host` :

```ts
  const callbackUrl = new URL(config().redirectUri);
  callbackUrl.search = new URL(request.url).search;
  return client.authorizationCodeGrant(await keenauth(), callbackUrl, {
    pkceCodeVerifier: transaction.codeVerifier,
    expectedState: transaction.state,
    expectedNonce: transaction.nonce,
    idTokenExpected: true,
  });
```

Une vérification échouée répond `401` avec un code d'erreur fixe. Après une connexion réussie, l'application supprime l'ancienne session et conserve l'utilisateur et les jetons dans une nouvelle session :

```ts
  // A new session id after sign-in prevents session fixation: the old session is deleted, a new one is created.
  await sessions().destroySession(old);
  const session = await sessions().getSession(null);
```

## 7. Protéger les pages et appeler votre API

Un loader qui nécessite un utilisateur appelle `requireUser`, qui redirige vers la page d'accueil en l'absence de session. L'exemple n'a pas de refresh token : lorsque le jeton d'accès expire, l'utilisateur se reconnecte.

```ts
export async function loader({ request }: Route.LoaderArgs) {
  const { user, accessToken } = await requireUser(request);
  const api = await fetchMessages(accessToken);
  return { user, api };
}
```

Le loader appelle votre API avec le jeton d'accès comme bearer token. Le jeton reste sur le serveur ; la page ne reçoit que l'utilisateur et les messages.

```ts
    response = await fetch(`${config().apiUrl}/api/messages`, {
      headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
    });
```

## 8. Se déconnecter

La déconnexion est un `POST` depuis une page de cette application. L'action termine la session locale et envoie le navigateur vers le endpoint de déconnexion de votre environnement (OpenID Connect RP-Initiated Logout) avec `id_token_hint`, `client_id` et `post_logout_redirect_uri`, qui le ramène dans votre application :

```ts
  const session = await getSession(request);
  const idToken = session.get("idToken");
  const destroyed = await sessions().destroySession(session);
  return redirect((await logoutUrl(idToken)).href, { headers: { "Set-Cookie": destroyed } });
```

## 9. Exécuter l'application

Démarrez l'API du démarrage rapide Express (ou toute API qui accepte les jetons), puis lancez l'application :

```sh
npm run dev
```

Ouvrez `http://localhost:5173`, choisissez **Sign in**, connectez-vous chez KeenAuth et ouvrez le tableau de bord.

## 10. Tester sans réseau

Les tests n'ont besoin ni de réseau ni d'un compte KeenAuth. `npm test` compile l'application et exécute la compilation via le gestionnaire de requêtes de React Router. Un faux `fetch` joue le rôle de votre environnement (découverte, jeu de clés créé pour l'exécution des tests, endpoint de jeton) et celui de l'API.

```ts
test("callback: an unknown or reused state is refused before any token request and starts no session", async () => {
  const b = browser();
  await startSignIn(b);
  const wrong = await returnFromKeenAuth(b, { state: "not-the-state" });
```

```sh
npm test
npm run deps    # exact pins, lockfile sources and licences
```

Les tests vérifient que la configuration est validée (et qu'aucune erreur ne contient de secret) ; que la connexion envoie PKCE S256, `state`, `nonce` et `resource`, et qu'elle est refusée depuis un autre site ou avec un `GET` ; que le code est échangé avec le verifier correspondant et l'authentification du client ; que le jeton d'accès est envoyé à l'API dans l'en-tête `Authorization` et jamais au navigateur ; qu'un `state` inconnu ou réutilisé, un `iss` absent ou incorrect, une erreur d'autorisation, ainsi qu'un ID token émis pour un autre client, avec un autre nonce ou signé par une autre clé, sont refusés sans session ; que la connexion démarre un nouvel identifiant de session ; qu'une page sans session redirige vers la page d'accueil ; et que la déconnexion est refusée depuis un autre site et se rend au endpoint de déconnexion avec `id_token_hint`.

La connexion complète dans un vrai navigateur nécessite un environnement KeenAuth. Elle ne fait pas partie des tests hors ligne.

## Derrière un proxy ou un répartiteur de charge

Définissez `APP_BASE_URL` sur l'URL publique de l'application ; l'URI de redirection et la vérification de l'Origin l'utilisent, jamais l'en-tête `Host`. Si un proxy termine le TLS, `APP_BASE_URL` reste l'URL `https`, de sorte que le cookie de session reste `Secure`.

## Limites actuelles

- Pas de refresh token ni de connexion silencieuse : l'utilisateur se reconnecte lorsque le jeton d'accès expire.
- Le stockage de session par fichiers conserve les jetons en clair dans des fichiers sous `.sessions/` et ne supprime pas les fichiers expirés. Protégez ce répertoire, ou utilisez un magasin partagé qui chiffre et fait expirer les sessions.
- Le client s'authentifie avec un secret client. `openid-client` prend aussi en charge `private_key_jwt` ; l'exemple ne le montre pas.

## Étapes suivantes

- [Protéger une API Express](/docs/quickstarts/express) : l'API appelée par cette application.
- [Vérifier les jetons](/docs/sdk/token-verification) : le vérificateur utilisé par le middleware de votre API.
