
# Sessioni Flask

`keenauth.bff.flask` aggiunge l'accesso a un'applicazione Flask 2.3 o successiva. Il tuo server è il client OAuth e conserva tutti i token lato server. Il browser riceve solo un cookie di sessione cifrato e HttpOnly e non vede mai un access token, un refresh token o un ID token. Questo è il pattern backend-for-frontend (BFF). Fa parte di `keenauth-python`, che non è ancora pubblicato su PyPI.

Per proteggere invece un'API che riceve bearer token, consulta [Proteggere la tua API](/docs/sdk/protect-api).

## Installazione

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

## Prima di iniziare

1. Nella dashboard, crea un'applicazione web nel tuo ambiente.
2. Scegli l'autenticazione del client: `private_key_jwt` (consigliata, `PrivateKeyJwt`) oppure un client secret (`ClientSecretBasic`).
3. Aggiungi il redirect URI `https://app.acme.example/auth/callback` e il post-logout redirect URI `https://app.acme.example/`.
4. Genera un cookie secret di almeno 32 caratteri, ad esempio con `openssl rand -base64 32`. Conservalo nel tuo secret store, non nel repository.

## Crea l'istanza

```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` è l'indirizzo pubblico della tua app. L'SDK costruisce da lì il redirect URI e ogni redirect, mai dagli header `Host` o `X-Forwarded-*` della richiesta. Il protocollo `http` è accettato solo per `localhost`. Le opzioni vengono verificate quando viene creato `KeenAuthBff`, quindi un errore di configurazione blocca l'app all'avvio.

## Usare private_key_jwt

Con `private_key_jwt` l'applicazione non ha alcun segreto condiviso. Passa a `PrivateKeyJwt` la tua chiave privata: l'SDK firma una nuova client assertion (RFC 7523) per ogni richiesta all'endpoint dei token:

```python
from keenauth.bff import PrivateKeyJwt

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

- Chiavi supportate: EC P-256 (`ES256`), EC P-384 (`ES384`), Ed25519 (`EdDSA`) e RSA di almeno 2048 bit (`PS256` per impostazione predefinita; passa `algorithm="RS256"` per `RS256`).
- Ogni assertion ha `iss` e `sub` impostati al tuo client ID, `aud` impostato all'issuer, un `jti` casuale e una durata di 60 secondi. Il suo `kid` è il thumbprint RFC 7638 della chiave pubblica.
- Registra la chiave pubblica sull'applicazione. `keenauth.m2m.load_private_key(pem).public_jwk()` la restituisce come JWK.

Per firmare con il tuo servizio di chiavi (un KMS o un HSM), passa invece una funzione: `PrivateKeyJwt(assertion=sign)`. `sign()` deve restituire una nuova assertion firmata a ogni chiamata.

## Registra gli hook

```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` serve le route sotto indicate e legge la sessione in `g.keenauth` a ogni altra richiesta. Questo SDK non usa il cookie `session` di Flask.

## Route

| Route | Cosa fa |
|---|---|
| `GET /auth/login` | Avvia l'accesso. Parametri query opzionali: `returnTo` (dove arriva l'utente) e `organization` (accedi a un'organizzazione). |
| `GET /auth/callback` | Completa l'accesso e imposta il cookie di sessione. |
| `GET` o `POST /auth/logout` | Disconnette in KeenAuth e riporta alla tua app. |

L'accesso usa il flusso authorization code con PKCE (S256). La richiesta viene inviata al server (PAR) quando il tuo ambiente la supporta. Nel callback l'SDK verifica `state`, `iss` e l'ID token, incluso il suo `nonce`. Ogni accesso riuscito crea una nuova sessione; quella precedente viene eliminata.

`returnTo` deve essere un percorso della tua app, ad esempio `/orders?tab=open`. Qualsiasi altro valore, come `//evil.example` o un URL completo, viene ignorato e l'utente arriva su `/`.

## Viste protette

Decora una vista con `@require_auth(bff)`, sotto il decoratore della route.


