Merge pull request 'fix/logout-anexo' (#392) from fix/logout-anexo into development

Reviewed-on: ADUANASOFT/anexo76#392
This commit is contained in:
2026-05-12 22:46:49 +00:00
9 changed files with 51 additions and 15 deletions

View File

@@ -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
});
}

View File

@@ -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()}`;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 }));
};

View File

@@ -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, {

View File

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