From 228cd533d239e02ad2e30321656d8b12ef28b6fb Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Thu, 14 May 2026 08:30:15 -0500 Subject: [PATCH] fix(sso): reduce cookie chunk threshold and add defensive error handling in SSO exchange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduce ACCESS_TOKEN_MAX_SINGLE y ACCESS_TOKEN_CHUNK_SIZE de 3500 a 2800 para garantizar que cada cookie fragmentada no supere el límite de 4096 bytes del navegador (incluye overhead del nombre, path, attrs). - Agrega try/catch alrededor de response.json() en el load de /auth/sso para evitar SyntaxError no manejado si el body no es JSON. - Type guards explícitos en tokens.access_token, refresh_token y tenant_id para detectar respuestas incompletas del hub antes de llamar cookies.set(). - .catch(() => ({})) en refreshRes.json() para que el refresh proactivo nunca bloquee el flujo SSO aunque el body sea inesperado. Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/access-token-cookie.shared.ts | 4 +-- frontend/src/routes/auth/sso/+page.server.ts | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/access-token-cookie.shared.ts b/frontend/src/lib/access-token-cookie.shared.ts index 182c073f..84766451 100644 --- a/frontend/src/lib/access-token-cookie.shared.ts +++ b/frontend/src/lib/access-token-cookie.shared.ts @@ -4,9 +4,9 @@ */ /** Por debajo de esto se usa una sola cookie `access_token` (compatibilidad). */ -export const ACCESS_TOKEN_MAX_SINGLE = 3500; +export const ACCESS_TOKEN_MAX_SINGLE = 2800; -export const ACCESS_TOKEN_CHUNK_SIZE = 3500; +export const ACCESS_TOKEN_CHUNK_SIZE = 2800; /** Número de fragmentos; si existe, el token está en access_token_0..access_token_{n-1}. */ export const ACCESS_TOKEN_CHUNK_COUNT = 'access_token_chunks'; diff --git a/frontend/src/routes/auth/sso/+page.server.ts b/frontend/src/routes/auth/sso/+page.server.ts index 1a87a7d6..df2560a8 100644 --- a/frontend/src/routes/auth/sso/+page.server.ts +++ b/frontend/src/routes/auth/sso/+page.server.ts @@ -76,10 +76,22 @@ export const load: PageServerLoad = async ({ url, cookies }) => { redirectToWorkspaceLogin(cookies, url); } - let tokens = await response.json(); + let tokens: Record; + try { + tokens = await response.json(); + } catch (err) { + console.error('[SSO] exchange devolvió body no-JSON (status 200):', err); + redirectToWorkspaceLogin(cookies, url); + } + + if (!tokens.access_token || typeof tokens.access_token !== 'string') { + console.error('[SSO] exchange exitoso pero access_token faltante o inválido:', tokens); + redirectToWorkspaceLogin(cookies, url); + } + console.log('[SSO] exchange exitoso, tokens recibidos:', { hasAccessToken: !!tokens.access_token, - accessTokenLen: tokens.access_token?.length, + accessTokenLen: (tokens.access_token as string).length, hasRefreshToken: !!tokens.refresh_token, tenant_id: tokens.tenant_id, tenant_slug: tokens.tenant_slug, @@ -89,7 +101,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => { // Los tokens del relay fueron emitidos por KC via el browser (iss=IP:8085). // El Hub backend valida contra KC interno (hub-keycloak:8080) → issuer mismatch → 401. // Refrescando aquí: Anexo76 backend → Hub → KC interno → iss=hub-keycloak:8080 → válido. - if (tokens.refresh_token) { + if (typeof tokens.refresh_token === 'string') { try { const internalApiUrl = ( process.env.INTERNAL_API_URL || @@ -102,7 +114,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => { body: JSON.stringify({ refresh_token: tokens.refresh_token }), }); if (refreshRes.ok) { - const refreshed = await refreshRes.json(); + const refreshed = await refreshRes.json().catch(() => ({})); if (refreshed.access_token && refreshed.refresh_token) { tokens = { ...tokens, ...refreshed }; console.log('[SSO] tokens refrescados exitosamente (iss normalizado)'); @@ -120,13 +132,13 @@ export const load: PageServerLoad = async ({ url, cookies }) => { console.log('[SSO] ORIGIN-based secure context:', isProduction); // access_token — NO HttpOnly (Bearer desde JS); fragmentado si el JWT supera ~4KB - setAccessTokenCookies(cookies, tokens.access_token, { + setAccessTokenCookies(cookies, tokens.access_token as string, { secure: isProduction, maxAge: 60 * 60 * 24 * 7, }); // refresh_token — HttpOnly (never exposed to JS) - if (tokens.refresh_token) { + if (typeof tokens.refresh_token === 'string') { cookies.set('refresh_token', tokens.refresh_token, { path: '/', httpOnly: true, @@ -139,7 +151,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => { // sso_tenant_id — HttpOnly cookie con el tenant seleccionado. // El backend lo pasa como X-Tenant-Override en Hub /auth/me para que // devuelva el tenant correcto aunque el KC token tenga otro tenant baked in. - if (tokens.tenant_id) { + if (typeof tokens.tenant_id === 'number') { cookies.set('sso_tenant_id', String(tokens.tenant_id), { path: '/', httpOnly: true, @@ -171,7 +183,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => { await fetch(`${internalApiUrl}/v1/auth/lazy-link`, { method: 'POST', headers: { - 'Authorization': `Bearer ${tokens.access_token}`, + 'Authorization': `Bearer ${tokens.access_token as string}`, 'Content-Type': 'application/json', }, }).catch(() => {});