dec-0113

Vista generada de dec-0113: Modelo de tokens de identity-svc

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0113-identity-token-model.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0113-identity-token-model.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.

CampoValor
EstadoPROPOSED
Fecha2026-09-28
Ficherodocs/decisions/dec-0113-identity-token-model.md

Por qué importa (del frontmatter del ADR):

Formato de credenciales que emite identity-svc y que verifican todos los servicios (qry.identity.session, JWKS) y clientes (web, Apple). track/identity#01 D3.

Nodos del roadmap que lo citan en refs: ninguno.

Páginas de la documentación que lo citan: API keys (especificado), Conectar la TV y el CLI (especificado), Iniciar sesión (especificado)

Texto del ADR

Leído de docs/decisions/dec-0113-identity-token-model.md, el fichero canónico.

dec-0113 — Modelo de tokens de identity-svc

  • Fecha: 2026-09-28
  • Estado: PROPOSED — pendiente de AskUserQuestion a waxin. Implementado en apps/identity-svc como opción recomendada porque sus piezas (issuer propio + JWKS + store de sesiones) son las que cualquier alternativa razonable también necesita; lo que el lock fija es el formato y las duraciones.
  • Cita: r20 §5 (OIDC-first, ports ISessionStore/ITokenIssuer), dec-0087 (identity authoritative sobre credenciales), dec-0006 (Valkey para estado efímero).

Contexto

El OP (PocketID) autentica personas; Styx necesita credenciales propias para sus servicios: el sub del OP no es un id de Styx, el OP no conoce las sesiones de Styx y un servicio no debe llamar al OP en cada request. Hay que decidir qué emite identity-svc y cómo lo verifican los demás.

Opciones

A. JWT propio + refresh opaco rotatorio (recomendada)B. Reenviar el access token del OPC. Sesión opaca, todo por qry.identity.session
Verificación en serviciosoffline (JWKS) o revocation-aware por NATS, a elegir por el consumercontra el JWKS del OP; el sub es del OPsiempre un round-trip NATS
Revocacióninmediata vía qry.identity.session; ventana ≤ TTL del access en verificación offlinedepende del OPinmediata
Cambiar de OPno cambia ids ni tokens de Styxrompe todos los idsno cambia
Costeclave de firma a custodiarninguno propioidentity-svc en el hot path de todo request

Qué se propone (A)

  1. Access token: JWT compacto, alg: EdDSA (Ed25519), typ: at+jwt (RFC 9068), TTL 15 min. Claims: iss = URL pública de identity-svc, aud = styx, sub = actorId de Styx, sid = sessionId, name opcional, iat, exp, jti. Clave pública en /.well-known/jwks.json con kid = thumbprint RFC 7638.
  2. Refresh token: opaco <sessionId>.<secreto 256 bit>, con sessionId UUID (cualquier otra forma se rechaza como IDENTITY_TOKEN_INVALID sin consultar Valkey); Valkey guarda sólo SHA-256(secreto). Rotación en cada uso (script Lua atómico). Presentar uno ya rotado revoca la sesión entera (reuse detection, OAuth 2.0 Security BCP RFC 9700 §4.14), con la excepción de la ventana de gracia:
    • Ventana de gracia IDENTITY_REFRESH_GRACE_S, 10 s por defecto. Sólo cuenta el token inmediatamente anterior al vigente y sólo durante los N segundos siguientes a su rotación. Dentro de la ventana emite un access token sin rotar y sin revocar (el vigente sigue siendo el mismo; la respuesta no trae refresh token nuevo). Fuera de la ventana, o con cualquier token más antiguo, se aplica la reuse detection: IDENTITY_REFRESH_TOKEN_REUSED y la sesión entera revocada.
    • Por qué: dos pestañas o una petición reintentada tras perder la respuesta presentan el mismo refresh token casi a la vez; sin ventana, la segunda revoca la sesión del usuario legítimo (falso positivo de reuse).
    • Coste: durante la ventana, quien haya robado el token anterior obtiene access tokens (TTL 15 min, revocables por qry.identity.session) sin que la sesión se revoque; el robo sólo se detecta si lo usa después de la ventana. Es la misma concesión que documentan los emisores que rotan refresh tokens con "reuse interval" (Auth0, Okta).
    • Desactivable: IDENTITY_REFRESH_GRACE_S=0 elimina la ventana (el anterior es reuse desde el primer milisegundo). El valor admite cualquier entero ≥ 0.
  3. Sesión: TTL idle 30 días deslizante, máximo absoluto 90 días; logout = borrado.
  4. Navegador: el refresh token sólo viaja en cookie HttpOnly; SameSite=Strict; Path=/auth (Secure si la URL pública es https). El access token se pide con POST /auth/refresh y vive en memoria del cliente, nunca en storage persistente.
  5. Clave de firma: IDENTITY_SIGNING_KEY (PKCS#8 PEM Ed25519) por entorno; sin clave el servicio no arranca. Rotación con varios kid publicados = siguiente paso, fuera de este lock.

Qué fija el lock y qué queda reversible

  • Fija el lock (cambiarlo rompe a consumers o la semántica de seguridad): formato y claims del access token (§1), formato opaco del refresh token y la regla "reuse fuera de la ventana de gracia revoca la sesión entera" (§2), transporte del refresh token en navegador (§4).
  • Reversible sin lock (configuración): TTLs concretos, la duración de la ventana de gracia (incluido 0 = sin ventana), nombres de cookie, y si un consumer verifica offline o por NATS.
  • Decisión que waxin toma en el lock: mantener la ventana de gracia por defecto (10 s, recomendada: evita cerrar sesiones legítimas por refresh concurrentes) o arrancar con 0 (reuse detection estricta, a costa de esos falsos positivos).