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
- Liest das Token aus dem
Authorization-Header mit dem SchemaBeareroderDPoP. Ein Token in der URL-Query (?access_token=) wird immer abgelehnt, da URLs in Logs undReferer-Header landen. Eine Anfrage mit mehr als einemAuthorization-Header oder mehr als einer Anmeldeinformation wird ebenfalls abgelehnt. - Prüft DPoP. Beim Schema
DPoPwerden derDPoP-Proof-Header, die Request-Methode und die absolute Request-URL (die konfigurierte öffentliche Basis-URL gefolgt vom Request-Pfad) an den Verifier übergeben. Ein DPoP-gebundenes Token, das mit dem SchemaBearergesendet wird, wird abgelehnt, ebenso das SchemaDPoPmit einem Token, das nicht DPoP-gebunden ist. - Prüft die Anforderungen der Route: erforderliche Scopes (alle) und optional Step-up: akzeptable
acr-Werte und ein maximales Alter der Anmeldung (RFC 9470). - Fragt den Widerrufs-Cache, falls Sie einen konfigurieren, ob das Token widerrufen wurde.
- Hängt die verifizierten Claims an die Anfrage an:
protect.ClaimsFrom(ctx)in Go,req.authin Express und Fastify,request.authin Django,g.authin Flask und der Rückgabewert der Dependency in FastAPI. - Beantwortet Fehler mit der standardmäßigen
WWW-Authenticate-Challenge und einem kleinenapplication/problem+json-Body. Die Antwort enthält niemals das Token oder einen Claim-Wert, und die Middleware protokolliert sie auch nicht.
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.