
# Flask-Sitzungen

`keenauth.bff.flask` ergänzt eine Flask-Anwendung ab Version 2.3 um Anmeldung. Ihr Server ist der OAuth-Client und bewahrt alle Tokens serverseitig auf. Der Browser erhält nur ein verschlüsseltes Session-Cookie mit dem Attribut HttpOnly und sieht niemals ein Access-, Refresh- oder ID-Token. Dies ist das Backend-for-Frontend-(BFF-)Muster. Es ist Teil von `keenauth-python`, das noch nicht auf PyPI veröffentlicht ist.

Um stattdessen eine API abzusichern, die Bearer-Tokens entgegennimmt, siehe [Ihre API absichern](/docs/sdk/protect-api).

## Installation

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

## Bevor Sie beginnen

1. Erstellen Sie im Dashboard eine Webanwendung in Ihrer Umgebung.
2. Wählen Sie die Client-Authentifizierung: `private_key_jwt` (empfohlen, `PrivateKeyJwt`) oder ein Client-Secret (`ClientSecretBasic`).
3. Fügen Sie die Redirect-URI `https://app.acme.example/auth/callback` und die Redirect-URI nach der Abmeldung `https://app.acme.example/` hinzu.
4. Erzeugen Sie ein Cookie-Secret mit mindestens 32 Zeichen, zum Beispiel mit `openssl rand -base64 32`. Bewahren Sie es in Ihrem Secret-Store auf, nicht im Repository.

## Instanz erstellen

```python
import os
from keenauth.bff import BffOptions, ClientSecretBasic, KeenAuthBff

bff = KeenAuthBff(BffOptions(
    issuer="https://acme.eu.keenauth.app",  # your environment's issuer, exactly
    client_id="app_01j9zzzzzzzzzzzzzzzzzzzz",
    client_auth=ClientSecretBasic(os.environ["KEENAUTH_CLIENT_SECRET"]),
    app_base_url="https://app.acme.example",
    secret=os.environ["KEENAUTH_COOKIE_SECRET"],
))
```

`app_base_url` ist die öffentliche Adresse Ihrer Anwendung. Das SDK erzeugt daraus die Redirect-URI und alle Weiterleitungen, niemals aus dem Header `Host` oder den Headern `X-Forwarded-*` der Anfrage. Klartext-`http` wird nur für `localhost` akzeptiert. Die Optionen werden beim Erstellen von `KeenAuthBff` geprüft, sodass ein Konfigurationsfehler die Anwendung beim Start anhält.

## private_key_jwt verwenden

Bei `private_key_jwt` besitzt die Anwendung kein gemeinsames Geheimnis. Übergeben Sie Ihren privaten Schlüssel an `PrivateKeyJwt`. Das SDK signiert dann für jede Anfrage an den Token-Endpunkt eine neue Client-Assertion (RFC 7523):

```python
from keenauth.bff import PrivateKeyJwt

client_auth = PrivateKeyJwt(key=os.environ["KEENAUTH_PRIVATE_KEY"])  # unencrypted PEM
```

- Unterstützte Schlüssel: EC P-256 (`ES256`), EC P-384 (`ES384`), Ed25519 (`EdDSA`) sowie RSA mit mindestens 2048 Bit (standardmäßig `PS256`; übergeben Sie `algorithm="RS256"` für `RS256`).
- Jede Assertion hat `iss` und `sub` mit Ihrer Client-ID, `aud` mit dem Issuer, einen zufälligen `jti` und eine Gültigkeitsdauer von 60 Sekunden. Ihre `kid` ist der RFC-7638-Thumbprint des öffentlichen Schlüssels.
- Hinterlegen Sie den öffentlichen Schlüssel an der Anwendung. `keenauth.m2m.load_private_key(pem).public_jwk()` gibt ihn als JWK zurück.

Wenn Sie mit Ihrem eigenen Schlüsseldienst (einem KMS oder HSM) signieren möchten, übergeben Sie stattdessen eine Funktion: `PrivateKeyJwt(assertion=sign)`. `sign()` muss bei jedem Aufruf eine neue signierte Assertion zurückgeben.

## Hooks registrieren

```python
from flask import Flask, g
from keenauth.bff.flask import init_app, require_auth

app = Flask(__name__)
init_app(app, bff)

@app.get("/orders")
@require_auth(bff)
def orders():
    return f"Signed in as {g.keenauth.session.user.email}"
```

`init_app` stellt die unten aufgeführten Routen bereit und liest bei jeder anderen Anfrage die Sitzung in `g.keenauth` ein. Dieses SDK verwendet das eigene `session`-Cookie von Flask nicht.

## Routen

| Route | Funktion |
|---|---|
| `GET /auth/login` | Startet die Anmeldung. Optionale Query-Parameter: `returnTo` (wohin der Benutzer gelangt) und `organization` (Anmeldung bei einer Organisation). |
| `GET /auth/callback` | Schließt die Anmeldung ab und setzt das Session-Cookie. |
| `GET` oder `POST /auth/logout` | Meldet bei KeenAuth ab und leitet zurück zu Ihrer Anwendung. |

Die Anmeldung verwendet den Authorization-Code-Flow mit PKCE (S256). Die Anfrage wird an den Server übermittelt (PAR), sofern Ihre Umgebung dies unterstützt. Beim Callback prüft das SDK `state`, `iss` und das ID-Token einschließlich seines `nonce`. Jede erfolgreiche Anmeldung erstellt eine neue Sitzung; die alte wird verworfen.

