Schutz Ihrer API mit Middleware

Jedes Server-SDK enthält eine schlanke Middleware, die Ihre API mit KeenAuth-Zugriffstoken schützt: net/http in Go, Express und Fastify in Node.js sowie Django, Flask und FastAPI in Python. Sie setzt auf dem Token-Verifier auf und enthält keine eigene Token-Logik.

Was die Middleware leistet

Go (net/http)

Das Paket protect verwendet nur die Standardbibliothek und funktioniert mit jedem Router, der einen http.Handler akzeptiert.

import (
	"github.com/keenauth/keenauth-go/protect"
	"github.com/keenauth/keenauth-go/verify"
)

v, err := verify.New(verify.Config{
	Issuer:   "https://acme.eu.keenauth.app",
	Audience: "https://api.acme.example",
})
if err != nil {
	return err
}
m, err := protect.New(v, protect.Options{
	PublicBaseURL:       "https://api.acme.example",
	ResourceMetadataURL: "https://api.acme.example/.well-known/oauth-protected-resource",
})
if err != nil {
	return err
}

mux := http.NewServeMux()
mux.Handle("GET /orders", m.Require(protect.Requirement{Scopes: []string{"orders:read"}})(http.HandlerFunc(
	func(w http.ResponseWriter, r *http.Request) {
		claims, _ := protect.ClaimsFrom(r.Context())
		fmt.Fprintln(w, claims.Subject)
	})))

age := 10 * time.Minute
mux.Handle("POST /payouts", m.Require(protect.Requirement{
	Scopes:        []string{"payouts:write"},
	AcceptableACR: []string{"urn:idp:acr:mfa"},
	MaxAuthAge:    &age,
})(payoutsHandler))

m.Handler(next) schützt einen Handler mit einem gültigen Token ohne weitere Anforderung. Require löst beim Start eine Panic aus, wenn ein Scope kein gültiger Scope-Token ist, so wie http.ServeMux bei einem ungültigen Muster.

Node.js (Express und Fastify)

Die Middleware befindet sich im Subpfad @keenauth/node/middleware. Express und Fastify sind keine Abhängigkeiten des SDK.

import { createVerifier } from "@keenauth/node";
import { createProtector, expressAuth, fastifyAuth } from "@keenauth/node/middleware";

const verifier = createVerifier({ issuer: "https://acme.eu.keenauth.app", audience: "https://api.acme.example" });
const protector = createProtector(verifier, { publicBaseUrl: "https://api.acme.example" });

// Express
app.get("/orders", expressAuth(protector, { scopes: ["orders:read"] }), (req, res) => {
  res.json({ sub: req.auth.subject });
});

// Fastify
fastify.decorateRequest("auth", null);
fastify.get("/orders", { preHandler: fastifyAuth(protector, { scopes: ["orders:read"] }) }, async (request) => {
  return { sub: request.auth.subject };
});

Step-up verwendet { acceptableAcr: ["urn:idp:acr:mfa"], maxAuthAgeSeconds: 600 }. In TypeScript typisieren Sie die Anfrage als AuthenticatedRequest (vom Subpfad exportiert), um req.auth zu lesen.

Python (Django, Flask, FastAPI)

Installieren Sie das Extra für Ihr Framework: pip install "keenauth-python[django]", "keenauth-python[flask]" oder "keenauth-python[fastapi]". Das Framework wird nur von seinem eigenen Adaptermodul importiert.

from keenauth.middleware import Protector
from keenauth.verify import Verifier, VerifierConfig

verifier = Verifier(VerifierConfig(issuer="https://acme.eu.keenauth.app", audience="https://api.acme.example"))
protector = Protector(verifier, public_base_url="https://api.acme.example")

Django:

from django.http import JsonResponse
from keenauth.middleware.django import protect

@protect(protector, scopes=["orders:read"])
def orders(request):
    return JsonResponse({"sub": request.auth.subject})

Flask:

from flask import g
from keenauth.middleware.flask import protect

@app.get("/orders")
@protect(protector, scopes=["orders:read"])
def orders():
    return {"sub": g.auth.subject}

FastAPI:

from fastapi import Depends, FastAPI
from keenauth.middleware.fastapi import RequireAuth, install
from keenauth.verify import AccessClaims

app = FastAPI()
install(app)

@app.get("/orders")
def orders(claims: AccessClaims = Depends(RequireAuth(protector, scopes=["orders:read"]))):
    return {"sub": claims.subject}

install(app) registriert den Handler, der den Problem-Body und pro Challenge einen WWW-Authenticate-Header schreibt. Step-up verwendet acceptable_acr=["urn:idp:acr:mfa"], max_auth_age=timedelta(minutes=10).

Die öffentliche Basis-URL (erforderlich)

