
# Sessions Flask

`keenauth.bff.flask` ajoute la connexion à une application Flask 2.3 ou ultérieure. Votre serveur est le client OAuth et conserve tous les jetons côté serveur. Le navigateur ne reçoit qu'un cookie de session chiffré et HttpOnly, et ne voit jamais de jeton d'accès, de jeton de rafraîchissement ou de jeton d'ID. Il s'agit du modèle backend-for-frontend (BFF). Ce SDK fait partie de `keenauth-python`, qui n'est pas encore publié sur PyPI.

Pour protéger une API qui reçoit des jetons Bearer, consultez plutôt [Protéger votre API](/docs/sdk/protect-api).

## Installation

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

## Avant de commencer

1. Dans le tableau de bord, créez une application web dans votre environnement.
2. Choisissez l'authentification du client : `private_key_jwt` (recommandé, `PrivateKeyJwt`) ou un secret client (`ClientSecretBasic`).
3. Ajoutez l'URI de redirection `https://app.acme.example/auth/callback` et l'URI de redirection après déconnexion `https://app.acme.example/`.
4. Générez un secret de cookie d'au moins 32 caractères, par exemple avec `openssl rand -base64 32`. Conservez-le dans votre gestionnaire de secrets, et non dans le dépôt.

## Créer l'instance

```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` est l'adresse publique de votre application. Le SDK construit à partir de cette valeur l'URI de redirection et toutes les redirections, et jamais à partir de l'en-tête `Host` ou des en-têtes `X-Forwarded-*` de la requête. Le protocole `http` n'est accepté que pour `localhost`. Les options sont vérifiées à la création de `KeenAuthBff`, de sorte qu'une erreur de configuration arrête l'application au démarrage.

## Utiliser private_key_jwt

Avec `private_key_jwt`, l'application ne possède aucun secret partagé. Transmettez votre clé privée à `PrivateKeyJwt` : le SDK signe une nouvelle assertion client (RFC 7523) pour chaque requête envoyée au point de terminaison de jeton :

```python
from keenauth.bff import PrivateKeyJwt

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

- Clés prises en charge : EC P-256 (`ES256`), EC P-384 (`ES384`), Ed25519 (`EdDSA`) et RSA d'au moins 2048 bits (`PS256` par défaut ; passez `algorithm="RS256"` pour `RS256`).
- Chaque assertion a `iss` et `sub` définis sur votre ID client, `aud` défini sur l'émetteur, un `jti` aléatoire et une durée de vie de 60 secondes. Son `kid` est l'empreinte RFC 7638 de la clé publique.
- Enregistrez la clé publique sur l'application. `keenauth.m2m.load_private_key(pem).public_jwk()` la renvoie sous forme de JWK.

Pour signer avec votre propre service de clés (KMS ou HSM), passez plutôt une fonction : `PrivateKeyJwt(assertion=sign)`. `sign()` doit renvoyer une nouvelle assertion signée à chaque appel.

## Enregistrer les hooks

```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` gère les routes ci-dessous et lit la session à chaque autre requête pour la placer dans `g.keenauth`. Ce SDK n'utilise pas le cookie `session` propre à Flask.

## Routes

| Route | Ce qu'elle fait |
|---|---|
| `GET /auth/login` | Démarre la connexion. Paramètres de requête facultatifs : `returnTo` (où l'utilisateur arrive) et `organization` (se connecter à une organisation). |
| `GET /auth/callback` | Termine la connexion et définit le cookie de session. |
| `GET` ou `POST /auth/logout` | Déconnecte l'utilisateur chez KeenAuth et le renvoie vers votre application. |

La connexion utilise le flux à code d'autorisation avec PKCE (S256). La requête est envoyée au serveur (PAR) lorsque votre environnement le propose. Au retour sur le callback, le SDK vérifie `state`, `iss` et le jeton d'ID, y compris son `nonce`. Chaque connexion réussie crée une nouvelle session ; l'ancienne est supprimée.

`returnTo` doit être un chemin de votre propre application, par exemple `/orders?tab=open`. Toute autre valeur, comme `//evil.example` ou une URL complète, est ignorée et l'utilisateur arrive sur `/`.

## Vues protégées

