fix(sso): reduce cookie chunk threshold and add defensive error handling in SSO exchange

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 08:30:15 -05:00
parent 35c47e32df
commit 228cd533d2
2 changed files with 22 additions and 10 deletions

View File

@@ -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';

View File

@@ -76,10 +76,22 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
redirectToWorkspaceLogin(cookies, url);
}
let tokens = await response.json();
let tokens: Record<string, unknown>;
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(() => {});