Senza sessione:

- una richiesta di pagina (`GET` o `HEAD`) viene reindirizzata a `/auth/login` con `returnTo`;
- una richiesta a `/api/…`, o con un altro metodo, riceve `401` con un documento problem. Passa `api=True` per rispondere `401` su qualsiasi percorso.

Per `POST`, `PUT`, `PATCH`, `DELETE` e gli altri metodi non sicuri, il browser deve dichiarare che la richiesta proviene dalla tua app: `Sec-Fetch-Site: same-origin`, oppure un header `Origin` uguale all'origine della tua app. Le altre richieste ricevono `403`. Questo protegge dal cross-site request forgery in aggiunta a `SameSite=Lax`.

## Usa la sessione e chiama la tua API

Il contesto di sessione contiene:

- `session`: `None`, oppure l'utente autenticato. `session.user` ha claim dell'ID token tipizzati come `subject`, `email`, `name`, `org_id` e `org_roles`; tutti i claim sono in `session.user.claims`. L'oggetto sessione non contiene mai un token.
- `access_token`: un access token valido per chiamate lato server, oppure `None`. Non inviarlo al browser.

Quando l'access token scade entro 60 secondi, l'SDK lo rinnova prima che la tua vista venga eseguita. Ogni rinnovo ruota il refresh token e imposta il nuovo cookie di sessione nella risposta. Se il server rifiuta il refresh token, la sessione termina. Un errore temporaneo del server lascia la sessione invariata. Le richieste concorrenti in un processo condividono un solo rinnovo.

## Cambia organizzazione

`bff.switch_organization_path("org_01j9…", "/dashboard")` restituisce un link a `/auth/login` che fa accedere di nuovo l'utente in quell'organizzazione. KeenAuth verifica l'appartenenza. Dopo l'accesso, `session.user.org_id` contiene l'organizzazione.

## Disconnessione

Invia una richiesta `POST` a `/auth/logout` da un form. L'SDK elimina il cookie di sessione (e la sessione memorizzata, se usi uno store), revoca il refresh token e invia il browser all'endpoint di logout del tuo ambiente, che lo riporta alla tua app. Una richiesta che il browser contrassegna come proveniente da un altro sito viene rifiutata. I browser che non inviano `Sec-Fetch-Site` possono disconnettere solo con un `POST` dalle tue pagine.

## Archiviazione della sessione

Per impostazione predefinita l'intera sessione è nel cookie, cifrata e autenticata con AES-256-GCM usando una chiave derivata da `secret`. Un cookie modificato o copiato viene rifiutato. Il cookie si chiama `__Host-keenauth_session.0` e viene impostato con `Secure`, `HttpOnly`, `SameSite=Lax` e `Path=/`. Su `http://localhost` il prefisso `__Host-` e `Secure` vengono omessi. Le sessioni grandi vengono suddivise in fino a tre cookie.

Per mantenere i token sul server, passa `store=` con un oggetto che abbia `get(id)`, `set(id, session, ttl_seconds)` e `delete(id)`, ad esempio basato su Redis. Il cookie contiene allora solo un ID di sessione casuale cifrato. Ogni accesso crea un nuovo ID ed elimina il record precedente. Lo store contiene token, quindi limitane l'accesso. `MemorySessionStore` è solo per sviluppo e test.

Una sessione dura al massimo 7 giorni (`session_max_age`); il rinnovo non la estende. Per cambiare il secret, passa una lista: `secret=[new_secret, old_secret]`. Rimuovi quello vecchio dopo `session_max_age`.

## Limiti attuali

- I token vincolati con DPoP e il back-channel logout non sono ancora gestiti.
- I rinnovi sono condivisi solo all'interno di un singolo processo del server. Con più processi, l'impostazione dell'applicazione `refresh_reuse_interval_s` (predefinita 5 secondi) consente a un rinnovo parallelo di riuscire; se la imposti a `0`, il secondo rinnovo termina la sessione.
