
# Protéger une API Express

Ce démarrage rapide protège une API Express 5 avec des jetons d'accès KeenAuth. Vous ajoutez le middleware `expressAuth` du SDK Node.js, exigez une scope sur une route et testez l'API hors ligne avec le kit de test du SDK. Express n'est pas une dépendance du SDK ; le middleware fonctionne aussi avec Express 4.

L'API comporte trois points de terminaison :

| Point de terminaison | Exige |
|---|---|
| `GET /api/public` | rien |
| `GET /api/private` | un jeton d'accès valide pour votre API |
| `GET /api/messages` | un jeton d'accès valide avec la scope `messages:read` |

## Avant de commencer

- Un environnement KeenAuth. Son émetteur ressemble à `https://your-environment.eu.keenauth.app`.
- Une API enregistrée dans l'environnement ([Créer une API](/docs/api/management/apis/create)). Son `identifier`, par exemple `https://api.example.com`, est l'audience des jetons acceptés par votre API.
- La scope `messages:read` sur cette API, accordée à l'application qui l'appelle.
- Node.js 22 ou une version ultérieure.

## 1. Installer le SDK

```sh
npm install @keenauth/node express@5.2.1
```

Le SDK n'est pas encore publié. En attendant, l'exemple renvoie vers le code source du SDK dans son `package.json`.

## 2. Configurer

L'API lit sa configuration uniquement depuis des variables d'environnement. Placez-les dans un fichier `.env`, que `npm start` charge avec `node --env-file-if-exists=.env`, et ne le versionnez jamais.

| Variable | Obligatoire | Signification |
|---|---|---|
| `KEENAUTH_ISSUER` | oui | L'émetteur de votre environnement |
| `KEENAUTH_AUDIENCE` | oui | L'identifiant de votre API |
| `PUBLIC_BASE_URL` | oui | L'URL appelée par les clients (voir [Derrière un proxy](#derrière-un-proxy)) |
| `KEENAUTH_WEBHOOK_SECRET` | recommandé | Le secret de signature `idp_whsec_` du point de terminaison webhook qui envoie `token.revoked` (deux valeurs séparées par des virgules pendant la rotation). Sans lui, l'API ne connaît jamais les jetons révoqués |
| `PORT` | non | Port d'écoute, par défaut `8080` |

## 3. Protéger vos routes

`createApp` crée le protecteur et les routes. `expressAuth(protector)` exige un jeton valide ; avec `{ scopes }`, il exige aussi des scopes. Le gestionnaire lit les revendications vérifiées dans `req.auth`.

```js
export function createApp({ verifier, publicBaseUrl, revocation, webhookSecrets = [] }) {
  const protector = createProtector(verifier, {
    publicBaseUrl,
    revocation,
    // The error never contains the token or claim values; never log the Authorization or DPoP header.
    onError: (err) => console.warn(`rejected: ${err.message}`),
  });

  const app = express();
  app.disable("x-powered-by");

  // No token needed.
  app.get("/api/public", (req, res) => {
    res.json({ message: "Hello from a public endpoint." });
  });

  // Any valid access token for this API.
  app.get("/api/private", expressAuth(protector), (req, res) => {
    res.json({ message: "Hello from a private endpoint.", sub: req.auth.subject });
  });

  // A valid access token that also carries the messages:read scope.
  app.get("/api/messages", expressAuth(protector, { scopes: ["messages:read"] }), (req, res) => {
    res.json({ messages: ["Hello from a scoped endpoint."] });
  });

  // token.revoked deliveries from KeenAuth; the handler reads the raw body itself, so no JSON parser runs before it.
  if (webhookSecrets.length > 0) {
    app.post(WEBHOOK_PATH, ...createWebhookHandler({ secrets: webhookSecrets, revocation }));
  } else {
    console.warn("KEENAUTH_WEBHOOK_SECRET is not set: token.revoked events are not received, so revoked tokens stay valid until they expire");
  }

  return app;
}
```

`server.js` construit le vérificateur réel à partir de la configuration. Le vérificateur récupère les clés de signature de votre environnement la première fois qu'il rencontre un jeton.

```js
const issuer = process.env.KEENAUTH_ISSUER;
const audience = process.env.KEENAUTH_AUDIENCE;
// The URL clients call: DPoP proofs are checked against it, never against the Host header.
const publicBaseUrl = process.env.PUBLIC_BASE_URL;
if (!issuer || !audience || !publicBaseUrl) {
  console.error("KEENAUTH_ISSUER, KEENAUTH_AUDIENCE and PUBLIC_BASE_URL are required");
  process.exit(1);
}

// createVerifier refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
const verifier = createVerifier({ issuer, audience });
const revocation = createMemoryRevocationCache();
// The signing secret (idp_whsec_...) of the webhook endpoint that delivers token.revoked; comma separated while rotating.
const webhookSecrets = (process.env.KEENAUTH_WEBHOOK_SECRET ?? "").split(",").map((s) => s.trim()).filter(Boolean);
const app = createApp({ verifier, publicBaseUrl, revocation, webhookSecrets });

const port = Number(process.env.PORT || 8080);
app.listen(port, () => console.log(`listening on port ${port}`));
```

