Saltar a contenido

La sesión con cookie

Por defecto, el login devuelve el access token y el refresh token en el cuerpo, y lo que haga el frontend con ellos es cosa suya. Casi siempre acaba en localStorage, donde cualquier XSS se los lleva. La sesión con cookie lo resuelve en la API: el refresh token viaja en una cookie que el JavaScript del navegador no puede leer, y rota en cada uso.

Es lo que usa el cliente TypeScript, y lo que exige.

Activarla

{
  "auth": {
    "type": "database",
    "config": { "session_management": true },
    "session": {
      "transport": "cookie",
      "access_minutes": 10,
      "idle_hours": 8,
      "absolute_hours": 12
    }
  }
}
Clave Qué es Por defecto
transport body (lo de siempre) o cookie body
access_minutes Lo que vale un access token 10
idle_hours La inactividad máxima: lo que vale el refresh, y la cookie 8
absolute_hours El límite de la sesión, que ningún refresco alarga 12

Con los valores por defecto se inicia sesión una vez al día. Sin el bloque, todo sigue como antes.

Qué cambia en /auth

Endpoint Con la cookie
POST /auth/{realm}/login Deja el refresh en la cookie. En el cuerpo, el access token, expires_in y session_expires_at; sin refresh_token
POST /auth/refresh Sin cuerpo: lee la cookie, y la devuelve rotada
POST /auth/logout Sin cuerpo: cierra la sesión y borra la cookie, siempre

La cookie se llama tai_refresh, es HttpOnly, SameSite=Strict, va limitada a /auth —no sale en ninguna petición de datos— y es Secure salvo en localhost.

Lo que la protege

  • Rota en cada refresco. Presentar un refresh que ya se usó es que alguien tiene una copia: se revoca la sesión entera y se responde SESSION_INVALIDATED con reason: "refresh_reused". Durante 10 segundos tras rotar, el anterior todavía vale, para que un reintento de red no cierre la sesión de un usuario legítimo.
  • Una sesión por usuario. Iniciar sesión en otro dispositivo cierra la anterior: SESSION_INVALIDATED con reason: "concurrent_login".
  • CSRF. Además del SameSite=Strict, refresh y logout exigen la cabecera X-Tai-Client: 1 (403 sin ella): una petición desde otro origen que la lleve obliga a un preflight que el CORS rechaza.
  • CORS. Con la cookie, la app no arranca con ALLOWED_ORIGINS = ["*"], que es lo que deja tai-api init en config.py. Detrás del proxy del frontend, la lista queda vacía; si el frontend está en otro puerto, van sus orígenes exactos.

Lo que exige cada modo

  • session_management: true: la rotación se apoya en la sesión que guarda el schema auth.
  • Un schema auth con las columnas refresh_generation y refresh_rotated_at en la tabla user. Los creados con tai-sql 0.7.10 o posterior las traen; en uno anterior, se añaden a la clase User de schemas/auth.py y se hace tai-sql push -s auth.

generate se niega, con la solución, si falta cualquiera de las dos.

  • PUBLIC_URL: la URL de la API tal como la ve el navegador, con su /api si va detrás de un proxy.
  • El realm preparado: lo hace tai-api rbac push (ver Keycloak).

La rotación, las duraciones y la sesión única las aplica Keycloak.

Desplegarla

La cookie solo viaja si el frontend y la API son el mismo sitio. En Azure, dos Web Apps no lo son. Ver Despliegue del frontend.