
# Sesiones de FastAPI

`keenauth.bff.fastapi` añade el inicio de sesión a una aplicación FastAPI. Tu servidor es el cliente OAuth y mantiene todos los tokens en el servidor. El navegador solo recibe una cookie de sesión cifrada y HttpOnly, y nunca ve un token de acceso, de actualización ni de ID. Este es el patrón backend-for-frontend (BFF). Forma parte de `keenauth-python`, que todavía no está publicado en PyPI.

Para proteger una API que recibe tokens de portador, consulta [Protecting your API](/docs/sdk/protect-api).

## Instalación

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

## Antes de empezar

1. En el panel, crea una aplicación web en tu entorno.
2. Elige la autenticación del cliente: `private_key_jwt` (recomendado, `PrivateKeyJwt`) o un secreto de cliente (`ClientSecretBasic`).
3. Añade el URI de redirección `https://app.acme.example/auth/callback` y el URI de redirección tras cerrar sesión `https://app.acme.example/`.
4. Genera un secreto para la cookie de al menos 32 caracteres, por ejemplo con `openssl rand -base64 32`. Guárdalo en tu almacén de secretos, no en el repositorio.

## Crear la instancia

```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` es la dirección pública de tu aplicación. El SDK construye el URI de redirección y todas las redirecciones a partir de ella, nunca a partir de los encabezados `Host` o `X-Forwarded-*` de la solicitud. El `http` simple solo se acepta para `localhost`. Las opciones se validan al crear `KeenAuthBff`, así que un error de configuración detiene la aplicación al arrancar.

## Usar private_key_jwt

Con `private_key_jwt` la aplicación no tiene ningún secreto compartido. Proporciona tu clave privada a `PrivateKeyJwt` y el SDK firma una nueva aserción de cliente (RFC 7523) en cada solicitud al endpoint de tokens:

```python
from keenauth.bff import PrivateKeyJwt

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

- Claves admitidas: EC P-256 (`ES256`), EC P-384 (`ES384`), Ed25519 (`EdDSA`) y RSA de al menos 2048 bits (`PS256` por defecto; pasa `algorithm="RS256"` para `RS256`).
- Cada aserción tiene `iss` y `sub` con el ID de tu cliente, `aud` con el emisor, un `jti` aleatorio y una vigencia de 60 segundos. Su `kid` es la huella RFC 7638 de la clave pública.
- Registra la clave pública en la aplicación. `keenauth.m2m.load_private_key(pem).public_jwk()` la devuelve como JWK.

Para firmar con tu propio servicio de claves (un KMS o HSM), pasa una función en su lugar: `PrivateKeyJwt(assertion=sign)`. `sign()` debe devolver una aserción firmada nueva en cada llamada.

## Instalar el middleware

```python
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` sirve las rutas siguientes y lee la sesión en cada otra solicitud para colocarla en `request.state.keenauth`. Las llamadas de red del SDK son síncronas; `install` las ejecuta en el pool de hilos de Starlette para que no bloqueen el bucle de eventos.

## Rutas

| Ruta | Qué hace |
|---|---|
| `GET /auth/login` | Inicia el inicio de sesión. Parámetros de consulta opcionales: `returnTo` (a dónde llega el usuario) y `organization` (iniciar sesión en una organización). |
| `GET /auth/callback` | Finaliza el inicio de sesión y establece la cookie de sesión. |
| `GET` o `POST /auth/logout` | Cierra la sesión en KeenAuth y vuelve a tu aplicación. |

El inicio de sesión usa el flujo de código de autorización con PKCE (S256). La solicitud se envía al servidor (PAR) cuando tu entorno lo ofrece. En el callback, el SDK comprueba `state`, `iss` y el token de ID, incluido su `nonce`. Cada inicio de sesión correcto crea una sesión nueva y descarta la anterior.

`returnTo` debe ser una ruta de tu propia aplicación, como `/orders?tab=open`. Cualquier otro valor, como `//evil.example` o una URL completa, se ignora y el usuario llega a `/`.

## Vistas protegidas

