
# Eine Django-API schützen

Diese Schnellstartanleitung schützt eine Django-API mit KeenAuth-Zugriffstokens. Sie fügen den `@protect`-View-Dekorator des Python-SDK hinzu, verlangen einen Scope für eine View und testen die API offline mit dem Test-Kit des SDK. Das Beispiel verwendet Django 5.2 und keine Datenbank.

Die API hat drei Endpunkte:

| Endpunkt | Erfordert |
|---|---|
| `GET /api/public` | nichts |
| `GET /api/private` | ein gültiges Zugriffstoken für Ihre API |
| `GET /api/messages` | ein gültiges Zugriffstoken mit dem `messages:read`-Scope |

## Bevor Sie beginnen

- Eine KeenAuth-Umgebung. Ihr Issuer sieht etwa so aus: `https://your-environment.eu.keenauth.app`.
- Eine in der Umgebung registrierte API ([API anlegen](/docs/api/management/apis/create)). Ihr `identifier`, zum Beispiel `https://api.example.com`, ist die Audience der Tokens, die Ihre API akzeptiert.
- Der `messages:read`-Scope für diese API, gewährt der Anwendung, die sie aufruft.
- Python 3.10 oder neuer.

## 1. SDK installieren

```sh
pip install "keenauth-python[django]"
```

Das SDK ist noch nicht veröffentlicht. Bis dahin installiert das Beispiel es aus dem SDK-Quellcode. Das Beispiel fixiert jedes Paket auf eine exakte Version in `requirements.txt`.

## 2. Konfigurieren

`project/settings.py` liest jeden Wert aus Umgebungsvariablen:

