Merge pull request 'refactor: centralize access token handling in cookies and improve related functions' (#356) from feature/cookies into development

Reviewed-on: ADUANASOFT/anexo76#356
This commit is contained in:
2026-05-02 02:38:48 +00:00
31 changed files with 256 additions and 91 deletions

View File

@@ -1,6 +1,7 @@
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { sequence } from '@sveltejs/kit/hooks';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => {
event.request = request;
@@ -12,7 +13,7 @@ const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(even
const handleAuth: Handle = async ({ event, resolve }) => {
// Obtener el token de las cookies
const token = event.cookies.get('access_token');
const token = getAccessTokenFromCookies(event.cookies);
// Agregar el token a los locals para que esté disponible en toda la app
event.locals.token = token || null;

View File

@@ -0,0 +1,71 @@
import { browser } from '$app/environment';
import {
ACCESS_TOKEN_CHUNK_COUNT,
accessTokenChunkName,
splitAccessTokenForCookies,
ACCESS_TOKEN_MAX_CHUNKS
} from '$lib/access-token-cookie.shared';
function readCookieRaw(name: string): string | null {
if (!browser) return null;
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() ?? null;
return null;
}
export function getAccessTokenFromDocument(): string | null {
if (!browser) return null;
const countRaw = readCookieRaw(ACCESS_TOKEN_CHUNK_COUNT);
if (countRaw) {
const n = parseInt(countRaw, 10);
if (!Number.isFinite(n) || n < 1 || n > ACCESS_TOKEN_MAX_CHUNKS) return null;
let out = '';
for (let i = 0; i < n; i++) {
const p = readCookieRaw(accessTokenChunkName(i));
if (p == null) return null;
out += p;
}
return out;
}
return readCookieRaw('access_token');
}
export function hasAccessTokenInDocument(): boolean {
if (!browser) return false;
return !!(readCookieRaw('access_token') || readCookieRaw(ACCESS_TOKEN_CHUNK_COUNT));
}
export function clearAccessTokenOnDocument() {
if (!browser) return;
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
const blank = `; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secure}`;
const clear = (name: string) => {
document.cookie = `${name}=${blank}`;
};
clear('access_token');
clear(ACCESS_TOKEN_CHUNK_COUNT);
for (let i = 0; i < ACCESS_TOKEN_MAX_CHUNKS; i++) {
clear(accessTokenChunkName(i));
}
}
/** Misma política que auth setCookie: expires + SameSite + Secure en HTTPS. */
export function setAccessTokenOnDocument(token: string, days: number = 7) {
if (!browser) return;
clearAccessTokenOnDocument();
const exp = new Date();
exp.setDate(exp.getDate() + days);
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
const suffix = `; path=/; expires=${exp.toUTCString()}; SameSite=Lax${secure}`;
const split = splitAccessTokenForCookies(token);
if (split.kind === 'single') {
document.cookie = `access_token=${split.value}${suffix}`;
return;
}
document.cookie = `${ACCESS_TOKEN_CHUNK_COUNT}=${split.parts.length}${suffix}`;
split.parts.forEach((part, i) => {
document.cookie = `${accessTokenChunkName(i)}=${part}${suffix}`;
});
}

View File

@@ -0,0 +1,32 @@
/**
* Fragmentación del JWT access_token en varias cookies cuando supera el límite ~4KB del navegador.
* La lógica de fetch / Bearer no cambia: solo lectura/escritura de cookies.
*/
/** Por debajo de esto se usa una sola cookie `access_token` (compatibilidad). */
export const ACCESS_TOKEN_MAX_SINGLE = 3500;
export const ACCESS_TOKEN_CHUNK_SIZE = 3500;
/** 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';
export const accessTokenChunkName = (index: number) => `access_token_${index}`;
export type AccessTokenSplit =
| { kind: 'single'; value: string }
| { kind: 'chunks'; parts: string[] };
export function splitAccessTokenForCookies(token: string): AccessTokenSplit {
if (token.length <= ACCESS_TOKEN_MAX_SINGLE) {
return { kind: 'single', value: token };
}
const parts: string[] = [];
for (let i = 0; i < token.length; i += ACCESS_TOKEN_CHUNK_SIZE) {
parts.push(token.slice(i, i + ACCESS_TOKEN_CHUNK_SIZE));
}
return { kind: 'chunks', parts };
}
/** Máximo de fragmentos soportados (JWT muy grande). */
export const ACCESS_TOKEN_MAX_CHUNKS = 32;

View File

@@ -4,9 +4,23 @@
import { getToken } from './auth';
import { browser } from '$app/environment';
import { toast } from 'svelte-sonner';
import { clearAccessTokenOnDocument, setAccessTokenOnDocument } from '$lib/access-token-cookie-browser';
// Normalize API_BASE_URL to remove trailing slash
const API_BASE_URL = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, '');
/** Base URL absoluta para fetch; corrige `http:host` sin `//` y añade `http://` si no hay esquema. */
function normalizeAbsoluteApiBaseUrl(raw: string): string {
let s = (raw ?? '').trim().replace(/\/+$/, '');
if (!s) return '';
if (s.startsWith('http:') && !s.startsWith('http://')) {
s = 'http://' + s.slice('http:'.length).replace(/^\/+/, '');
}
if (s.startsWith('https:') && !s.startsWith('https://')) {
s = 'https://' + s.slice('https:'.length).replace(/^\/+/, '');
}
if (/^https?:\/\//i.test(s)) return s;
return `http://${s.replace(/^\/+/, '')}`;
}
const API_BASE_URL = normalizeAbsoluteApiBaseUrl(String(import.meta.env.VITE_API_URL ?? ''));
export interface ApiResponse<T = any> {
data?: T;
@@ -164,8 +178,7 @@ async function refreshToken(): Promise<string | null> {
if (!response.ok) {
console.error('❌ [API] Silent refresh falló, status:', response.status);
// Limpiar la cookie del access_token (no HttpOnly) para forzar re-login
document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
clearAccessTokenOnDocument();
setTimeout(() => { window.location.href = '/login'; }, 1500);
return null;
}
@@ -173,9 +186,7 @@ async function refreshToken(): Promise<string | null> {
const data = await response.json() as { access_token?: string };
if (data.access_token) {
// Actualizar cookie no-HttpOnly del access_token
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `access_token=${data.access_token}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Lax${secure}`;
setAccessTokenOnDocument(data.access_token);
// Actualizar authStore en memoria
try {

View File

@@ -2,7 +2,7 @@
* Servicio de autenticación con Keycloak
*
* Seguridad de tokens:
* - access_token → en memoria (authStore) + cookie no-HttpOnly (para SSR)
* - access_token → en memoria (authStore) + cookies no-HttpOnly (una o varias si el JWT es grande)
* - refresh_token → cookie HttpOnly únicamente (JS nunca lo lee directamente)
* - El refresh se hace server-side via /api-sveltekit/auth/silent-refresh
* - NO se usa localStorage para tokens
@@ -11,6 +11,11 @@
import Keycloak from 'keycloak-js';
import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';
import {
clearAccessTokenOnDocument,
getAccessTokenFromDocument,
setAccessTokenOnDocument
} from '$lib/access-token-cookie-browser';
// ─────────────────────────────────────────────────────────
// Tipos
@@ -45,35 +50,6 @@ const keycloakConfig = {
let keycloakInstance: Keycloak | null = null;
// ─────────────────────────────────────────────────────────
// Cookie helpers (solo para access_token no-HttpOnly)
// ─────────────────────────────────────────────────────────
/** Lee el valor de una cookie no-HttpOnly */
const getCookie = (name: string): string | null => {
if (!browser) return null;
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() ?? null;
return null;
};
/** Escribe una cookie no-HttpOnly */
const setCookie = (name: string, value: string, days: number = 7) => {
if (!browser) return;
const exp = new Date();
exp.setDate(exp.getDate() + days);
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `${name}=${value}; path=/; expires=${exp.toUTCString()}; SameSite=Lax${secure}`;
};
/** Elimina una cookie */
const deleteCookie = (name: string) => {
if (!browser) return;
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secure}`;
};
// ─────────────────────────────────────────────────────────
// Auth store (tokens solo en memoria)
// ─────────────────────────────────────────────────────────
@@ -139,7 +115,7 @@ export const initAuth = async (): Promise<boolean> => {
authStore.setLoading(true);
// Restaurar token desde cookie no-HttpOnly (password login flow)
const cookieToken = getCookie('access_token');
const cookieToken = getAccessTokenFromDocument();
if (cookieToken) {
authStore.setToken(cookieToken);
authStore.setAuthenticated(true);
@@ -309,7 +285,7 @@ export const loginWithKeycloak = async (tenantSlug?: string) => {
/**
* Login con usuario/contraseña (legacy — el login principal es via form action del servidor).
* Los tokens se guardan en cookies (vía setCookie) y en memoria (authStore).
* Los tokens se guardan en cookies no-HttpOnly (fragmentadas si hace falta) y en memoria (authStore).
* NO se guardan en localStorage.
*/
export const login = async (credentials: {
@@ -330,7 +306,7 @@ export const login = async (credentials: {
// Guardar en memoria y en cookie no-HttpOnly para SSR
authStore.setToken(loginData.access_token);
authStore.setAuthenticated(true);
setCookie('access_token', loginData.access_token);
setAccessTokenOnDocument(loginData.access_token);
// El refresh_token llega en cookie HttpOnly desde el servidor.
// NO lo guardamos en JS.
await loadUserInfo(loginData.access_token);
@@ -401,8 +377,7 @@ export const logout = async () => {
// Limpiar estado en memoria
authStore.reset();
// Eliminar cookie no-HttpOnly del access_token
deleteCookie('access_token');
clearAccessTokenOnDocument();
// La cookie HttpOnly del refresh_token la limpia el servidor
// Logout unificado (SSO y password): POST al logout route del servidor.
@@ -445,7 +420,7 @@ export const getToken = (): string | null => {
if (token) return token;
// Prioridad 3: cookie no-HttpOnly (fallback para acceso inicial antes del onMount)
if (browser) return getCookie('access_token');
if (browser) return getAccessTokenFromDocument();
return null;
};
@@ -488,7 +463,7 @@ export const refreshAccessToken = async (): Promise<boolean> => {
const data = await resp.json() as { access_token?: string };
if (data.access_token) {
authStore.setToken(data.access_token);
setCookie('access_token', data.access_token);
setAccessTokenOnDocument(data.access_token);
return true;
}
} catch (err) {

View File

@@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { GLOBAL_NAV } from '$lib/config/shortcuts';
import { hasAccessTokenInDocument } from '$lib/access-token-cookie-browser';
import { shortcutStore, activeShortcutsList } from '$lib/stores/shortcut-store';
import { focusStore, interactionMode } from '$lib/stores/focus-store';
import ShortcutsHelpModal from './ShortcutsHelpModal.svelte';
@@ -30,7 +31,7 @@
const currentPath = $page?.url?.pathname || '';
const isAuthenticatedRoute = currentPath.startsWith('/dashboard');
const hasAccessToken =
document.cookie.includes('access_token=') || localStorage.getItem('access_token');
hasAccessTokenInDocument() || localStorage.getItem('access_token');
return isAuthenticatedRoute && !!hasAccessToken;
}

View File

@@ -1,20 +1,21 @@
<script lang="ts">
import * as Card from "$lib/components/ui/card/index.ts";
import * as Card from "$lib/components/ui/card/index";
import {
FieldGroup,
Field,
FieldLabel,
FieldDescription,
} from "$lib/components/ui/field/index.ts";
import { Input } from "$lib/components/ui/input/index.ts";
import { Button } from "$lib/components/ui/button/index.ts";
import { cn } from "$lib/utils.ts";
} from "$lib/components/ui/field/index";
import { Input } from "$lib/components/ui/input/index";
import { Button } from "$lib/components/ui/button/index";
import { cn } from "$lib/utils";
import faviconUrl from '$lib/assets/favicon.svg';
import type { HTMLAttributes } from "svelte/elements";
import { page } from '$app/state';
import { enhance } from '$app/forms';
import { loginWithProvider } from '$lib/sso.ts';
import { loginWithProvider } from '$lib/sso';
import { onMount, tick } from 'svelte';
import { clearAccessTokenOnDocument } from '$lib/access-token-cookie-browser';
let { class: className, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
@@ -51,7 +52,7 @@
if (typeof document !== 'undefined') {
const isSecure = window.location.protocol === 'https:';
const secureFlag = isSecure ? '; Secure' : '';
document.cookie = `access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`;
clearAccessTokenOnDocument();
document.cookie = `refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`;
document.cookie = `active_company_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`;
}

View File

@@ -3,6 +3,7 @@ import { shortcutStore, type ShortcutDef } from '$lib/stores/shortcut-store';
import { page } from '$app/stores';
import { get } from 'svelte/store';
import { browser } from '$app/environment';
import { hasAccessTokenInDocument } from '$lib/access-token-cookie-browser';
/**
* Hook to register shortcuts for a component lifecycle.
@@ -19,7 +20,7 @@ export function useShortcuts(context: string, shortcuts: ShortcutDef[]) {
const isAuthenticatedRoute = currentPath.startsWith('/dashboard');
// Verificar también que haya un token de acceso
const hasAccessToken = document.cookie.includes('access_token=') ||
const hasAccessToken = hasAccessTokenInDocument() ||
localStorage.getItem('access_token');
if (isAuthenticatedRoute && hasAccessToken) {

View File

@@ -0,0 +1,55 @@
import type { Cookies } from '@sveltejs/kit';
import {
ACCESS_TOKEN_CHUNK_COUNT,
accessTokenChunkName,
splitAccessTokenForCookies,
ACCESS_TOKEN_MAX_CHUNKS
} from '$lib/access-token-cookie.shared';
export function getAccessTokenFromCookies(cookies: Cookies): string | null {
const countRaw = cookies.get(ACCESS_TOKEN_CHUNK_COUNT);
if (countRaw) {
const n = parseInt(countRaw, 10);
if (!Number.isFinite(n) || n < 1 || n > ACCESS_TOKEN_MAX_CHUNKS) return null;
let out = '';
for (let i = 0; i < n; i++) {
const p = cookies.get(accessTokenChunkName(i));
if (p == null) return null;
out += p;
}
return out;
}
return cookies.get('access_token') ?? null;
}
export function clearAccessTokenCookies(cookies: Cookies) {
cookies.delete('access_token', { path: '/' });
cookies.delete(ACCESS_TOKEN_CHUNK_COUNT, { path: '/' });
for (let i = 0; i < ACCESS_TOKEN_MAX_CHUNKS; i++) {
cookies.delete(accessTokenChunkName(i), { path: '/' });
}
}
export function setAccessTokenCookies(
cookies: Cookies,
token: string,
opts: { secure: boolean; maxAge: number }
) {
clearAccessTokenCookies(cookies);
const split = splitAccessTokenForCookies(token);
const base = {
path: '/',
httpOnly: false as const,
sameSite: 'lax' as const,
secure: opts.secure,
maxAge: opts.maxAge
};
if (split.kind === 'single') {
cookies.set('access_token', split.value, base);
return;
}
cookies.set(ACCESS_TOKEN_CHUNK_COUNT, String(split.parts.length), base);
split.parts.forEach((part, i) => {
cookies.set(accessTokenChunkName(i), part, base);
});
}

View File

@@ -4,6 +4,11 @@
*/
import { redirect, type Cookies } from '@sveltejs/kit';
import {
clearAccessTokenCookies,
getAccessTokenFromCookies,
setAccessTokenCookies
} from '$lib/server/access-token-cookie';
/**
* Obtiene y normaliza la URL base de la API para llamadas desde el servidor
@@ -34,7 +39,7 @@ export function getServerApiUrl(): string {
*/
export function getAuthTokens(cookies: Cookies) {
return {
accessToken: cookies.get('access_token'),
accessToken: getAccessTokenFromCookies(cookies),
refreshToken: cookies.get('refresh_token')
};
}
@@ -43,7 +48,7 @@ export function getAuthTokens(cookies: Cookies) {
* Establece los tokens de autenticación en las cookies
*
* Política de seguridad:
* - access_token → NO HttpOnly (el cliente necesita incluirlo en el header Authorization)
* - access_token → NO HttpOnly (Bearer desde JS); si el JWT es muy grande, varias cookies fragmentadas
* - refresh_token → HttpOnly=true (JS nunca lo lee; el servidor lo maneja via /api-sveltekit/auth/silent-refresh)
*/
export function setAuthTokens(
@@ -51,10 +56,7 @@ export function setAuthTokens(
accessToken: string,
refreshToken?: string
) {
cookies.set('access_token', accessToken, {
path: '/',
httpOnly: false, // El cliente JS necesita leerlo para el header Bearer
sameSite: 'lax',
setAccessTokenCookies(cookies, accessToken, {
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7 // 7 días
});
@@ -74,7 +76,7 @@ export function setAuthTokens(
* Limpia todos los tokens de autenticación de las cookies
*/
export function clearAuthTokens(cookies: Cookies) {
cookies.delete('access_token', { path: '/' });
clearAccessTokenCookies(cookies);
cookies.delete('refresh_token', { path: '/' });
cookies.delete('active_company_id', { path: '/' });
}

View File

@@ -15,6 +15,7 @@
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import { getServerApiUrl, setAuthTokens } from '$lib/server/api';
import { clearAccessTokenCookies } from '$lib/server/access-token-cookie';
export const POST = async ({ cookies, fetch }: RequestEvent) => {
const refreshToken = cookies.get('refresh_token');
@@ -36,7 +37,7 @@ export const POST = async ({ cookies, fetch }: RequestEvent) => {
// El refresh token expiró o fue invalidado por Keycloak (sesión terminada).
// Limpiar las cookies para que el servidor redirigir al login en la siguiente carga.
cookies.delete('refresh_token', { path: '/' });
cookies.delete('access_token', { path: '/' });
clearAccessTokenCookies(cookies);
cookies.delete('active_company_id', { path: '/' });
const status = response.status === 401 ? 401 : 400;

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
// Obtener company_id de la cookie
const companyId = cookies.get('active_company_id');

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url, params }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
const { id } = params;
// Obtener company_id de la cookie o query params

View File

@@ -3,9 +3,10 @@
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, fetch }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
if (!token) {
// Limpiar cualquier cookie de compañía si no hay autenticación

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
// Configurar la URL de la API usando las variables de entorno
let apiUrl = process.env.INTERNAL_API_URL;

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
// Obtener company_id de la cookie
const companyId = cookies.get('active_company_id');

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
// Obtener company_id de la cookie
const companyId = cookies.get('active_company_id');

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url, params }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
const { id } = params;
// Obtener company_id de la cookie o query params

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
// Configurar la URL de la API usando las variables de entorno
let apiUrl = process.env.INTERNAL_API_URL;

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
// Configurar la URL de la API usando las variables de entorno
let apiUrl = process.env.INTERNAL_API_URL;

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
// Obtener company_id de la cookie
const companyId = cookies.get('active_company_id');

View File

@@ -1,7 +1,8 @@
import type { RequestHandler } from './$types';
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
export const GET: RequestHandler = async ({ cookies, params, url }) => {
const token = cookies.get('access_token');
const token = getAccessTokenFromCookies(cookies);
const { id } = params;
// Obtener company_id de la cookie o query params

View File

@@ -1,5 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { setAccessTokenCookies } from '$lib/server/access-token-cookie';
export const load: PageServerLoad = async ({ url, cookies, fetch }) => {
// Obtener el código y state de los query params
@@ -61,11 +62,8 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => {
// refresh_token → HttpOnly (el JS nunca lo lee; el servidor lo gestiona)
const isProduction = process.env.NODE_ENV === 'production';
cookies.set('access_token', tokens.access_token, {
path: '/',
httpOnly: false, // El cliente necesita leerlo para Bearer
setAccessTokenCookies(cookies, tokens.access_token, {
secure: isProduction,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 días
});

View File

@@ -6,6 +6,7 @@
*/
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { setAccessTokenCookies } from '$lib/server/access-token-cookie';
export const load: PageServerLoad = async ({ url, cookies }) => {
const relayToken = url.searchParams.get('relay');
@@ -54,12 +55,9 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
const isProduction = process.env.NODE_ENV === 'production';
console.log('[SSO] NODE_ENV:', process.env.NODE_ENV, '→ isProduction:', isProduction);
// access_token — NO HttpOnly (client JS reads it for Bearer headers)
cookies.set('access_token', tokens.access_token, {
path: '/',
httpOnly: false,
// access_token — NO HttpOnly (Bearer desde JS); fragmentado si el JWT supera ~4KB
setAccessTokenCookies(cookies, tokens.access_token, {
secure: isProduction,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
});

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { getAccessTokenFromDocument } from '$lib/access-token-cookie-browser';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import {
@@ -56,7 +57,7 @@
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
const cookieToken = getCookie('access_token');
const cookieToken = getAccessTokenFromDocument();
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken)
localStorage.setItem('access_token', cookieToken);

View File

@@ -22,6 +22,7 @@
import { Label } from '$lib/components/ui/label';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { getAccessTokenFromDocument } from '$lib/access-token-cookie-browser';
import { currentUser } from '$lib/auth';
import { companyStore } from '$lib/stores/company.svelte';
import { getCompany } from '$lib/api/dashboard/a76/general_catalogs/company';
@@ -193,7 +194,7 @@
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const cookieToken = getAccessTokenFromDocument();
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {

View File

@@ -10,8 +10,9 @@
import * as Dialog from '$lib/components/ui/dialog';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { getAccessTokenFromDocument } from '$lib/access-token-cookie-browser';
import { companyStore } from '$lib/stores/company.svelte';
import { Edit, Send, Plus, Trash2, RefreshCw } from 'lucide-svelte';
import { Edit, Send, Plus, Trash2, RefreshCw } from 'lucide-svelte';
import { obtenerAtajosListaPedimento } from '$lib/config/shortcuts/dashboard/pedimentos/list';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { goto } from '$app/navigation';
@@ -45,7 +46,7 @@ let filters = $state({
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const cookieToken = getAccessTokenFromDocument();
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {

View File

@@ -13,6 +13,7 @@
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/countries/list';
import { browser } from '$app/environment';
import { currentUser, userHasPermission } from '$lib/auth';
import { getAccessTokenFromDocument } from '$lib/access-token-cookie-browser';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
// Los datos iniciales vienen del servidor
@@ -36,7 +37,7 @@
return null;
};
const cookieToken = getCookie('access_token');
const cookieToken = getAccessTokenFromDocument();
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {

View File

@@ -13,6 +13,7 @@
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/currency_types/list';
import { browser } from '$app/environment';
import { currentUser, userHasPermission } from '$lib/auth';
import { getAccessTokenFromDocument } from '$lib/access-token-cookie-browser';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
// Los datos iniciales vienen del servidor
@@ -36,7 +37,7 @@
return null;
};
const cookieToken = getCookie('access_token');
const cookieToken = getAccessTokenFromDocument();
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { getAccessTokenFromDocument } from '$lib/access-token-cookie-browser';
import { page } from '$app/stores';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
@@ -26,7 +27,7 @@
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
const cookieToken = getCookie('access_token');
const cookieToken = getAccessTokenFromDocument();
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
}

View File

@@ -1,6 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';
import { clearAccessTokenCookies } from '$lib/server/access-token-cookie';
export const POST: RequestHandler = async ({ cookies, request }) => {
const refreshToken = cookies.get('refresh_token');
@@ -14,8 +15,8 @@ export const POST: RequestHandler = async ({ cookies, request }) => {
? `${hubPublicUrl}/login`
: `${new URL(request.url).origin}/login`;
// Eliminar todas las cookies de autenticación
cookies.delete('access_token', { path: '/' });
// Eliminar todas las cookies de autenticación (access_token puede estar fragmentado)
clearAccessTokenCookies(cookies);
cookies.delete('refresh_token', { path: '/' });
cookies.delete('active_company_id', { path: '/' });
cookies.delete('sso_tenant_id', { path: '/' });