Saltar a contenido

Auth Cognito PKCE

Pieza por pieza

Pieza Valor
User pool us-east-2_iv0sPC5lo (zen-dev-app-users)
App client SPA (público, PKCE) 4mr4vrvcvklm81m8gp2mb2eb72
Hosted UI domain zen-dev-auth.auth.us-east-2.amazoncognito.com
Auth SPA (ze-login reusado) https://auth.dev.zen.zervizdev.com/login.html
Callback URL Admin https://admin.dev.zen.zervizdev.com/callback
auth-bff API GW + Lambda (token exchange + session)

Decisión: ADR-0005.

Flujo PKCE detallado

sequenceDiagram
    autonumber
    participant A as Admin SPA
    participant H as Hosted UI
    participant T as Cognito Token Endpoint
    participant B as auth-bff
    A->>A: code_verifier = random 64 chars
    A->>A: code_challenge = base64url(sha256(code_verifier))
    A->>A: state = random 32 chars (CSRF)
    A->>A: sessionStorage.set({code_verifier, state})
    A->>H: redirect /oauth2/authorize?
response_type=code&client_id=...&
scope=openid+profile+email&
code_challenge=...&code_challenge_method=S256&
state=...&redirect_uri=.../callback H-->>A: redirect .../callback?code=...&state=... A->>A: valida state == sessionStorage.state A->>T: POST /oauth2/token
grant_type=authorization_code
code=...
code_verifier=...
client_id=... T-->>A: id_token, access_token, refresh_token A->>A: sessionStorage.set(tokens) A->>B: GET /v1/auth/session
Authorization: Bearer access_token B-->>A: { tenantId, roles, features }

Por qué PKCE (no implicit)

  • Implicit flow está deprecado por OAuth 2.1.
  • PKCE evita exfiltración del code sin client_secret (que un SPA no puede tener).
  • Mismo flow funciona para SPA, móvil nativo y CLI.

Storage de tokens

  • sessionStorage (no localStorage): tokens mueren al cerrar pestaña.
  • id_token y access_token: directos en sessionStorage.
  • refresh_token: idem, dev only. En qa/prod evaluamos cookie HttpOnly via auth-bff (ADR pendiente).

Refresh transparente

El SPA detecta expires_at y refresca 60s antes de expirar:

async function ensureFreshToken() {
  const exp = Number(sessionStorage.getItem('exp'));
  if (Date.now() / 1000 < exp - 60) return; // still fresh
  const refresh = sessionStorage.getItem('refresh_token');
  const res = await fetch(`${cognitoDomain}/oauth2/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: clientId,
      refresh_token: refresh
    })
  });
  // ...persist new tokens
}

Logout

const url = new URL(`${cognitoDomain}/logout`);
url.searchParams.set('client_id', clientId);
url.searchParams.set('logout_uri', `${origin}/`);
window.location.assign(url.toString());
sessionStorage.clear();

ze-login legacy reusado

https://auth.dev.zen.zervizdev.com es el SPA ze-login del legacy, con config.js re-apuntado al user pool v2. Sirve como página de landing/login para tenants que aún no migraron al Admin SPA nuevo.

Tokens y claims relevantes

ID token incluye:

  • sub, email, cognito:username.
  • custom:tenant_id — propagado a authorizers y handlers.
  • custom:roleviewer | editor | admin | super-admin.

El authorizer JWT de API Gateway (HTTP) valida JWKS + audience + expiry. Los handlers leen claims de event.requestContext.authorizer.jwt.claims.

Errores frecuentes

Error Causa
invalid_grant: redirect_uri mismatch redirect_uri no registrado en el app client
invalid_grant: code expired Code intercambiado más de 60s después
invalid_grant: code_verifier mismatch sessionStorage perdido entre redirect
403 missing tenant claim Usuario sin custom:tenant_id (resolver con tenant-mgmt)