| Variable | Erforderlich | Bedeutung |
|---|---|---|
| `DJANGO_SECRET_KEY` | ja | Der Secret Key von Django; erzeugen Sie einen und committen Sie ihn niemals |
| `DJANGO_ALLOWED_HOSTS` | nein | Durch Kommas getrennte Hostnamen, Standard `localhost,127.0.0.1` |
| `KEENAUTH_ISSUER` | ja | Der Issuer Ihrer Umgebung |
| `KEENAUTH_AUDIENCE` | ja | Der Bezeichner Ihrer API |
| `PUBLIC_BASE_URL` | ja | Die URL, die Clients aufrufen (siehe [Hinter einem Proxy](#hinter-einem-proxy)) |
| `KEENAUTH_WEBHOOK_SECRET` | empfohlen | Das `idp_whsec_`-Signaturgeheimnis des Webhook-Endpunkts, der `token.revoked` liefert (zwei, durch Komma getrennt, während Sie es rotieren). Ohne dieses erfährt die API nie von widerrufenen Tokens |

## 3. Ihre Views schützen

`@protect(protector)` verlangt ein gültiges Token und legt die geprüften Claims in `request.auth` ab; `scopes=[...]` verlangt zusätzlich Scopes. Schlägt eine Prüfung fehl, gibt sie eine `application/problem+json`-Antwort zurück, bevor die View ausgeführt wird.

```python
@require_GET
def public(request: HttpRequest) -> JsonResponse:
    # No token needed.
    return JsonResponse({"message": "Hello from a public endpoint."})


@require_GET
@protect(protector)
def private(request: HttpRequest) -> JsonResponse:
    # Any valid access token for this API; the verified claims are in request.auth.
    return JsonResponse({"message": "Hello from a private endpoint.", "sub": request.auth.subject})


@require_GET
@protect(protector, scopes=["messages:read"])
def messages(request: HttpRequest) -> JsonResponse:
    # A valid access token that also carries the messages:read scope.
    return JsonResponse({"messages": ["Hello from a scoped endpoint."]})
```

`api/auth.py` baut den Protector beim Start aus den Einstellungen auf (`ApiConfig.ready` in `api/apps.py`), sodass eine fehlende Einstellung den Server vor der ersten Anfrage stoppt. Der Verifier lädt die Signaturschlüssel beim ersten Token aus Ihrer Umgebung.

```python
@cache
def get_protector() -> Protector:
    if not settings.KEENAUTH_ISSUER or not settings.KEENAUTH_AUDIENCE or not settings.KEENAUTH_PUBLIC_BASE_URL:
        raise ImproperlyConfigured("KEENAUTH_ISSUER, KEENAUTH_AUDIENCE and PUBLIC_BASE_URL are required")
    # The verifier refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
    verifier = Verifier(VerifierConfig(issuer=settings.KEENAUTH_ISSUER, audience=settings.KEENAUTH_AUDIENCE))
    return Protector(
        verifier,
        public_base_url=settings.KEENAUTH_PUBLIC_BASE_URL,
        revocation=revocation,
        # The error never contains the token or claim values; never log the Authorization or DPoP header.
        on_error=lambda err: log.warning("rejected: %s", err),
    )


@cache
def get_webhook() -> Optional[RevocationWebhook]:
    """The token.revoked webhook, or None when KEENAUTH_WEBHOOK_SECRET is not set. A malformed secret raises
    WebhookSecretError (without echoing it), so ApiConfig.ready stops the server at startup."""
    if not settings.KEENAUTH_WEBHOOK_SECRETS:
        return None
    return RevocationWebhook(settings.KEENAUTH_WEBHOOK_SECRETS, revocation)


class ProtectorProxy:
    """Hands each request to `get_protector()`, so the `@protect` decorators can be applied at import time."""

    def authenticate(self, req: RequestInfo, requirement: Optional[Requirement] = None) -> AccessClaims:
        return get_protector().authenticate(req, requirement)


protector = ProtectorProxy()
```

## 4. Starten und die API aufrufen

```sh
export DJANGO_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))')"
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
python manage.py runserver 8080
```

`runserver` ist nur für die Entwicklung gedacht; in Produktion betreiben Sie `project.wsgi` unter einem WSGI-Server.

Für einen ersten Aufruf holen Sie ein Token über den Client-Credentials-Grant einer Machine-to-Machine-Anwendung, die den `messages:read`-Scope für Ihre API besitzt:

```sh
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource=https://api.example.com \
  -d scope=messages:read \
  https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)

curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
```

## 5. Ohne Netzwerk testen

`keenauth.testing` erstellt Tokens, die mit einem öffentlichen Testschlüssel signiert sind, und `testing.verifier` akzeptiert nur diese Tokens. Die Tests ersetzen `get_protector` durch einen Protector, der diesen Verifier verwendet; sie benötigen daher kein Netzwerk, kein KeenAuth-Konto und keinen Testschalter in den Einstellungen. Siehe [Offline-Test-Kit](/docs/sdk/testing).

```python
class ProtectedAPITest(SimpleTestCase):
    def setUp(self) -> None:
        self.cache = MemoryRevocationCache()
        test_protector = Protector(testing.verifier(audience=AUDIENCE), public_base_url=BASE_URL, revocation=self.cache)
        patcher = mock.patch.object(auth, "get_protector", return_value=test_protector)
        patcher.start()
        self.addCleanup(patcher.stop)
```

```python
    def test_scope(self) -> None:
        self.assert_problem(self.get("/api/messages", testing.mint_access_token(AUDIENCE)), 403, "insufficient_scope",
                            'scope="messages:read"')
        tok = testing.mint_access_token(AUDIENCE, claims={"scope": "profile messages:read"})
        self.assertEqual(self.get("/api/messages", tok).status_code, 200)
```

```sh
set -a; . ./.env.example; set +a
DJANGO_SECRET_KEY=insecure-test-only python manage.py test
```

Die Tests des Beispiels prüfen, dass ein gültiges Token akzeptiert wird und dass ein fehlendes, abgelaufenes oder für eine andere Audience ausgestelltes Token, ein fehlender Scope, ein Token in der URL-Query und ein widerrufenes Token mit den unten genannten Statuscodes abgelehnt werden. Außerdem senden sie ein DPoP-gebundenes Token mit einem Nachweis für `PUBLIC_BASE_URL` und prüfen, dass der Produktions-Protector den Test-Issuer ablehnt. Die Tests senden zudem signierte `token.revoked`-Zustellungen: Eine gültige widerruft das Token, eine gefälschte, manipulierte oder veraltete erhält `400` und widerruft nichts, und eine wiederholte hat genau eine Wirkung.

## Antworten

| Anfrage | Status | `code` im Body |
|---|---|---|
| Kein `Authorization`-Header | `401` | `missing_token` |
| Abgelaufenes Token, Token für eine andere API, ungültige Signatur, widerrufenes Token | `401` | `invalid_token` |
| Token ohne den erforderlichen Scope | `403` | `insufficient_scope` |
| Token in der URL-Query (`?access_token=`) | `400` | `invalid_request` |

Der Body ist `application/problem+json`, und der `WWW-Authenticate`-Header enthält die Standard-Challenge; Django fasst mehrere Challenges in einem Header zusammen. Die vollständige Tabelle steht unter [API mit Middleware schützen](/docs/sdk/protect-api#antworten).

## Hinter einem Proxy

Wenn Clients DPoP verwenden, prüft die Middleware, dass jeder Nachweis für die URL erstellt wurde, die der Client aufgerufen hat. Setzen Sie `PUBLIC_BASE_URL` auf die externe URL, die Clients aufrufen, zum Beispiel `https://api.example.com`, auch hinter einem Proxy oder Load Balancer. Die Einstellung ist erforderlich und muss `https` verwenden (`http` nur für `localhost`, `127.0.0.1` oder `[::1]`); ohne sie startet die App nicht. Die Middleware liest selbst niemals `X-Forwarded-*`-Header; Django wertet sie nur aus, wenn Sie `SECURE_PROXY_SSL_HEADER` oder `USE_X_FORWARDED_HOST` setzen, und wenn `PUBLIC_BASE_URL` gesetzt ist, hängt die geprüfte URL nicht von ihnen ab.

## Widerrufene Tokens

Ein Zugriffstoken bleibt bis zu seinem Ablauf gültig. Deshalb teilt KeenAuth Ihrer API mit, wenn Tokens widerrufen werden. Das Beispiel übergibt dem Protector einen In-Memory-Widerrufs-Cache und stellt unter `POST /webhooks/keenauth` (`api/webhook.py`) einen `token.revoked`-Webhook bereit, der diesen Cache speist.

1. Legen Sie im Dashboard einen Webhook-Endpunkt mit der URL `https://<your host>/webhooks/keenauth` an, der `token.revoked` abonniert, und exportieren Sie sein Signaturgeheimnis als `KEENAUTH_WEBHOOK_SECRET`.
2. Der Handler prüft die Signatur mit dem SDK auf dem Rohbody und antwortet mit `400`, wenn sie ungültig ist oder der Zeitstempel mehr als fünf Minuten abweicht. Er ordnet den Selektor des Ereignisses (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) einer Widerrufsmarkierung zu und antwortet mit `204`; die Middleware lehnt danach jedes passende Token ab, das vor `not_before` ausgestellt wurde.
3. KeenAuth liefert mindestens einmal, daher wendet der Handler jedes `webhook-id` genau einmal an. Kann die Markierung nicht gespeichert werden, antwortet er mit `503`, und KeenAuth liefert das Ereignis erneut. Andere Ereignistypen und Selektoren, auf die kein Zugriffstoken passen kann (`refresh_family`, `api`), ignoriert er.

Der Handler protokolliert die Ereignis-ID, den Selektortyp und den Grund, niemals den Body, eine Signatur oder das Geheimnis. Jeder Worker-Prozess hat seinen eigenen Cache; bei mehreren Workern oder Instanzen verwenden Sie stattdessen einen gemeinsamen Speicher. Siehe [Widerrufs-Cache](/docs/sdk/protect-api#widerrufs-cache-tokenrevoked).

## Nächste Schritte

- [API mit Middleware schützen](/docs/sdk/protect-api): Flask, Step-up-Anforderungen, DPoP und alle Optionen.
- [Tokens prüfen](/docs/sdk/token-verification): der Verifier unter der Middleware.
- [Offline-Test-Kit](/docs/sdk/testing).
