> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Tokens und JWKS: Didit-Authentifizierungs-JWTs prüfen

> Didit-Tokens prüfen: PKCE-Codeaustausch, ES256-Claims, Discovery, JWKS, Schlüsselrotation und Erkennung von Refresh-Token-Wiederverwendung.

> [!WARNING]
>
> Authentifizierung ist neu. Jeder Aufruf auf dieser Seite verwendet **Endbenutzer-Zugangsdaten** oder gar keine. Senden Sie Ihren API-Schlüssel für die Verifizierung oder ein Console-Token niemals an diese Endpoints.

Jede Anwendung ist ein eigener OpenID-Connect-Issuer. Der Issuer ist der Ursprung der Anmeldung Ihrer Anwendung:

* `https://<name>.auth.didit.me` für eine Live-Anwendung,
* `https://<name>.sandbox.auth.didit.me` für eine Sandbox-Anwendung,
* oder Ihre eigene benutzerdefinierte Domain, sobald sie aktiv ist.

Live- und Sandbox-Issuer sind getrennt und haben jeweils eigene Schlüssel. Setzen Sie `DIDIT_AUTH_ISSUER` auf Ihren Issuer ohne abschließenden Schrägstrich und `DIDIT_APPLICATION_ID` auf Ihre Anwendungs-ID: Tokens enthalten sie als `aud`. Ihr Publishable Key kodiert den Issuer: Nach `pk_live_` oder `pk_test_` steht der Issuer als base64url ohne Padding.

> [!NOTE]
>
> Wenn Ihre benutzerdefinierte Domain aktiv wird, ersetzt sie den Standard-Host als Issuer. Neue Tokens nennen sie in `iss`, Discovery verweist `jwks_uri` auf sie, und Ihr Publishable Key ändert sich. Access-Tokens, die unter dem Standard-Host ausgestellt wurden, schlagen bei `GET /session/` fehl, bis sie aktualisiert werden. Aktualisieren Sie `DIDIT_AUTH_ISSUER`, wenn Sie die Domain aktivieren, oder akzeptieren Sie beide Issuer, bis die älteren Tokens innerhalb von 5 Minuten ablaufen.

