
# Token in Ihrer API verifizieren

Der Token-Verifier des SDK prüft Access-Tokens und ID-Tokens, die von Ihrer KeenAuth-Umgebung ausgestellt wurden. Verwenden Sie ihn in der API, die Sie schützen. Alle drei SDKs (Go, TypeScript und Python) enthalten ihn, und sie folgen denselben Regeln und denselben Testvektoren.

## Go

```go
import "github.com/keenauth/keenauth-go/verify"

v, err := verify.New(verify.Config{
	Issuer:   "https://acme.eu.keenauth.app", // your environment's issuer, exactly
	Audience: "https://api.acme.example",     // the identifier of your API
})
if err != nil {
	return err
}

// In your HTTP handler:
claims, err := v.VerifyAccessToken(r.Context(), token, &verify.DPoPInput{
	Proof:  r.Header.Get("DPoP"),
	Method: r.Method,
	URL:    "https://api.acme.example" + r.URL.Path,
})
if err != nil {
	w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
	http.Error(w, "unauthorized", http.StatusUnauthorized)
	return
}
// claims.Subject, claims.ClientID, claims.Scope
```

Für ID-Tokens setzen Sie `ClientID` in der Konfiguration und rufen `v.VerifyIDToken(ctx, idToken, verify.IDTokenOptions{Nonce: nonce})` auf.

## TypeScript

Der TypeScript-Verifier verwendet nur das WebCrypto und `fetch` der Plattform (Node.js 22 oder neuer) und hat keine Abhängigkeiten.

```ts
import { createVerifier, VerifyError } from "@keenauth/node";

const verifier = createVerifier({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  audience: "https://api.acme.example",   // the identifier of your API
});

// In your HTTP handler:
try {
  const claims = await verifier.verifyAccessToken(token, {
    proof: req.get("DPoP") ?? "",
    method: req.method,
    url: "https://api.acme.example" + req.path,
  });
  // claims.subject, claims.clientId, claims.scope
} catch (e) {
  if (!(e instanceof VerifyError)) throw e;
  res.set("WWW-Authenticate", 'Bearer error="invalid_token"').status(401).send("unauthorized");
}
```

Für ID-Tokens setzen Sie `clientId` in der Konfiguration und rufen `verifier.verifyIdToken(idToken, { nonce })` auf.

## Python

Der Python-Verifier benötigt das Paket `cryptography`, das mit dem Extra `verify` installiert wird: `pip install "keenauth-python[verify]"`. Die Aufrufe sind synchron und threadsicher.

```python
from keenauth.verify import DPoPInput, Verifier, VerifierConfig, VerifyError

verifier = Verifier(VerifierConfig(
    issuer="https://acme.eu.keenauth.app",  # your environment's issuer, exactly
    audience="https://api.acme.example",    # the identifier of your API
))

# In your HTTP handler:
try:
    claims = verifier.verify_access_token(
        token,
        DPoPInput(
            proof=request.headers.get("DPoP", ""),
            method=request.method,
            url="https://api.acme.example" + request.path,
        ),
    )
    # claims.subject, claims.client_id, claims.scope
except VerifyError:
    return Response("unauthorized", status=401, headers={"WWW-Authenticate": 'Bearer error="invalid_token"'})
```

Für ID-Tokens setzen Sie `client_id` in der Konfiguration und rufen `verifier.verify_id_token(id_token, nonce=nonce)` auf.

## Step-up (RFC 9470)

Um eine sensible Operation zu schützen, verlangen Sie pro Aufruf eine stärkere oder aktuellere Anmeldung. Übergeben Sie die akzeptablen Authentication-Context-Class-References (`acr`) und das maximale Alter der Anmeldung (`auth_time`). `dpop` unten ist die DPoP-Eingabe aus den obigen Beispielen (`nil`/`undefined`/`None`, wenn Sie DPoP nicht verwenden).

### Go

```go
age := 15 * time.Minute
claims, err := v.VerifyAccessToken(r.Context(), token, dpop, verify.AccessTokenOptions{
	AcceptableACR: []string{"urn:idp:acr:mfa", "urn:idp:acr:phr"},
	MaxAuthAge:    &age,
})
if challenge, ok := verify.StepUpChallenge(err); ok {
	w.Header().Set("WWW-Authenticate", challenge)
	http.Error(w, "step-up required", http.StatusUnauthorized)
	return
}
```

### TypeScript