## 4. Lancer et appeler l'API

```sh
npm start
```

Pour un premier appel, obtenez un jeton avec le grant client credentials d'une application machine-à-machine qui dispose de la scope `messages:read` sur votre API :

```sh
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource=https://api.example.com \
  -d scope=messages:read \
  https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)

curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
```

## 5. Tester sans réseau

`@keenauth/node/testing` génère des jetons signés avec une clé de test publique, et `createTestVerifier` n'accepte que ces jetons. Les tests transmettent ce vérificateur à `createApp` et appellent l'application sur un port local ; ils ne nécessitent donc ni réseau ni compte KeenAuth. Voir [Kit de test hors ligne](/docs/sdk/testing).

```js
test("scope check: 403 insufficient_scope without messages:read, 200 with it", async () => {
  await expectProblem(await get("/api/messages", bearer(mintAccessToken(AUDIENCE))), 403, "insufficient_scope", /scope="messages:read"/);
  const ok = await get("/api/messages", bearer(mintAccessToken(AUDIENCE, { claims: { scope: "profile messages:read" } })));
  assert.equal(ok.status, 200);
});
```

```sh
npm test
```

Les tests de l'exemple vérifient qu'un jeton valide est accepté, et qu'un jeton absent, expiré ou destiné à une autre audience, une scope manquante, un jeton placé dans la query de l'URL et un jeton révoqué sont rejetés avec les statuts ci-dessous. Ils envoient aussi un jeton lié à DPoP avec une preuve pour `PUBLIC_BASE_URL`, et vérifient que `server. The tests also send signed `token.revoked` deliveries: a valid one revokes the token, a forged, tampered or stale one gets `400` and revokes nothing, and a repeated one has one effect.js` refuse de démarrer avec l'émetteur de test.

## Réponses

| Requête | Statut | `code` dans le corps |
|---|---|---|
| Pas d'en-tête `Authorization` | `401` | `missing_token` |
| Jeton expiré, jeton pour une autre API, mauvaise signature, jeton révoqué | `401` | `invalid_token` |
| Jeton sans la scope requise | `403` | `insufficient_scope` |
| Jeton dans la query de l'URL (`?access_token=`) | `400` | `invalid_request` |

Le corps est `application/problem+json` et l'en-tête `WWW-Authenticate` transporte le challenge standard. La table complète figure dans [Protéger votre API avec un middleware](/docs/sdk/protect-api#réponses).

## Derrière un proxy

Si les clients utilisent DPoP, le middleware vérifie que chaque preuve a été produite pour l'URL appelée par le client. Définissez `PUBLIC_BASE_URL` sur l'URL externe appelée par les clients, par exemple `https://api.example.com`, y compris derrière un proxy ou un load balancer. Cette variable est obligatoire et doit utiliser `https` (`http` uniquement pour `localhost`, `127.0.0.1` ou `[::1]`) ; l'application refuse de démarrer sans elle. Le middleware ne fait jamais confiance aux en-têtes `X-Forwarded-*`, quelle que soit la valeur du paramètre Express `trust proxy`.

## Jetons révoqués

Un jeton d'accès reste valide jusqu'à son expiration ; KeenAuth informe donc votre API lorsque des jetons sont révoqués. L'exemple transmet un cache de révocation en mémoire au protecteur et sert un webhook `token.revoked` sur `POST /webhooks/keenauth` (`webhook.js`) qui l'alimente.

1. Dans le tableau de bord, créez un point de terminaison webhook avec l'URL `https://<your host>/webhooks/keenauth` qui s'abonne à `token.revoked`, puis exportez son secret de signature dans `KEENAUTH_WEBHOOK_SECRET`.
2. Le gestionnaire vérifie la signature avec le SDK sur le corps brut et répond `400` lorsqu'elle est invalide ou que l'horodatage s'écarte de plus de cinq minutes. Il associe le sélecteur de l'événement (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) à un marqueur de révocation et répond `204` ; le middleware rejette ensuite chaque jeton correspondant émis avant `not_before`.
3. KeenAuth livre au moins une fois, donc le gestionnaire applique chaque `webhook-id` une seule fois. Si le marqueur ne peut pas être stocké, il répond `503` et KeenAuth renvoie l'événement. Il ignore les autres types d'événements et les sélecteurs qu'aucun jeton d'accès ne peut correspondre (`refresh_family`, `api`).

Le gestionnaire journalise l'identifiant de l'événement, le type de sélecteur et la raison, jamais le corps, une signature ni le secret. Chaque processus a son propre cache ; avec plusieurs instances, placez les marqueurs dans un stockage partagé derrière la même méthode `revoke`. Voir [Cache de révocation](/docs/sdk/protect-api#cache-de-révocation-tokenrevoked).

## Étapes suivantes

- [Protéger votre API avec un middleware](/docs/sdk/protect-api) : Fastify, exigences de step-up, DPoP et toutes les options.
- [Vérifier les jetons](/docs/sdk/token-verification) : le vérificateur sous le middleware.
- [Kit de test hors ligne](/docs/sdk/testing).
