fix(auth): enhance logout and login flow with Keycloak integration and cookie management
This commit is contained in:
@@ -315,5 +315,5 @@ networks:
|
||||
driver: bridge
|
||||
|
||||
hub-net:
|
||||
external: false
|
||||
external: true
|
||||
name: aduanasoft-hub_default
|
||||
|
||||
@@ -180,7 +180,10 @@ async function refreshToken(): Promise<string | null> {
|
||||
if (!response.ok) {
|
||||
console.error('❌ [API] Silent refresh falló, status:', response.status);
|
||||
clearAccessTokenOnDocument();
|
||||
setTimeout(() => { window.location.href = '/login'; }, 1500);
|
||||
const hubBase = (import.meta.env.VITE_HUB_URL || 'https://workspace.aduanasoft.com').replace(/\/+$/, '');
|
||||
setTimeout(() => {
|
||||
window.location.href = `${hubBase}/login?return_to=${encodeURIComponent(window.location.origin + '/login?sso_verified=1')}`;
|
||||
}, 1500);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -662,7 +662,8 @@ export const logout = async () => {
|
||||
form.submit();
|
||||
} catch (err) {
|
||||
console.error('[auth] Error durante logout:', err);
|
||||
window.location.href = '/login';
|
||||
const hubBase = (import.meta.env.VITE_HUB_URL || 'https://workspace.aduanasoft.com').replace(/\/+$/, '');
|
||||
window.location.href = `${hubBase}/login?return_to=${encodeURIComponent(window.location.origin + '/login?sso_verified=1')}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ export function setAuthTokens(
|
||||
export function clearAuthTokens(cookies: Cookies) {
|
||||
clearAccessTokenCookies(cookies);
|
||||
cookies.delete('refresh_token', { path: '/' });
|
||||
cookies.delete('id_token', { path: '/' });
|
||||
cookies.delete('active_company_id', { path: '/' });
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,26 @@ export function buildKeycloakAuthorizationUrl(systemBaseUrl: string, redirectPat
|
||||
return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/auth?${params.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye URL de login directo en KC sin prompt=none.
|
||||
* Usa la sesión KC existente si la hay; si no, muestra el form de login.
|
||||
* Usar cuando se recibe ?redirect= del Hub (rompe el loop Hub↔login).
|
||||
*/
|
||||
export function buildKeycloakLoginUrl(systemBaseUrl: string, redirectPath: string): string {
|
||||
const keycloakBaseUrl = getPublicKeycloakBaseUrl();
|
||||
const redirectUri = `${systemBaseUrl}/auth/callback`;
|
||||
const state = JSON.stringify({ redirect_url: redirectPath });
|
||||
const params = new URLSearchParams({
|
||||
client_id: getKeycloakClientId(),
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope: 'openid',
|
||||
state
|
||||
});
|
||||
|
||||
return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/auth?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function redirectToWorkspaceLogin(cookies: Cookies, url: URL): never {
|
||||
storeWorkspaceReturnPath(cookies, url);
|
||||
throw redirect(303, getWorkspaceLoginUrl(url.origin));
|
||||
@@ -156,17 +176,23 @@ export function redirectToKeycloakAuthorization(systemBaseUrl: string, redirectP
|
||||
throw redirect(303, buildKeycloakAuthorizationUrl(systemBaseUrl, redirectPath));
|
||||
}
|
||||
|
||||
export function buildKeycloakLogoutUrl(systemBaseUrl: string): string {
|
||||
export function redirectToKeycloakLogin(systemBaseUrl: string, redirectPath: string): never {
|
||||
throw redirect(303, buildKeycloakLoginUrl(systemBaseUrl, redirectPath));
|
||||
}
|
||||
|
||||
export function buildKeycloakLogoutUrl(systemBaseUrl: string, idTokenHint?: string): string {
|
||||
const keycloakBaseUrl = getPublicKeycloakBaseUrl();
|
||||
// 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: postLogoutRedirectUri
|
||||
});
|
||||
|
||||
// Con id_token_hint KC acepta cualquier post_logout_redirect_uri sin necesidad
|
||||
// de que esté registrado explícitamente en el cliente.
|
||||
if (idTokenHint) {
|
||||
params.set('id_token_hint', idTokenHint);
|
||||
}
|
||||
|
||||
return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/logout?${params.toString()}`;
|
||||
}
|
||||
@@ -94,6 +94,16 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => {
|
||||
maxAge: 60 * 60 * 24 * 30 // 30 días
|
||||
});
|
||||
}
|
||||
|
||||
if (tokens.id_token) {
|
||||
cookies.set('id_token', tokens.id_token, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener la URL de redirección del state o ir al dashboard
|
||||
let redirectTo = readWorkspaceReturnPath(cookies, '/dashboard');
|
||||
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
getWorkspaceLoginUrl,
|
||||
readWorkspaceReturnPath,
|
||||
storeReturnPath,
|
||||
redirectToKeycloakAuthorization
|
||||
redirectToKeycloakAuthorization,
|
||||
redirectToKeycloakLogin
|
||||
} from '$lib/server/workspace-auth';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
console.error('[LOGIN] url.pathname:', url.pathname, '| params:', Object.fromEntries(url.searchParams));
|
||||
clearAuthTokens(cookies);
|
||||
|
||||
// Workspace redirige de vuelta aquí con ?sso_verified=1 después de que el usuario
|
||||
@@ -26,12 +28,22 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
redirectToKeycloakAuthorization(url.origin, intendedPath);
|
||||
}
|
||||
|
||||
// Sin sso_verified → primera visita o sesión expirada.
|
||||
// Guardar la ruta deseada y mandar al Workspace a autenticar.
|
||||
const intendedPath = url.searchParams.get('redirect') || '/dashboard';
|
||||
if (intendedPath !== '/dashboard') {
|
||||
// El Hub devolvió ?redirect= sin sso_verified=1.
|
||||
// Ir directo a Keycloak SIN prompt=none para romper el loop Hub↔login:
|
||||
// - KC tiene sesión activa → devuelve código al callback → éxito.
|
||||
// - KC no tiene sesión → muestra el form de login → callback → éxito.
|
||||
const redirectParam = url.searchParams.get('redirect');
|
||||
if (redirectParam) {
|
||||
const existingReturnPath = readWorkspaceReturnPath(cookies, '');
|
||||
const intendedPath =
|
||||
existingReturnPath && existingReturnPath !== '/login'
|
||||
? existingReturnPath
|
||||
: redirectParam;
|
||||
storeReturnPath(cookies, intendedPath);
|
||||
redirectToKeycloakLogin(url.origin, intendedPath);
|
||||
}
|
||||
|
||||
// Primera visita sin ningún parámetro → guardar destino y mandar al workspace.
|
||||
storeReturnPath(cookies, '/dashboard');
|
||||
throw redirect(303, getWorkspaceLoginUrl(url.origin));
|
||||
};
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { clearAccessTokenCookies } from '$lib/server/access-token-cookie';
|
||||
import { buildKeycloakLogoutUrl, clearWorkspaceReturnPath } from '$lib/server/workspace-auth';
|
||||
import {
|
||||
buildKeycloakLogoutUrl,
|
||||
clearWorkspaceReturnPath,
|
||||
getWorkspaceLoginUrl
|
||||
} from '$lib/server/workspace-auth';
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies, request, url }) => {
|
||||
export const POST: RequestHandler = async ({ cookies, url }) => {
|
||||
const systemBaseUrl = url.origin;
|
||||
|
||||
// Eliminar todas las cookies de autenticación (access_token puede estar fragmentado)
|
||||
const idToken = cookies.get('id_token');
|
||||
|
||||
// Eliminar todas las cookies de autenticación
|
||||
clearAccessTokenCookies(cookies);
|
||||
cookies.delete('refresh_token', { path: '/' });
|
||||
cookies.delete('id_token', { path: '/' });
|
||||
cookies.delete('active_company_id', { path: '/' });
|
||||
cookies.delete('sso_tenant_id', { path: '/' });
|
||||
cookies.delete('sso_tenant_pub', { path: '/' });
|
||||
clearWorkspaceReturnPath(cookies);
|
||||
|
||||
throw redirect(303, buildKeycloakLogoutUrl(systemBaseUrl));
|
||||
// Sin id_token_hint KC rechaza post_logout_redirect_uri no registrado.
|
||||
// En ese caso redirigir directo al workspace — las cookies ya están limpias.
|
||||
if (!idToken) {
|
||||
throw redirect(303, getWorkspaceLoginUrl(systemBaseUrl, { forPostLogout: true }));
|
||||
}
|
||||
|
||||
throw redirect(303, buildKeycloakLogoutUrl(systemBaseUrl, idToken));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user