Añade `Depends(RequireSession(bff))` a una operación de ruta. Devuelve el contexto de la sesión.


Sin sesión:

- una solicitud de página (`GET` o `HEAD`) se redirige a `/auth/login` con `returnTo`;
- una solicitud a `/api/…`, o con otro método, recibe `401` con un documento de problema. Pasa `api=True` para responder `401` en cualquier ruta.

Para `POST`, `PUT`, `PATCH`, `DELETE` y otros métodos no seguros, el navegador debe indicar que la solicitud proviene de tu aplicación: `Sec-Fetch-Site: same-origin`, o un encabezado `Origin` igual al origen de tu aplicación. Las demás solicitudes reciben `403`. Esto protege contra la falsificación de solicitudes entre sitios además de `SameSite=Lax`.

## Usar la sesión y llamar a tu API

El contexto de la sesión tiene:

- `session`: `None`, o el usuario que ha iniciado sesión. `session.user` tiene las reclamaciones tipadas del token de ID, como `subject`, `email`, `name`, `org_id` y `org_roles`; todas las reclamaciones están en `session.user.claims`. El objeto de sesión nunca contiene un token.
- `access_token`: un token de acceso válido para llamadas del lado del servidor, o `None`. No lo envíes al navegador.

Cuando el token de acceso caduca en menos de 60 segundos, el SDK lo renueva antes de ejecutar tu vista. Cada renovación rota el token de actualización y establece la nueva cookie de sesión en la respuesta. Si el servidor rechaza el token de actualización, la sesión termina. Un error temporal del servidor deja la sesión sin cambios. Las solicitudes concurrentes en un mismo proceso comparten una única renovación.

## Cambiar de organización

`bff.switch_organization_path("org_01j9…", "/dashboard")` devuelve un enlace a `/auth/login` que vuelve a iniciar sesión al usuario en esa organización. KeenAuth comprueba la pertenencia. Tras el inicio de sesión, `session.user.org_id` contiene la organización.

## Cerrar sesión

Envía una solicitud `POST` a `/auth/logout` desde un formulario. El SDK elimina la cookie de sesión (y la sesión almacenada, si usas un almacén), revoca el token de actualización y envía el navegador al endpoint de cierre de sesión de tu entorno, que lo devuelve a tu aplicación. Una solicitud que el navegador marca como procedente de otro sitio se rechaza. Los navegadores que no envían `Sec-Fetch-Site` solo pueden cerrar sesión con un `POST` desde tus propias páginas.

## Almacenamiento de sesiones

Por defecto, toda la sesión está en la cookie, cifrada y autenticada con AES-256-GCM usando una clave derivada de `secret`. Una cookie modificada o copiada se rechaza. La cookie se llama `__Host-keenauth_session.0` y se establece con `Secure`, `HttpOnly`, `SameSite=Lax` y `Path=/`. En `http://localhost` se omiten el prefijo `__Host-` y `Secure`. Las sesiones grandes se dividen en hasta tres cookies.

Para mantener los tokens en el servidor, pasa `store=` con un objeto que tenga `get(id)`, `set(id, session, ttl_seconds)` y `delete(id)`, por ejemplo respaldado por Redis. La cookie entonces contiene solo un ID de sesión aleatorio y cifrado. Cada inicio de sesión crea un ID nuevo y elimina el registro anterior. El almacén contiene tokens, así que restringe el acceso a él. `MemorySessionStore` es solo para desarrollo y pruebas.

Una sesión dura como máximo 7 días (`session_max_age`); renovar no la prolonga. Para cambiar el secreto, pasa una lista: `secret=[new_secret, old_secret]`. Elimina el antiguo después de `session_max_age`.

## Límites actuales

- Los tokens vinculados a DPoP y el cierre de sesión en segundo plano (back-channel) aún no se gestionan.
- Las renovaciones se comparten solo dentro de un mismo proceso del servidor. Con varios procesos, el ajuste de la aplicación `refresh_reuse_interval_s` (por defecto 5 segundos) permite que una renovación en paralelo tenga éxito; si lo estableces en `0`, la segunda renovación termina la sesión.
