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
codesin 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_tokenyaccess_token: directos ensessionStorage.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:role—viewer | 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) |