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:
@@ -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';
|
||||
|
||||
@@ -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(() => {});
|
||||
|
||||
Reference in New Issue
Block a user