Proteggi un'API Express

Questa guida rapida protegge un'API Express 5 con access token di KeenAuth. Aggiungi il middleware expressAuth dell'SDK per Node.js, richiedi uno scope su una route e testi l'API offline con il test kit dell'SDK. Express non è una dipendenza dell'SDK; il middleware funziona anche con Express 4.

L'API ha tre endpoint:

Endpoint Richiede
GET /api/public nulla
GET /api/private un access token valido per la tua API
GET /api/messages un access token valido con lo scope messages:read

Prima di iniziare

1. Installa l'SDK

npm install @keenauth/node express@5.2.1

L'SDK non è ancora pubblicato. Finché non lo sarà, l'esempio rimanda al codice sorgente dell'SDK nel suo package.json.

2. Configura

L'API legge la configurazione solo da variabili d'ambiente. Inseriscile in un file .env, che npm start carica con node --env-file-if-exists=.env, e non committare mai quel file.

Variabile Obbligatoria Significato
KEENAUTH_ISSUER sì L'issuer del tuo ambiente
KEENAUTH_AUDIENCE sì L'identificatore della tua API
PUBLIC_BASE_URL sì L'URL che i client chiamano (vedi Dietro un proxy)
KEENAUTH_WEBHOOK_SECRET consigliata Il segreto di firma idp_whsec_ dell'endpoint webhook che consegna token.revoked (due, separati da virgola, durante la rotazione). Senza di essa l'API non viene mai a sapere dei token revocati
PORT no Porta di ascolto, predefinita 8080

3. Proteggi le tue route

createApp crea il protector e le route. expressAuth(protector) richiede un token valido; con { scopes } richiede anche degli scope. L'handler legge i claim verificati da 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 costruisce il verifier reale a partire dalla configurazione. Il verifier recupera le chiavi di firma dal tuo ambiente la prima volta che vede un token.

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. Avvia e chiama l'API

npm start

Per una prima chiamata, ottieni un token con il grant client credentials di un'applicazione machine-to-machine che ha lo scope messages:read sulla tua 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. Testa senza rete

@keenauth/node/testing genera token firmati con una chiave di test pubblica, e createTestVerifier accetta solo quei token. I test passano quel verifier a createApp e chiamano l'app su una porta locale, quindi non richiedono rete né un account KeenAuth. Vedi Test kit offline.

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

I test dell'esempio verificano che un token valido venga accettato e che un token mancante, scaduto o con audience errata, uno scope mancante, un token nella query dell'URL e un token revocato vengano rifiutati con gli stati indicati sotto. Inviano anche un token legato a DPoP con una proof per PUBLIC_BASE_URL, e verificano che 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 rifiuti di avviarsi con l'issuer di test.

Risposte

Richiesta Stato code nel body
Nessun header Authorization 401 missing_token
Token scaduto, token per un'altra API, firma non valida, token revocato 401 invalid_token
Token senza lo scope richiesto 403 insufficient_scope
Token nella query dell'URL (?access_token=) 400 invalid_request

Il body è application/problem+json e l'header WWW-Authenticate contiene la challenge standard. La tabella completa è in Proteggere la tua API con il middleware.

Dietro un proxy

Se i client usano DPoP, il middleware verifica che ogni proof sia stata creata per l'URL chiamato dal client. Imposta PUBLIC_BASE_URL sull'URL esterno che i client chiamano, per esempio https://api.example.com, anche dietro un proxy o un load balancer. È obbligatorio e deve usare https (http solo per localhost, 127.0.0.1 o [::1]); l'app rifiuta di avviarsi senza di esso. Il middleware non si fida mai degli header X-Forwarded-*, qualunque sia l'impostazione trust proxy di Express.

Token revocati

Un access token resta valido fino alla scadenza, quindi KeenAuth comunica alla tua API quando i token vengono revocati. L'esempio passa una cache di revoca in memoria al protector e serve un webhook token.revoked su POST /webhooks/keenauth (webhook.js) che la alimenta.

  1. Nella dashboard, crea un endpoint webhook con l'URL https://<your host>/webhooks/keenauth che si iscrive a token.revoked, ed esporta il suo segreto di firma come KEENAUTH_WEBHOOK_SECRET.
  2. L'handler verifica la firma con l'SDK sul body grezzo e risponde 400 quando non è valida o quando il timestamp differe di più di cinque minuti. Associa il selettore dell'evento (access_token, session, user, client, org, org_user, client_user) a un marcatore di revoca e risponde 204; il middleware rifiuta poi ogni token corrispondente emesso prima di not_before.
  3. KeenAuth consegna almeno una volta, quindi l'handler applica ogni webhook-id una sola volta. Se il marcatore non può essere salvato, risponde 503 e KeenAuth consegna di nuovo l'evento. Ignora gli altri tipi di evento e i selettori che nessun access token può corrispondere (refresh_family, api).

L'handler registra l'id dell'evento, il tipo di selettore e il motivo, mai il body, una firma o il segreto. Ogni processo ha la propria cache; con più istanze, inserisci i marcatori in uno store condiviso dietro lo stesso metodo revoke. Vedi Cache di revoca.

Prossimi passi