Décorez une vue avec `@require_auth(bff)`, sous le décorateur de route.


Sans session :

- une requête de page (`GET` ou `HEAD`) est redirigée vers `/auth/login` avec `returnTo` ;
- une requête vers `/api/…`, ou utilisant une autre méthode, reçoit `401` avec un document de problème. Passez `api=True` pour répondre `401` sur tous les chemins.

Pour `POST`, `PUT`, `PATCH`, `DELETE` et les autres méthodes non sûres, le navigateur doit indiquer que la requête provient de votre application : `Sec-Fetch-Site: same-origin`, ou un en-tête `Origin` égal à l'origine de votre application. Les autres requêtes reçoivent `403`. Cela protège contre la falsification de requête intersite, en complément de `SameSite=Lax`.

## Utiliser la session et appeler votre API

Le contexte de session contient :

- `session` : `None`, ou l'utilisateur connecté. `session.user` expose des revendications typées du jeton d'ID, comme `subject`, `email`, `name`, `org_id` et `org_roles` ; toutes les revendications figurent dans `session.user.claims`. L'objet de session ne contient jamais de jeton.
- `access_token` : un jeton d'accès valide pour les appels côté serveur, ou `None`. Ne l'envoyez pas au navigateur.

Lorsque le jeton d'accès expire dans moins de 60 secondes, le SDK le rafraîchit avant l'exécution de votre vue. Chaque rafraîchissement remplace le jeton de rafraîchissement et définit le nouveau cookie de session dans la réponse. Si le serveur refuse le jeton de rafraîchissement, la session prend fin. Une erreur serveur temporaire laisse la session inchangée. Les requêtes simultanées d'un même processus partagent un seul rafraîchissement.

## Changer d'organisation

`bff.switch_organization_path("org_01j9…", "/dashboard")` renvoie un lien vers `/auth/login` qui reconnecte l'utilisateur dans cette organisation. KeenAuth vérifie l'appartenance. Après la connexion, `session.user.org_id` contient l'organisation.

## Se déconnecter

Envoyez une requête `POST` vers `/auth/logout` depuis un formulaire. Le SDK supprime le cookie de session (et la session stockée, si vous utilisez un magasin), révoque le jeton de rafraîchissement et redirige le navigateur vers le point de terminaison de déconnexion de votre environnement, qui le renvoie vers votre application. Une requête que le navigateur signale comme provenant d'un autre site est refusée. Les navigateurs qui n'envoient pas `Sec-Fetch-Site` ne peuvent se déconnecter qu'avec un `POST` depuis vos propres pages.

## Stockage des sessions

Par défaut, la session entière est dans le cookie, chiffrée et authentifiée avec AES-256-GCM à l'aide d'une clé dérivée de `secret`. Un cookie modifié ou copié est rejeté. Le cookie se nomme `__Host-keenauth_session.0` et est défini avec `Secure`, `HttpOnly`, `SameSite=Lax` et `Path=/`. Sur `http://localhost`, le préfixe `__Host-` et `Secure` sont omis. Les sessions volumineuses sont réparties sur jusqu'à trois cookies.

Pour conserver les jetons côté serveur, passez `store=` avec un objet disposant de `get(id)`, `set(id, session, ttl_seconds)` et `delete(id)`, par exemple adossé à Redis. Le cookie ne contient alors qu'un identifiant de session aléatoire et chiffré. Chaque connexion crée un nouvel identifiant et supprime l'enregistrement précédent. Le magasin contient des jetons : limitez l'accès à celui-ci. `MemorySessionStore` est réservé au développement et aux tests.

Une session dure au maximum 7 jours (`session_max_age`) ; le rafraîchissement ne la prolonge pas. Pour changer le secret, passez une liste : `secret=[new_secret, old_secret]`. Retirez l'ancien après `session_max_age`.

## Limites actuelles

- Les jetons liés par DPoP et la déconnexion back-channel ne sont pas encore gérés.
- Les rafraîchissements ne sont partagés qu'au sein d'un même processus serveur. Avec plusieurs processus, le paramètre d'application `refresh_reuse_interval_s` (5 secondes par défaut) permet à un rafraîchissement parallèle de réussir ; si vous le définissez à `0`, le second rafraîchissement met fin à la session.
