
# 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](/docs/sdk/token-verification) auf und enthält keine eigene Token-Logik.

## Was die Middleware leistet

- **Liest das Token** aus dem `Authorization`-Header mit dem Schema `Bearer` oder `DPoP`. Ein Token in der URL-Query (`?access_token=`) wird immer abgelehnt, da URLs in Logs und `Referer`-Header landen. Eine Anfrage mit mehr als einem `Authorization`-Header oder mehr als einer Anmeldeinformation wird ebenfalls abgelehnt.
- **Prüft DPoP.** Beim Schema `DPoP` werden der `DPoP`-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 Schema `Bearer` gesendet wird, wird abgelehnt, ebenso das Schema `DPoP` mit 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.auth` in Express und Fastify, `request.auth` in Django, `g.auth` in Flask und der Rückgabewert der Dependency in FastAPI.
- **Beantwortet Fehler** mit der standardmäßigen `WWW-Authenticate`-Challenge und einem kleinen `application/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.

```go
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.

```ts
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.

```python
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:

```python
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:

```python
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:

```python
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.

```go
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](/docs/sdk/testing), 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.
