Sessioni FastAPI

keenauth.bff.fastapi aggiunge l'accesso a un'applicazione FastAPI. 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.

Installazione

pip install "keenauth-python[fastapi]"

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

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

bff = KeenAuthBff(BffOptions(
    issuer="https://acme.eu.keenauth.app",  # l'issuer del tuo ambiente, esattamente
    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 lui il redirect URI e tutti i redirect, mai dagli header Host o X-Forwarded-* della richiesta. Il protocollo http è accettato solo per localhost. Le opzioni vengono verificate alla creazione di KeenAuthBff, quindi un errore di configurazione blocca l'app all'avvio.

Usa private_key_jwt

Con private_key_jwt l'applicazione non ha un segreto condiviso. Passa a PrivateKeyJwt la tua chiave privata: l'SDK firma un nuovo client assertion (RFC 7523) per ogni richiesta al token endpoint:

from keenauth.bff import PrivateKeyJwt

client_auth = PrivateKeyJwt(key=os.environ["KEENAUTH_PRIVATE_KEY"])  # PEM non cifrato

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.

Installa il middleware

from fastapi import Depends, FastAPI
from keenauth.bff import SessionContext
from keenauth.bff.fastapi import RequireSession, install

app = FastAPI()
install(app, bff)

@app.get("/orders")
def orders(ctx: SessionContext = Depends(RequireSession(bff))):
    return {"email": ctx.session.user.email}

install serve le route qui sotto e legge la sessione a ogni altra richiesta in request.state.keenauth. Le chiamate di rete dell'SDK sono sincrone; install le esegue nel thread pool di Starlette, così non bloccano l'event loop.

Route

Route Cosa fa
GET /auth/login Avvia l'accesso. Parametri query facoltativi: returnTo (dove atterra 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 l'utente su KeenAuth e torna 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. Al 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, come /orders?tab=open. Qualsiasi altro valore, come //evil.example o un URL completo, viene ignorato e l'utente atterra su /.

Viste protette

Aggiungi Depends(RequireSession(bff)) a una path operation. Restituisce il contesto di sessione.

Senza una sessione:

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:

Quando l'access token scade entro 60 secondi, l'SDK lo aggiorna prima che la tua vista venga eseguita. Ogni aggiornamento 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 aggiornamento.

Cambia organizzazione

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

Disconnessione

Invia una 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 manda il browser all'endpoint di logout del tuo ambiente, che lo riporta alla tua app. Una richiesta che il browser segnala come proveniente da un altro sito viene rifiutata. I browser che non inviano Sec-Fetch-Site possono disconnettere solo con una POST dalle tue pagine.

Archiviazione della sessione

Per impostazione predefinita l'intera sessione sta 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 al massimo tre cookie.

Per tenere 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 e cifrato. Ogni accesso crea un nuovo ID ed elimina il record precedente. Lo store contiene token, quindi limitane l'accesso. MemorySessionStore è pensato solo per sviluppo e test.

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

Limiti attuali