```ts
import { createVerifier, stepUpChallenge } from "@keenauth/node";

try {
  const claims = await verifier.verifyAccessToken(token, dpop, {
    acceptableAcr: ["urn:idp:acr:mfa", "urn:idp:acr:phr"],
    maxAuthAgeSeconds: 900,
  });
} catch (e) {
  const challenge = stepUpChallenge(e);
  if (challenge === undefined) throw e;
  res.set("WWW-Authenticate", challenge).status(401).send("step-up required");
}
```

### Python

```python
from datetime import timedelta
from keenauth.verify import VerifyError, step_up_challenge

try:
    claims = verifier.verify_access_token(
        token,
        dpop,
        acceptable_acr=["urn:idp:acr:mfa", "urn:idp:acr:phr"],
        max_auth_age=timedelta(minutes=15),
    )
except VerifyError as err:
    challenge = step_up_challenge(err)
    if challenge is None:
        raise
    return Response("step-up required", status=401, headers={"WWW-Authenticate": challenge})
```

Die Prüfungen laufen nach allen anderen Prüfungen, daher ist ein ungültiges Token immer `invalid_token`. Ein gültiges Token, das die Anforderung nicht erfüllt, erhält den Code `insufficient_user_authentication`, und der Fehler enthält die geforderten `acr_values` und `max_age` (nur die, die Sie gesetzt haben). `StepUpChallenge` (Go), `stepUpChallenge` (TypeScript) und `step_up_challenge` (Python) wandeln ihn in den Wert des Headers `WWW-Authenticate` gemäß RFC 9470 §3 um, zum Beispiel `Bearer error="insufficient_user_authentication", error_description="authentication too old", max_age="900"`. Ein Client, der die Challenge versteht, startet eine neue Autorisierungsanfrage mit diesen `acr_values` und `max_age`. `acr` muss exakt einem der Werte entsprechen (Groß-/Kleinschreibung wird beachtet); `auth_time` ist erforderlich, wenn Sie ein maximales Alter setzen, und die Toleranz für Uhrenabweichung gilt wie bei ID-Tokens. Die Werte `urn:idp:acr:1fa`, `urn:idp:acr:mfa` und `urn:idp:acr:phr` sind diejenigen, die Ihre Umgebung in `acr_values_supported` veröffentlicht.

## DPoP und das Authorization-Schema (RFC 9449)

Ein DPoP-gebundenes Token (`cnf.jkt`) ist ohne den privaten Schlüssel seines Clients wertlos, sodass eine gestohlene Kopie nicht wiederverwendet werden kann. Zwei Regeln sichern dies ab, und der Verifier erzwingt beide, wenn Sie das Schema des `Authorization`-Headers der Anfrage übergeben (`Bearer` oder `DPoP`):

- Ein DPoP-gebundenes Token, das mit dem Schema `Bearer` gesendet wird, wird mit `invalid_token` abgelehnt, selbst wenn ein gültiger Proof beiliegt (RFC 9449 §7.2).
- Ein Token, das mit dem Schema `DPoP` gesendet wird, muss DPoP-gebunden sein und einen gültigen Proof mitbringen (RFC 9449 §7.1).

Um auf einer API nur sender-constrained Tokens zu akzeptieren (empfohlen für sensible APIs), setzen Sie zusätzlich "require DPoP": Ein Token, das nicht DPoP-gebunden ist, erhält den Code `dpop_required`. Der DPoP-Challenge-Helper wandelt jeden `401`-Fehler in den Wert `WWW-Authenticate` gemäß RFC 9449 §7.1 um, zum Beispiel `DPoP error="invalid_token", error_description="a DPoP-bound access token is required", algs="ES256 EdDSA PS256 RS256"`.

### Go

```go
claims, err := v.VerifyAccessToken(r.Context(), token, dpop, verify.AccessTokenOptions{
	Scheme:      scheme, // "Bearer" or "DPoP", from the Authorization header
	RequireDPoP: true,
})
if challenge, ok := verify.DPoPChallenge(err); ok {
	w.Header().Set("WWW-Authenticate", challenge)
	http.Error(w, "unauthorized", http.StatusUnauthorized)
	return
}
```

### TypeScript

```ts
import { dpopChallenge } from "@keenauth/node";

try {
  const claims = await verifier.verifyAccessToken(token, dpop, { scheme, requireDPoP: true });
} catch (e) {
  const challenge = dpopChallenge(e);
  if (challenge === undefined) throw e;
  res.set("WWW-Authenticate", challenge).status(401).send("unauthorized");
}
```

### Python

