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). Son
identifier, par exemplehttps://api.example.com, est l'audience des jetons acceptés par votre API. - La scope
messages:readsur cette API, accordée à l'application qui l'appelle. - Node.js 22 ou une version ultérieure.
1. Installer le SDK
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) |
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.
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.
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
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 :
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.
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);
});
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.revokeddeliveries: a valid one revokes the token, a forged, tampered or stale one gets400 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.
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.
- Dans le tableau de bord, créez un point de terminaison webhook avec l'URL
https://<your host>/webhooks/keenauthqui s'abonne àtoken.revoked, puis exportez son secret de signature dansKEENAUTH_WEBHOOK_SECRET. - Le gestionnaire vérifie la signature avec le SDK sur le corps brut et répond
400lorsqu'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épond204; le middleware rejette ensuite chaque jeton correspondant émis avantnot_before. - KeenAuth livre au moins une fois, donc le gestionnaire applique chaque
webhook-idune seule fois. Si le marqueur ne peut pas être stocké, il répond503et 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.
Étapes suivantes
- Protéger votre API avec un middleware : Fastify, exigences de step-up, DPoP et toutes les options.
- Vérifier les jetons : le vérificateur sous le middleware.
- Kit de test hors ligne.