Eine Express-API absichern

Diese Schnellstartanleitung sichert eine Express-5-API mit Zugriffstoken von KeenAuth ab. Sie fügen die expressAuth-Middleware des Node.js-SDKs hinzu, verlangen einen Scope für eine Route und testen die API offline mit dem Test-Kit des SDKs. Express ist keine Abhängigkeit des SDKs; die Middleware funktioniert auch mit Express 4.

Die API hat drei Endpunkte:

Endpunkt Erfordert
GET /api/public nichts
GET /api/private ein gültiges Zugriffstoken für Ihre API
GET /api/messages ein gültiges Zugriffstoken mit dem messages:read-Scope

Vor dem Start

1. SDK installieren

npm install @keenauth/node express@5.2.1

Das SDK ist noch nicht veröffentlicht. Bis dahin verlinkt das Beispiel auf den SDK-Quellcode in seinem package.json.

2. Konfigurieren

Die API liest ihre Konfiguration ausschließlich aus Umgebungsvariablen. Legen Sie sie in einer .env-Datei ab, die npm start mit node --env-file-if-exists=.env lädt, und committen Sie diese Datei niemals.

Variable Erforderlich Bedeutung
KEENAUTH_ISSUER ja Der Aussteller Ihrer Umgebung
KEENAUTH_AUDIENCE ja Der Bezeichner Ihrer API
PUBLIC_BASE_URL ja Die URL, die Clients aufrufen (siehe Hinter einem Proxy)
KEENAUTH_WEBHOOK_SECRET empfohlen Das idp_whsec_-Signaturgeheimnis des Webhook-Endpunkts, der token.revoked zustellt (zwei, durch Komma getrennt, während Sie es rotieren). Ohne dieses erfährt die API nie von widerrufenen Token
PORT nein Listen-Port, Standard 8080

3. Routen absichern

createApp erstellt den Schutz und die Routen. expressAuth(protector) verlangt ein gültiges Token; mit { scopes } verlangt es zusätzlich Scopes. Der Handler liest die geprüften Claims aus 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 erstellt aus der Konfiguration den echten Verifier. Der Verifier lädt die Signaturschlüssel von Ihrer Umgebung, sobald er das erste Token sieht.

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. Ausführen und API aufrufen

npm start

Für einen ersten Aufruf holen Sie ein Token mit dem Client-Credentials-Grant einer Machine-to-Machine-Anwendung, die den messages:read-Scope auf Ihrer API hat:

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. Ohne Netzwerk testen

@keenauth/node/testing erzeugt Token, die mit einem öffentlichen Testschlüssel signiert sind, und createTestVerifier akzeptiert nur diese Token. Die Tests übergeben diesen Verifier an createApp und rufen die App auf einem lokalen Port auf, daher benötigen sie weder Netzwerk noch ein KeenAuth-Konto. Siehe Offline-Test-Kit.

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

Die Tests des Beispiels prüfen, dass ein gültiges Token akzeptiert wird und dass ein fehlendes, abgelaufenes oder für eine andere Zielgruppe ausgestelltes Token, ein fehlender Scope, ein Token in der URL-Abfrage und ein widerrufenes Token mit den unten stehenden Statuscodes abgelehnt werden. Sie senden außerdem ein DPoP-gebundenes Token mit einem Nachweis für PUBLIC_BASE_URL und prüfen, dass 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 mit dem Test-Aussteller den Start verweigert.

Antworten

Anfrage Status code im Body
Kein Authorization-Header 401 missing_token
Abgelaufenes Token, Token für eine andere API, ungültige Signatur, widerrufenes Token 401 invalid_token
Token ohne den erforderlichen Scope 403 insufficient_scope
Token in der URL-Abfrage (?access_token=) 400 invalid_request

Der Body ist application/problem+json, und der WWW-Authenticate-Header enthält die Standard-Challenge. Die vollständige Tabelle steht unter API mit Middleware absichern.

Hinter einem Proxy

Wenn Clients DPoP verwenden, prüft die Middleware, dass jeder Nachweis für die URL erstellt wurde, die der Client aufgerufen hat. Setzen Sie PUBLIC_BASE_URL auf die externe URL, die Clients aufrufen, zum Beispiel https://api.example.com, auch hinter einem Proxy oder Load Balancer. Die Variable ist erforderlich und muss https verwenden (http nur für localhost, 127.0.0.1 oder [::1]); ohne sie startet die Anwendung nicht. Die Middleware vertraut X-Forwarded-*-Headern niemals, unabhängig von der Einstellung trust proxy in Express.

Widerrufene Token

Ein Zugriffstoken bleibt gültig, bis es abläuft. Daher teilt KeenAuth Ihrer API mit, wenn Token widerrufen werden. Das Beispiel übergibt dem Schutz einen In-Memory-Widerrufscache und stellt unter POST /webhooks/keenauth (webhook.js) einen token.revoked-Webhook bereit, der diesen Cache füttert.

  1. Legen Sie im Dashboard einen Webhook-Endpunkt mit der URL https://<your host>/webhooks/keenauth an, der token.revoked abonniert, und exportieren Sie sein Signaturgeheimnis als KEENAUTH_WEBHOOK_SECRET.
  2. Der Handler prüft die Signatur mit dem SDK auf dem rohen Body und antwortet mit 400, wenn sie ungültig ist oder der Zeitstempel mehr als fünf Minuten abweicht. Er ordnet den Selektor des Ereignisses (access_token, session, user, client, org, org_user, client_user) einer Widerrufsmarkierung zu und antwortet mit 204; die Middleware lehnt danach jedes passende Token ab, das vor not_before ausgestellt wurde.
  3. KeenAuth liefert mindestens einmal aus, daher wendet der Handler jedes webhook-id genau einmal an. Kann die Markierung nicht gespeichert werden, antwortet er mit 503, und KeenAuth liefert das Ereignis erneut. Andere Ereignistypen und Selektoren, auf die kein Zugriffstoken passen kann (refresh_family, api), ignoriert er.

Der Handler protokolliert die Ereignis-ID, den Selektortyp und den Grund, nie den Body, eine Signatur oder das Geheimnis. Jeder Prozess hat seinen eigenen Cache; bei mehreren Instanzen legen Sie die Markierungen in einen gemeinsamen Speicher hinter derselben revoke-Methode. Siehe Widerrufscache.

Nächste Schritte