diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 916ce963..ff6e1c53 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -365,6 +365,8 @@ services: - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} + # SvelteKit ORIGIN — evita que request.url.origin use la IP/puerto interno del contenedor + - ORIGIN=${ORIGIN:-https://anexo76-dev.aduanasoft.com} ports: - "5111:5173" depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 28b35572..b948675a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -142,6 +142,8 @@ services: # CORS / CSRF — trusted origins para svelte.config.js - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3001} - TRUSTED_ORIGINS=${TRUSTED_ORIGINS:-} + # SvelteKit ORIGIN — evita que request.url.origin use la IP/puerto interno del contenedor + - ORIGIN=${ORIGIN:-http://localhost:5173} ports: - "5173:5173" depends_on: diff --git a/frontend/src/lib/server/api.ts b/frontend/src/lib/server/api.ts index 3cd200c9..6d8c9980 100644 --- a/frontend/src/lib/server/api.ts +++ b/frontend/src/lib/server/api.ts @@ -9,6 +9,7 @@ import { getAccessTokenFromCookies, setAccessTokenCookies } from '$lib/server/access-token-cookie'; +import { isSecureContext } from '$lib/server/workspace-auth'; /** * Obtiene y normaliza la URL base de la API para llamadas desde el servidor @@ -57,7 +58,7 @@ export function setAuthTokens( refreshToken?: string ) { setAccessTokenCookies(cookies, accessToken, { - secure: process.env.NODE_ENV === 'production', + secure: isSecureContext(), maxAge: 60 * 60 * 24 * 7 // 7 días }); @@ -66,7 +67,7 @@ export function setAuthTokens( path: '/', httpOnly: true, // *** HttpOnly: JS nunca lee el refresh_token *** sameSite: 'lax', - secure: process.env.NODE_ENV === 'production', + secure: isSecureContext(), maxAge: 60 * 60 * 24 * 30 // 30 días }); } diff --git a/frontend/src/lib/server/workspace-auth.ts b/frontend/src/lib/server/workspace-auth.ts index 317b045a..d1a4f628 100644 --- a/frontend/src/lib/server/workspace-auth.ts +++ b/frontend/src/lib/server/workspace-auth.ts @@ -4,6 +4,17 @@ import { redirect, type Cookies } from '@sveltejs/kit'; const DEFAULT_WORKSPACE_BASE_URL = 'https://workspace.aduanasoft.com'; const RETURN_PATH_COOKIE = 'workspace_return_path'; +/** + * Returns true only when the public-facing URL uses HTTPS. + * Use this for cookie `secure` flag instead of NODE_ENV so that + * cookies work on HTTP LAN dev environments (e.g. 192.168.x.x). + */ +export function isSecureContext(): boolean { + const origin = (env.ORIGIN || process.env.ORIGIN || '').trim(); + if (origin) return origin.startsWith('https://'); + return process.env.NODE_ENV === 'production'; +} + function stripTrailingSlashes(value: string): string { return value.replace(/\/+$/, ''); } @@ -51,9 +62,9 @@ export function getWorkspaceLoginUrl( if (options?.forPostLogout) { return `${workspaceBaseUrl}/login`; } - // return_to points to /login so that after Workspace auth the browser lands on - // /login, which immediately attempts a prompt=none KC auth. - const loginUrl = `${systemBaseUrl}/login`; + // return_to includes sso_verified=1 so the workspace preserves it when redirecting + // back, regardless of what additional params the workspace appends. + const loginUrl = `${systemBaseUrl}/login?sso_verified=1`; return `${workspaceBaseUrl}/login?return_to=${encodeURIComponent(loginUrl)}`; } @@ -63,7 +74,7 @@ export function storeReturnPath(cookies: Cookies, path: string): void { path: '/', httpOnly: true, sameSite: 'lax', - secure: env.NODE_ENV === 'production', + secure: isSecureContext(), maxAge: 60 * 10 }); } @@ -100,7 +111,7 @@ export function storeWorkspaceReturnPath(cookies: Cookies, url: URL): string { path: '/', httpOnly: true, sameSite: 'lax', - secure: env.NODE_ENV === 'production', + secure: isSecureContext(), maxAge: 60 * 10 }); @@ -147,10 +158,14 @@ export function redirectToKeycloakAuthorization(systemBaseUrl: string, redirectP export function buildKeycloakLogoutUrl(systemBaseUrl: string): string { const keycloakBaseUrl = getPublicKeycloakBaseUrl(); - const workspaceLoginUrl = getWorkspaceLoginUrl(systemBaseUrl, { forPostLogout: true }); + // post_logout_redirect_uri must be a URI registered in the KC client. + // The workspace login URL (workspace.aduanasoft.com/login) is NOT registered there. + // Use a local /auth/post-logout route which IS covered by the app's registered wildcard, + // then that route bounces to workspace login. + const postLogoutRedirectUri = `${systemBaseUrl}/auth/post-logout`; const params = new URLSearchParams({ client_id: getKeycloakClientId(), - post_logout_redirect_uri: workspaceLoginUrl + post_logout_redirect_uri: postLogoutRedirectUri }); return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/logout?${params.toString()}`; diff --git a/frontend/src/routes/api-sveltekit/auth/switch-tenant/+server.ts b/frontend/src/routes/api-sveltekit/auth/switch-tenant/+server.ts index cd2781f7..143fe048 100644 --- a/frontend/src/routes/api-sveltekit/auth/switch-tenant/+server.ts +++ b/frontend/src/routes/api-sveltekit/auth/switch-tenant/+server.ts @@ -40,7 +40,8 @@ export const POST = async ({ request, cookies, fetch }: RequestEvent) => { if (!hasAccess) { return json({ error: 'Access denied to tenant' }, { status: 403 }); } - const isProduction = process.env.NODE_ENV === 'production'; + const { isSecureContext } = await import('$lib/server/workspace-auth'); + const isProduction = isSecureContext(); cookies.set('sso_tenant_id', String(tenant_id), { path: '/', httpOnly: true, diff --git a/frontend/src/routes/auth/callback/+page.server.ts b/frontend/src/routes/auth/callback/+page.server.ts index d6d3e80b..3a7c6cc1 100644 --- a/frontend/src/routes/auth/callback/+page.server.ts +++ b/frontend/src/routes/auth/callback/+page.server.ts @@ -77,7 +77,8 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => { // Establecer las cookies en el servidor // access_token → NO HttpOnly (el cliente JS lo usa para el header Authorization) // refresh_token → HttpOnly (el JS nunca lo lee; el servidor lo gestiona) - const isProduction = process.env.NODE_ENV === 'production'; + const { isSecureContext } = await import('$lib/server/workspace-auth'); + const isProduction = isSecureContext(); setAccessTokenCookies(cookies, tokens.access_token, { secure: isProduction, diff --git a/frontend/src/routes/auth/post-logout/+server.ts b/frontend/src/routes/auth/post-logout/+server.ts new file mode 100644 index 00000000..97807e33 --- /dev/null +++ b/frontend/src/routes/auth/post-logout/+server.ts @@ -0,0 +1,13 @@ +import { redirect } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getWorkspaceLoginUrl } from '$lib/server/workspace-auth'; + +/** + * KC redirects here after completing the logout flow. + * This URL is covered by the app's registered wildcard in KC (e.g. anexo76-dev.aduanasoft.com/*). + * We then send the user to workspace login so it can apply myApps() launcher logic. + */ +export const GET: RequestHandler = async ({ request, url }) => { + const systemBaseUrl = url.origin; + throw redirect(303, getWorkspaceLoginUrl(systemBaseUrl, { forPostLogout: true })); +}; diff --git a/frontend/src/routes/auth/sso/+page.server.ts b/frontend/src/routes/auth/sso/+page.server.ts index 9cced10e..1a87a7d6 100644 --- a/frontend/src/routes/auth/sso/+page.server.ts +++ b/frontend/src/routes/auth/sso/+page.server.ts @@ -115,8 +115,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); + const { isSecureContext } = await import('$lib/server/workspace-auth'); + const isProduction = isSecureContext(); + 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, { diff --git a/frontend/src/routes/logout/+server.ts b/frontend/src/routes/logout/+server.ts index 0b0f6474..162e4a65 100644 --- a/frontend/src/routes/logout/+server.ts +++ b/frontend/src/routes/logout/+server.ts @@ -3,8 +3,8 @@ import type { RequestHandler } from './$types'; import { clearAccessTokenCookies } from '$lib/server/access-token-cookie'; import { buildKeycloakLogoutUrl, clearWorkspaceReturnPath } from '$lib/server/workspace-auth'; -export const POST: RequestHandler = async ({ cookies, request }) => { - const systemBaseUrl = new URL(request.url).origin; +export const POST: RequestHandler = async ({ cookies, request, url }) => { + const systemBaseUrl = url.origin; // Eliminar todas las cookies de autenticación (access_token puede estar fragmentado) clearAccessTokenCookies(cookies);