Die serverseitigen Endpoints auf dieser Seite (Discovery, Schlüssel, Tokens und Sitzung) finden Sie in der [Authentication-API-Referenz](https://docs.didit.me/de/authentication/api-reference/get-openid-configuration.md), die aus [openapi-authentication.json](https://docs.didit.me/openapi-authentication.json) generiert wird. Die gehostete Anmeldeseite ist dort nicht enthalten.

## Discovery und Schlüssel

| Anfrage | Ergebnis |
| - | - |
| `GET {issuer}/.well-known/openid-configuration` | `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` und was der Issuer unterstützt. |
| `GET {issuer}/.well-known/jwks.json` | `{"keys": [...]}`, die öffentlichen Signaturschlüssel. |

Der Issuer unterstützt:

* Response-Type `code` und Response-Mode `query`.
* Grant-Types `authorization_code` und `refresh_token`.
* Ausschließlich die PKCE-Methode `S256`. Es handelt sich um öffentliche Clients: Es gibt kein Client-Secret, und `client_id` ist Ihre Anwendungs-ID.
* Signaturalgorithmus `ES256` (P-256-Schlüssel).
* Scopes `openid`, `email`, `phone`, `profile` und `offline_access`.

Es gibt keinen Userinfo-, Introspection- oder End-Session-Endpoint, und `prompt=none` wird nicht unterstützt. Verwenden Sie [`GET /session/`](#sitzung-prüfen), um eine Sitzung zu lesen und zu prüfen.

### Caching und Rotation

Beide Dokumente liefern `Cache-Control: public, max-age=300`, sodass Sie und gemeinsam genutzte Caches sie 5 Minuten lang behalten können. Pro IP-Adresse sind 1.200 Anfragen pro Minute und 12.000 pro Stunde erlaubt. Das liegt weit über den Anmelde-Endpoints, weil sich viele Backend-Instanzen oft eine Adresse teilen.

* Jeder Schlüssel hat eine `kid`; neue Schlüssel-IDs beginnen mit `k_`. Jedes Token nennt seinen Schlüssel im `kid`-Header.
* Schlüssel rotieren alle 30 Tage. Didit veröffentlicht den nächsten Schlüssel einen Tag, bevor er mit dem Signieren beginnt, und hält den vorherigen Schlüssel einen Tag lang veröffentlicht, nachdem er nicht mehr signiert. Ein 5 Minuten lang gecachtes JWKS ermöglicht daher auch während einer Rotation weiterhin eine erfolgreiche Verifizierung.
* Cachen Sie Schlüssel nach `kid`. Nennt ein Token eine `kid`, die Sie nicht haben, laden Sie das JWKS erneut, höchstens einmal pro Minute, und weisen Sie das Token zurück, wenn der Schlüssel weiterhin fehlt.
* Wenn Didit einen kompromittierten Schlüssel widerruft, entfernt es den Schlüssel sofort aus dem JWKS, und `GET /session/` weist jedes Token zurück, das mit diesem Schlüssel signiert wurde. Ein Ersatzschlüssel beginnt sofort mit dem Signieren, ohne die eintägige Vorabveröffentlichung. Ein bis zu 5 Minuten früher gecachtes JWKS akzeptiert den widerrufenen Schlüssel noch und kennt den Ersatzschlüssel noch nicht. Neue Tokens können deshalb bei der Offline-Verifizierung fehlschlagen, bis der Cache aktualisiert wird.

## Anmeldung starten

Öffnen Sie die gehostete Seite, also den `authorization_endpoint`:

```text theme={null}
{issuer}/sign-in?redirect_uri=...&state=...&code_challenge=...&code_challenge_method=S256
```

| Parameter | Regel |
| - | - |
| `redirect_uri` | Muss exakt mit einer Ihrer [registrierten Redirect-URLs](https://docs.didit.me/de/authentication/overview.md#weiterleitungs-urls) übereinstimmen. |
| `state` | Bis zu 512 Zeichen. Prüfen Sie ihn im Callback. |
| `code_challenge` | Der SHA-256 Ihres PKCE-Verifiers als base64url: genau 43 Zeichen. |
| `code_challenge_method` | `S256`. |
| `client_id` | Optional. Muss Ihrer Anwendungs-ID entsprechen. |
| `nonce` | Optional. Wird in das ID-Token kopiert, damit Sie ihn an diese Anmeldung binden können. |
| `scope`, `login_hint`, `ui_locales`, `max_age`, `prompt` | Optional. `login_hint` füllt nur das E-Mail-Feld voraus. |

Die [Schnellstarts](https://docs.didit.me/de/authentication/quickstarts/javascript.md) erstellen diese Anfrage für Sie.

## Code einlösen

Der Callback erhält `code` und `state`. Prüfen Sie `state` und lösen Sie den Code dann innerhalb von 60 Sekunden ein. Jeder Code funktioniert nur einmal. Der Body kann JSON oder form-kodiert sein.

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/token/" \
  -H 'Content-Type: application/json' \
  -d '{
    "grant_type": "authorization_code",
    "code": "CODE_FROM_CALLBACK",
    "code_verifier": "YOUR_PKCE_VERIFIER",
    "redirect_uri": "https://app.example.com/auth/callback"
  }'
```

```json theme={null}
{
  "access_token": "eyJhbGciOiJFUzI1NiIs...",
  "id_token": "eyJhbGciOiJFUzI1NiIs...",
  "refresh_token": "N4yZTl6y24YKYq77X_SO...",
  "token_type": "Bearer",
  "expires_in": 299,
  "session_id": "c9abf6cc-2d64-4bf2-b31e-a4d09a070ab1"
}
```

`expires_in` gibt an, wie viele Sekunden das Access-Token noch gültig bleibt. Wenn Sie mehrere Sitzungen deaktivieren, beendet dieser Austausch die anderen Sitzungen der Person.

| Antwort | Bedeutung |
| - | - |
| `400 {"error": "invalid_grant"}` | Der Code ist unbekannt, bereits verwendet oder abgelaufen; der Verifier oder `redirect_uri` stimmt nicht überein, oder die `redirect_uri` ist nicht mehr registriert; die Person ist gesperrt oder ihre E-Mail-Adresse ist nicht mehr verifiziert; oder Sie haben die von ihr verwendete Anmeldemethode deaktiviert. |
| `400 {"error": "invalid_client"}` | `client_id` ist nicht Ihre Anwendungs-ID. |
| `400 {"error": "mfa_required"}` | Die Anmeldung entsprach nicht Ihrer MFA-Richtlinie. |
| `400 {"<field>": [...]}` | Ein Feld fehlt oder ist ungültig, zum Beispiel `{"non_field_errors": ["invalid_request"]}`, wenn einem Grant seine Pflichtfelder fehlen. |
| `400 {"<field>": "unknown_field"}` | Der Body enthält ein Feld, das der Endpoint nicht akzeptiert, zum Beispiel `client_secret`. Öffentliche Clients senden keines. |

## Access-Token-Claims

Das Access-Token ist ein JWT mit dem Header `alg: ES256` und `typ: at+jwt`. Es ist höchstens 5 Minuten gültig und nie länger als seine Sitzung.

| Claim | Bedeutung |
| - | - |
| `iss` | Ihr Aussteller. |
| `aud` | Ihre Anwendungs-ID. |
| `client_id` | Ihre Anwendungs-ID. |
| `sub` | Die Identitäts-ID der Person in dieser Anwendung. |
| `sid` | Die Sitzungs-ID. Bei einem Token eines KI-Agenten die ID des Zugriffs, den die Person dem Agenten gewährt hat. |
| `jti`, `iat`, `exp` | Token-ID, Ausstellungszeitpunkt und Ablaufzeitpunkt. |
| `vendor_data` | Die stabile Referenz der Person, die mit Verifizierung und Transaktionsüberwachung geteilt wird. |
| `amr` | Die Anmeldemethode, zum Beispiel `["passkey"]`. |
| `org` | `{"id", "role"}` der frühesten Organisationsmitgliedschaft der Person in Ihrer Anwendung oder `null`, wenn sie keine hat oder Organisationen deaktiviert sind. `GET /session/` listet alle Mitgliedschaften auf. |
| `verified` | Das Verifizierungsniveau und die von Ihnen ausgewählten Claims oder `{}`. Eine Prüfung, die während einer Sitzung genehmigt wurde, erscheint beim nächsten Refresh im Token. |
| `act` | `null` für eine Person. Für einen KI-Agenten, der im Auftrag der Person handelt: `{"sub", "agent", "scopes"}`. Siehe [Schritt 5](#token-auf-ihrem-server-prüfen). |

Das ID-Token hat den Header `typ: JWT`. Es enthält dieselben Claims mit Ausnahme von `client_id`, außerdem `auth_time` und `email_verified` sowie `email`, `name` und `phone_number`, sofern sie einen Wert haben. Nur das beim Codeaustausch ausgegebene ID-Token enthält `nonce`, wenn die Anmeldung einen hatte: Prüfen Sie ihn dort, denn ID-Tokens aus einem Refresh enthalten ihn nie. Ein ID-Token wird niemals als Access-Token akzeptiert.

## Token auf Ihrem Server prüfen

Prüfen Sie alle diese Punkte, bevor Sie einem Claim vertrauen:

1. `alg` ist `ES256` und `typ` ist `at+jwt`. Lehnen Sie jeden anderen Algorithmus ab, auch `none`.
2. Der `kid`-Wert ist im JWKS Ihrer Anwendung enthalten, und die Signatur wird verifiziert.
3. `iss` entspricht Ihrem konfigurierten Aussteller, und `aud` entspricht Ihrer Anwendungs-ID.
4. `exp` und `iat` sind gültig, mit einer Uhrzeitabweichung von bis zu 60 Sekunden, und `sub`, `sid` und `jti` sind vorhanden.
5. `act` ist `null`. Das Access-Token eines KI-Agenten wird mit demselben Schlüssel signiert und trägt denselben `typ`, `iss` und `aud`; nur `act` unterscheidet es. Lehnen Sie es ab, sofern der Endpoint keine Agenten akzeptiert. Ist das der Fall, erlauben Sie nur, was `act.scopes` gewährt. `GET /session/` lehnt das Token eines Agenten immer ab.

Nehmen Sie den Aussteller aus Ihrer Konfiguration, niemals aus dem Token.

```javascript Node.js theme={null}
// npm install jose
import { createRemoteJWKSet, jwtVerify } from "jose";

const ISSUER = process.env.DIDIT_AUTH_ISSUER; // https://<name>.auth.didit.me
const AUDIENCE = process.env.DIDIT_APPLICATION_ID;
// Cache keys for 5 minutes; refetch an unknown kid at most once a minute.
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`), {
  cacheMaxAge: 5 * 60 * 1000,
  cooldownDuration: 60 * 1000,
});

export async function verifyAccessToken(token) {
  const { payload } = await jwtVerify(token, jwks, {
    issuer: ISSUER,
    audience: AUDIENCE,
    algorithms: ["ES256"],
    typ: "at+jwt",
    clockTolerance: 60,
    maxTokenAge: "5m", // also rejects an iat in the future
    requiredClaims: ["sub", "sid", "jti", "exp", "iat"],
  });
  if (payload.act !== null) throw new Error("agent tokens are not accepted");
  return payload; // payload.vendor_data is the person across Didit products
}
```

```python Python theme={null}
# pip install "pyjwt[crypto]"
import os
import threading
import time

import jwt

ISSUER = os.environ["DIDIT_AUTH_ISSUER"]  # https://<name>.auth.didit.me
AUDIENCE = os.environ["DIDIT_APPLICATION_ID"]
jwks = jwt.PyJWKClient(f"{ISSUER}/.well-known/jwks.json", lifespan=300)
refresh_lock = threading.Lock()
last_refresh = 0.0


def signing_key(kid: str | None) -> jwt.PyJWK:
    # PyJWKClient refetches for every unknown kid: limit it to once a minute.
    global last_refresh
    keys = {key.key_id: key for key in jwks.get_signing_keys()}
    if kid not in keys:
        with refresh_lock:
            if time.monotonic() - last_refresh >= 60:
                last_refresh = time.monotonic()
                fresh = jwks.get_signing_keys(refresh=True)
                keys = {key.key_id: key for key in fresh}
    if kid not in keys:
        raise jwt.InvalidTokenError("unknown signing key")
    return keys[kid]


def verify_access_token(token: str) -> dict:
    header = jwt.get_unverified_header(token)
    if header.get("typ") != "at+jwt":
        raise jwt.InvalidTokenError("not an access token")
    claims = jwt.decode(
        token,
        signing_key(header.get("kid")).key,
        algorithms=["ES256"],
        audience=AUDIENCE,
        issuer=ISSUER,
        leeway=60,
        options={"require": ["iss", "aud", "sub", "sid", "jti", "exp", "iat"]},
    )
    if "act" not in claims or claims["act"] is not None:
        raise jwt.InvalidTokenError("agent tokens are not accepted")
    return claims
```

Die Offline-Prüfung kann eine Sitzung, die gerade eben beendet wurde, nicht erkennen. Wenn das wichtig ist, etwa vor einer Auszahlung, rufen Sie `GET /session/` auf.

## Sitzung prüfen

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/session/" \
  -H "Authorization: Bearer $DIDIT_ACCESS_TOKEN"
```

Die Antwort enthält `sub`, `sid`, `application_id`, `vendor_data`, `amr`, `mfa_verified`, `email`, `email_verified`, `phone`, `verified_identity`, `verified`, `expires_at`, `post_logout_redirect_uri` und `organizations`. Jeder Organisationseintrag enthält `organization_id`, `organization__name`, `role` und `custom_role`.

Diese Prüfung ist strenger als die Offline-Prüfung. Sie schlägt auch fehl, wenn die Sitzung widerrufen wurde, wegen Inaktivität abgelaufen ist oder ihre Gültigkeitsdauer überschritten hat, wenn die Person gesperrt wurde oder ihre E-Mail-Adresse nicht mehr verifiziert ist, wenn die Sitzung Ihrer MFA-Richtlinie nicht mehr entspricht (zum Beispiel nachdem die Person eine Authenticator-App hinzugefügt hat oder Organisationsadministrator geworden ist) oder wenn das Token durch einen Refresh ersetzt wurde. Ein Fehler liefert `401 {"detail": "invalid_token"}` mit `WWW-Authenticate: Bearer`.

`GET /session/` erlaubt 30 Anfragen pro Minute und 300 pro Stunde und IP-Adresse, ein Limit, das es mit `DELETE /session/` teilt. Prüfen Sie Tokens bei jeder Anfrage offline und nutzen Sie diesen Aufruf nur in Momenten, die ihn erfordern. Jeder erfolgreiche Aufruf zählt als Aktivität und startet daher das `idle_timeout` der Sitzung neu.

Eine verifizierte E-Mail-Adresse ist keine Identitätsverifizierung. Lesen Sie dafür `verified`: Dort ist der Wert `null`, wenn keine vorliegt, während die Tokens `{}` enthalten.

## Refresh-Tokens

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/token/" \
  -H 'Content-Type: application/json' \
  -d '{"grant_type": "refresh_token", "refresh_token": "YOUR_REFRESH_TOKEN"}'
```

Ein Refresh liefert ein neues Access-Token, ein neues ID-Token und ein neues Refresh-Token, und das gesendete Refresh-Token funktioniert nicht mehr.

* Das Refresh-Token ist eine zufällige Zeichenfolge, die bis zum Ende der Sitzung gültig ist.
* **Eine Wiederverwendung beendet die Sitzung.** Wenn ein bereits verwendetes Refresh-Token erneut eingeht, wertet Didit das als Hinweis auf Diebstahl: Es widerruft die Sitzung und liefert `400 {"error": "invalid_grant"}`. Auch die Tokens der Sitzung funktionieren nicht mehr, einschließlich derer aus dem früheren Refresh: Deren Refresh-Token schlägt fehl, und deren Access-Token schlägt sofort bei `GET /session/` fehl. Offline lässt es sich noch bis zu seinem Ablauf verifizieren, also innerhalb von 5 Minuten.
* Führen Sie für eine Sitzung jeweils nur einen Refresh aus, über Tabs, Geräte und Serverinstanzen hinweg, und ersetzen Sie alle drei Tokens gemeinsam.
* Schlägt ein Refresh mit `400` fehl oder bleibt eine Antwort aus, starten Sie die Anmeldung neu. Versuchen Sie es nicht blind erneut: Ein Wiederholungsversuch nach einem erfolgreichen Refresh ist eine Wiederverwendung.
* Ein `503 {"error": "temporarily_unavailable"}` bedeutet, dass der Refresh nicht stattgefunden hat und Ihr Refresh-Token weiterhin funktioniert. Versuchen Sie es mit Backoff erneut.
* Didit antwortet mit `429`, bevor der Refresh ausgeführt wird. Wiederholen Sie ihn daher nach `Retry-After`. Der Token-Endpoint erlaubt 30 Anfragen pro Minute und 300 pro Stunde und IP-Adresse. Zusätzlich teilen sich alle `POST`- und `DELETE`-Anfragen Ihrer Anwendung Bot-Schutz-Kontingente: 60 Anfragen pro Minute und IP-Adresse über diese Endpoints hinweg und 30 pro Minute und Gerät. Für die gesamte Anwendung gibt es für Refreshes ein Kontingent von 6.000 pro Minute, und jedes andere `POST` und `DELETE` (einschließlich Browser und Anmeldeschritte) ein Kontingent von 3.000 pro Minute. So verbrauchen Refreshes nie Ihr Kontingent für Anmeldungen.
* Bei 5-Minuten-Access-Tokens decken 300 Refreshes pro Stunde etwa 25 Personen ab, die aktiv bleiben. Wenn Ihr Server für viele Personen von einer Adresse aus refresht, refreshen Sie nur, wenn eine Anfrage ein neues Token benötigt, und führen Sie Refreshes möglichst im Browser oder in der App durch.

## Abmelden

`DELETE /session/` mit dem aktuellen Access-Token der Person beendet die Sitzung und gibt `204` zurück. Das zugehörige Refresh-Token funktioniert danach nicht mehr, und ihre Access-Tokens schlagen bei `GET /session/` sofort fehl. Tokens, die Sie offline prüfen, bleiben bis zu ihrem Ablauf gültig. Anschließend leiten Sie die Person auf Ihre `post_logout_redirect_uri` weiter, sofern Sie eine gesetzt haben.

Für die Abmeldung benötigen Sie ein Access-Token, das `GET /session/` akzeptieren würde: eines, das nicht abgelaufen ist und nicht durch ein Refresh ersetzt wurde. Ein `401` bedeutet, dass die Sitzung nicht beendet wurde. Führen Sie in diesem Fall ein Refresh durch und melden Sie sich dann mit dem neuen Access-Token ab. Schlägt das Refresh mit `400` fehl, kann die Sitzung nicht mehr verwendet werden.

Eine Sitzung endet außerdem, wenn:

* sie `session_lifetime` (Standard 30 Tage) oder `idle_timeout` (Standard 1 Tag) überschreitet,
* sich die Person erneut anmeldet, während die Option für mehrere Sitzungen deaktiviert ist,
* die Person ihr Passwort zurücksetzt, während `revoke_on_password_change` aktiviert ist,
* Ihr Team die Sitzung in der Konsole widerruft oder die Person dort sperrt,
* ein bereits verwendetes Refresh-Token erneut gesendet wird.

## Lebensdauer

| Element | Lebensdauer |
| - | - |
| Anmeldetransaktion (`POST /authorize/`) | 10 Minuten |
| Autorisierungscode | 60 Sekunden, einmalig verwendbar |
| Access-Token | höchstens 5 Minuten |
| ID-Token | Dieselbe `exp` wie das zusammen mit ihm ausgestellte Access-Token |
| Refresh-Token | Bis zum Ende der Sitzung, einmalig verwendbar |
| Sitzung | `session_lifetime` und `idle_timeout` |
| Step-up | 30 Minuten |
| Cache für Discovery und JWKS | 5 Minuten |

## Zugangsdaten je nach Zweck speichern

* Bewahren Sie Verifizierungs-API-Schlüssel und Console-Tokens auf Ihrem Server auf.
* Bewahren Sie Access-Tokens für den Browser nach Möglichkeit im Arbeitsspeicher auf. Bei serverseitig gerenderten Anwendungen verwenden Sie sichere Cookies mit HttpOnly-Flag und schützen Anfragen mit Cookie-Authentifizierung vor CSRF.
* Speichern Sie Refresh-Tokens für mobile Apps in einem plattformgeschützten Speicher. Löschen Sie den lokalen Zustand nach der Abmeldung.
* Setzen Sie Tokens niemals in URLs oder Logs.

Fahren Sie mit dem [JavaScript-Quickstart](https://docs.didit.me/de/authentication/quickstarts/javascript.md), dem [Next.js-Quickstart](https://docs.didit.me/de/authentication/quickstarts/nextjs.md) oder der [Authentifizierungs-API-Referenz](https://docs.didit.me/de/authentication/api-reference/get-openid-configuration.md) fort.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.