`returnTo` muss ein Pfad Ihrer eigenen Anwendung sein, etwa `/orders?tab=open`. Jeder andere Wert, etwa `//evil.example` oder eine vollständige URL, wird ignoriert und der Benutzer gelangt zu `/`.

## Geschützte Ansichten

Versehen Sie eine Ansicht mit `@require_auth(bff)` unterhalb des Route-Decorators.


Ohne Sitzung gilt:

- Eine Seitenanfrage (`GET` oder `HEAD`) wird mit `returnTo` auf `/auth/login` umgeleitet;
- eine Anfrage an `/api/…` oder mit einer anderen Methode erhält `401` mit einem Problem-Dokument. Übergeben Sie `api=True`, um auf jedem Pfad mit `401` zu antworten.

Bei `POST`, `PUT`, `PATCH`, `DELETE` und anderen unsicheren Methoden muss der Browser angeben, dass die Anfrage von Ihrer Anwendung stammt: `Sec-Fetch-Site: same-origin` oder ein Header `Origin`, der dem Ursprung Ihrer Anwendung entspricht. Andere Anfragen erhalten `403`. Dies schützt zusätzlich zu `SameSite=Lax` vor Cross-Site-Request-Forgery.

## Sitzung verwenden und Ihre API aufrufen

Der Sitzungskontext enthält:

- `session`: `None` oder der angemeldete Benutzer. `session.user` enthält typisierte Claims des ID-Tokens wie `subject`, `email`, `name`, `org_id` und `org_roles`; alle Claims befinden sich in `session.user.claims`. Das Sitzungsobjekt enthält niemals ein Token.
- `access_token`: ein gültiges Access-Token für serverseitige Aufrufe oder `None`. Senden Sie es nicht an den Browser.

Läuft das Access-Token innerhalb von 60 Sekunden ab, erneuert das SDK es, bevor Ihre Ansicht ausgeführt wird. Jede Erneuerung rotiert das Refresh-Token und setzt das neue Session-Cookie in der Antwort. Verweigert der Server das Refresh-Token, endet die Sitzung. Ein vorübergehender Serverfehler lässt die Sitzung unverändert. Gleichzeitige Anfragen in einem Prozess teilen sich eine Erneuerung.

## Organisation wechseln

`bff.switch_organization_path("org_01j9…", "/dashboard")` gibt einen Link zu `/auth/login` zurück, der den Benutzer erneut in dieser Organisation anmeldet. KeenAuth prüft die Mitgliedschaft. Nach der Anmeldung enthält `session.user.org_id` die Organisation.

## Abmelden

Senden Sie aus einem Formular ein `POST` an `/auth/logout`. Das SDK löscht das Session-Cookie (und die gespeicherte Sitzung, falls Sie einen Store verwenden), widerruft das Refresh-Token und schickt den Browser zum Logout-Endpunkt Ihrer Umgebung, der ihn zu Ihrer Anwendung zurückleitet. Eine Anfrage, die der Browser als von einer anderen Seite stammend kennzeichnet, wird abgelehnt. Browser, die kein `Sec-Fetch-Site` senden, können sich nur mit einem `POST` von Ihren eigenen Seiten abmelden.

## Sitzungsspeicher

Standardmäßig liegt die gesamte Sitzung im Cookie, verschlüsselt und mit AES-256-GCM authentifiziert, unter einem Schlüssel, der aus `secret` abgeleitet wird. Ein geändertes oder kopiertes Cookie wird abgelehnt. Das Cookie heißt `__Host-keenauth_session.0` und wird mit `Secure`, `HttpOnly`, `SameSite=Lax` und `Path=/` gesetzt. Auf `http://localhost` entfallen das Präfix `__Host-` und `Secure`. Große Sitzungen werden auf bis zu drei Cookies aufgeteilt.

Um Tokens auf dem Server zu halten, übergeben Sie `store=` mit einem Objekt, das `get(id)`, `set(id, session, ttl_seconds)` und `delete(id)` bereitstellt, zum Beispiel auf Basis von Redis. Das Cookie enthält dann nur noch eine verschlüsselte, zufällige Sitzungs-ID. Jede Anmeldung erzeugt eine neue ID und löscht den vorherigen Datensatz. Der Store enthält Tokens, daher sollten Sie den Zugriff darauf einschränken. `MemorySessionStore` ist nur für Entwicklung und Tests gedacht.

Eine Sitzung dauert höchstens 7 Tage (`session_max_age`); eine Erneuerung verlängert sie nicht. Um das Secret zu ändern, übergeben Sie eine Liste: `secret=[new_secret, old_secret]`. Entfernen Sie das alte Secret nach `session_max_age`.

## Aktuelle Einschränkungen

- An DPoP gebundene Tokens und Back-Channel-Logout werden noch nicht unterstützt.
- Erneuerungen werden nur innerhalb eines Serverprozesses geteilt. Bei mehreren Prozessen lässt die Anwendungseinstellung `refresh_reuse_interval_s` (Standard: 5 Sekunden) eine parallele Erneuerung erfolgreich sein; setzen Sie sie auf `0`, beendet die zweite Erneuerung die Sitzung.