```python
from keenauth.verify import VerifyError, dpop_challenge

try:
    claims = verifier.verify_access_token(token, dpop, scheme=scheme, require_dpop=True)
except VerifyError as err:
    challenge = dpop_challenge(err)
    if challenge is None:
        raise
    return Response("unauthorized", status=401, headers={"WWW-Authenticate": challenge})
```

Die Resource-Server-Middleware (Go `protect`, TypeScript `@keenauth/node/middleware`, Python `keenauth.middleware`) liest das Schema für Sie aus. Ihre Einstellung "require DPoP" (pro API oder pro Route) beantwortet eine `Bearer`-Anfrage ausschließlich mit `401` und der DPoP-Challenge.

## Was der Verifier prüft

- **Issuer und Schlüssel:** Das `iss` des Tokens muss dem konfigurierten Issuer entsprechen. Schlüssel werden nur vom `jwks_uri` im Discovery-Dokument dieses Issuers abgerufen, und die `kid` wird nur in diesem Schlüsselsatz gesucht. Ein Token, das mit dem Schlüssel einer anderen Umgebung signiert ist, wird abgelehnt.
- **Algorithmus:** Nur RS256, PS256, ES256 und EdDSA. Das `alg` des Schlüssels muss dem `alg` des Tokens entsprechen; `none` und HMAC-Algorithmen werden immer abgelehnt.
- **Access-Tokens:** `typ` muss `at+jwt` sein (RFC 9068), `aud` muss Ihre konfigurierte Audience enthalten, und `sub` und `client_id` müssen vorhanden sein.
- **ID-Tokens:** `aud` muss Ihre Client-ID enthalten, `azp` muss ihr entsprechen, sofern vorhanden, und `nonce` sowie `max_age` werden geprüft, wenn Sie sie übergeben.
- **Zeit:** `exp` ist erforderlich; `nbf` und `iat` dürfen nicht in der Zukunft liegen. Eine Uhrenabweichung von höchstens 60 Sekunden ist zulässig.
- **DPoP:** Ist das Token an einen Schlüssel gebunden (`cnf.jkt`), ist ein gültiger DPoP-Proof (RFC 9449) erforderlich. Wiederverwendete Proofs werden abgelehnt. Mit der Schema-Option wird ein gebundenes Token nie als Bearer-Token akzeptiert; mit "require DPoP" werden nur gebundene Tokens akzeptiert.
- **Schlüssel-Cache:** Schlüssel werden für das `max-age` des Servers zwischengespeichert (standardmäßig 300 Sekunden, höchstens 600). Eine unbekannte `kid` löst höchstens einen erneuten Abruf pro Minute aus.

## Fehler

Jeder Fehler hat einen `Code`. Die Meldung enthält niemals das Token oder seine Claims.

| Code | Bedeutung | Antworten mit |
|---|---|---|
| `invalid_token` | Das Token ist ungültig, abgelaufen oder nicht für diese API bestimmt. | `401` |
| `jwks_unavailable` | Die Schlüssel des Issuers konnten nicht abgerufen werden, und es sind keine zwischengespeichert. | `503` |
| `dpop_invalid` | Der DPoP-Proof fehlt oder ist ungültig. | `401` mit `WWW-Authenticate` aus dem DPoP-Challenge-Helper |
| `dpop_required` | Sie verlangen DPoP, und das Token ist nicht DPoP-gebunden. | `401` mit `WWW-Authenticate` aus dem DPoP-Challenge-Helper |
| `insufficient_user_authentication` | Das Token ist gültig, aber sein `acr` oder `auth_time` erfüllt Ihre Anforderung nicht (RFC 9470). | `401` mit `WWW-Authenticate` aus dem Step-up-Helper |
| `config_invalid` | Die Konfiguration des Verifiers ist fehlerhaft. | Korrigieren Sie die Konfiguration. |

## Wenn Sie eine andere JWT-Bibliothek verwenden

Wenden Sie dieselbe Checkliste an: Binden Sie den Issuer an sein Discovery-Dokument, beziehen Sie Schlüssel nur von diesem `jwks_uri`, verlangen Sie, dass das `alg` des Schlüssels dem `alg` des Tokens entspricht, und lehnen Sie `none` sowie HMAC-Algorithmen ab.

## Offline testen

Für Unit-Tests und CI verwenden Sie das [Offline-Testkit](/docs/sdk/testing). Es erstellt Test-Tokens, die nur ein Verifier im expliziten Testmodus akzeptiert; ein Produktions-Verifier lehnt sie immer ab.