Ein DPoP-Proof nennt die URL, die der Client aufgerufen hat (htu). Die Middleware vergleicht sie mit der öffentlichen Basis-URL, die Sie konfigurieren (PublicBaseURL, publicBaseUrl, public_base_url), gefolgt vom Request-Pfad. Setzen Sie sie auf die Adresse, die Clients verwenden, zum Beispiel https://api.acme.example oder https://acme.example/api. Sie ist erforderlich: Die Erstellung der Middleware ohne sie schlägt beim Start mit einem Konfigurationsfehler fehl, selbst wenn Ihre Clients nur Bearer-Token senden.

Die Middleware erstellt diese URL niemals aus dem Host-Header der Anfrage oder aus X-Forwarded-*-Headern, und in Python verwendet sie nicht die eigene Request-URL des Frameworks, selbst wenn Framework oder Server so konfiguriert sind, dass sie weitergeleiteten Headern vertrauen. Die Clients bestimmen diese Werte: Wird Ihre API unter zwei Hostnamen bereitgestellt, könnte ein für einen der beiden erstellter Proof sonst gegen den anderen wiederverwendet werden. Hinter einem Load Balancer oder Reverse Proxy teilt die öffentliche Basis-URL der Middleware außerdem Schema, Host und Pfadpräfix mit, die die Clients sehen. Die öffentliche Basis-URL muss https verwenden, außer für localhost, 127.0.0.1 und [::1] (RFC 6750 §5.3); Schema und Host werden ohne Berücksichtigung der Groß-/Kleinschreibung verglichen, und ein Standard-Port (:443, :80) wird ignoriert, sowohl in der Basis-URL als auch im htu des Proofs.

Antworten

Situation Status WWW-Authenticate code
Kein Token oder ein nicht unterstütztes Schema 401 Bearer und DPoP algs="…", ohne Fehler missing_token
Token in der Query, wiederholter oder fehlerhafter Authorization-Header 400 Bearer error="invalid_request" invalid_request
Ungültiges, abgelaufenes oder widerrufenes Token 401 error="invalid_token" invalid_token
Fehlender, wiederholter oder ungültiger DPoP-Proof 401 DPoP error="invalid_dpop_proof" invalid_dpop_proof
Ein erforderlicher Scope fehlt 403 error="insufficient_scope", scope="…" insufficient_scope
Step-up erforderlich 401 error="insufficient_user_authentication", acr_values="…", max_age="…" insufficient_user_authentication
Signaturschlüssel oder Widerrufsstatus nicht verfügbar 503 keine jwks_unavailable, revocation_unavailable

Die Challenge verwendet das Schema, das der Client verwendet hat. Wenn Sie die URL der Ressourcenmetadaten festlegen, enthält jede 401-Challenge resource_metadata (RFC 9728). Der Body enthält type, title, status, code und ein festes detail; er gibt niemals an, warum ein Token ungültig war. Für Ihre Logs übergeben Sie einen Fehler-Callback (OnError, onError, on_error): Er erhält den Fehler des Verifiers, der kein Token oder keinen Claim-Wert enthält. Protokollieren Sie die Header Authorization oder DPoP nicht selbst.

Widerrufs-Cache (token.revoked)

Ein JWT-Zugriffstoken bleibt bis zu seinem Ablauf gültig. Um widerrufene Token früher abzulehnen, verarbeiten Sie den Webhook token.revoked und speichern dessen Selector in einem Widerrufs-Cache, den die Middleware abfragt. Die SDKs enthalten einen In-Memory-Cache: protect.NewMemoryRevocationCache (Go), createMemoryRevocationCache (Node.js) und MemoryRevocationCache (Python).

Ein Selector nennt eine Token-ID (jti), eine Sitzung (sid), einen Benutzer (sub), eine Organisation (org_id), eine Anwendung (client_id) oder die Paare aus Organisation und Benutzer sowie aus Anwendung und Benutzer. Ein jti-Selector widerruft genau dieses Token. Die anderen Selectoren widerrufen alle Token, die vor dem not_before des Ereignisses ausgestellt wurden; später ausgestellte Token werden akzeptiert. Markierungen werden standardmäßig 24 Stunden und 1 Minute aufbewahrt, also die längstmögliche Lebensdauer eines Zugriffstokens plus Uhrabweichung.

cache := protect.NewMemoryRevocationCache(0, nil)
m, _ := protect.New(v, protect.Options{PublicBaseURL: "https://api.acme.example", Revocation: cache})

// In Ihrem verifizierten Handler für token.revoked:
err := cache.Revoke(protect.Selector{SessionID: "ses_…"}, notBefore)

Der In-Memory-Cache gilt nur für einen Prozess. Läuft Ihre API auf mehreren Instanzen, implementieren Sie die Schnittstelle des Widerrufs-Checkers über einen gemeinsamen Speicher. Wenn der Cache keine Antwort geben kann, lehnt die Middleware die Anfrage mit 503 ab.

Testen

Verwenden Sie das Offline-Testkit, um Token für Ihre Middleware-Tests zu erstellen. Das Kit enthält keinen Helfer für DPoP-Proofs; signieren Sie einen Proof in Ihrem Test mit der Krypto-Bibliothek der jeweiligen Sprache.