refactor: update SSO handling and remove deprecated components

- Changed the hub-net network configuration to external in docker-compose.
- Removed the Single Sign-On (SSO) service implementation and associated login form component.
- Enhanced authentication callback logic to improve error handling and redirect management.
- Updated various routes to streamline login and authentication processes, ensuring proper redirection to workspace login.
- Cleaned up unused code and improved overall structure for better maintainability.
This commit is contained in:
2026-05-11 17:17:37 -05:00
parent af415eb604
commit 52577b9e66
12 changed files with 266 additions and 664 deletions

View File

@@ -32,7 +32,7 @@ export const load: PageServerLoad = async ({ cookies, fetch }) => {
clearAuthTokens(cookies);
}
}
// Si no está autenticado, mostrar la página principal pública
return {
isAuthenticated: false

View File

@@ -1,6 +1,12 @@
import { redirect } from '@sveltejs/kit';
import { redirect, isRedirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { setAccessTokenCookies } from '$lib/server/access-token-cookie';
import {
clearWorkspaceReturnPath,
getWorkspaceLoginUrl,
readWorkspaceReturnPath,
storeReturnPath,
} from '$lib/server/workspace-auth';
export const load: PageServerLoad = async ({ url, cookies, fetch }) => {
// Obtener el código y state de los query params
@@ -10,13 +16,24 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => {
const errorDescription = url.searchParams.get('error_description');
if (errorParam) {
console.error('❌ [Callback Server] Error en autenticación:', errorParam, errorDescription);
throw redirect(303, `/login?error=${encodeURIComponent(errorDescription || errorParam)}`);
console.error('❌ [Callback Server] KC auth error:', errorParam, errorDescription);
// login_required means no KC session exists yet → send to Workspace login.
// Preserve the intended destination through the detour so /login can pick it up.
if (state) {
try {
const stateObj = JSON.parse(state);
const returnPath = stateObj.redirect_url;
if (returnPath && returnPath.startsWith('/') && returnPath !== '/login') {
storeReturnPath(cookies, returnPath);
}
} catch { /* ignore malformed state */ }
}
throw redirect(303, getWorkspaceLoginUrl(url.origin));
}
if (!code) {
console.error('❌ [Callback Server] No se recibió código de autorización');
throw redirect(303, '/login?error=No se recibió código de autorización');
throw redirect(303, getWorkspaceLoginUrl(url.origin));
}
try {
@@ -78,7 +95,7 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => {
}
// Obtener la URL de redirección del state o ir al dashboard
let redirectTo = '/dashboard';
let redirectTo = readWorkspaceReturnPath(cookies, '/dashboard');
if (state) {
try {
const stateObj = JSON.parse(state);
@@ -87,12 +104,15 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => {
console.warn('⚠️ [Callback Server] No se pudo obtener redirect_url del state');
}
}
clearWorkspaceReturnPath(cookies);
// Redirigir a la página de destino
throw redirect(303, redirectTo);
} catch (err: any) {
if (isRedirect(err)) throw err;
console.error('❌ [Callback Server] Error procesando autenticación:', err);
throw redirect(303, `/login?error=${encodeURIComponent(err.message || 'Error procesando autenticación')}`);
throw redirect(303, getWorkspaceLoginUrl(url.origin));
}
};

View File

@@ -7,6 +7,7 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { setAccessTokenCookies } from '$lib/server/access-token-cookie';
import { redirectToWorkspaceLogin } from '$lib/server/workspace-auth';
// Disable client-side rendering to prevent SvelteKit from making a second
// __data.json request that would consume the one-time relay token twice.
@@ -17,7 +18,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
console.log('[SSO] relay token presente:', !!relayToken);
if (!relayToken) {
throw redirect(303, '/login?error=sso_missing_token');
redirectToWorkspaceLogin(cookies, url);
}
// Limpiar sesión anterior para que el nuevo usuario reciba sus propias cookies.
@@ -51,7 +52,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
body: JSON.stringify({ relay_token: relayToken }),
});
} catch (err) {
throw redirect(303, '/login?error=sso_hub_unreachable');
redirectToWorkspaceLogin(cookies, url);
}
if (!response.ok) {
@@ -72,10 +73,10 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
throw redirect(303, '/dashboard');
}
throw redirect(303, `/login?error=${encodeURIComponent(detail)}`);
redirectToWorkspaceLogin(cookies, url);
}
const tokens = await response.json();
let tokens = await response.json();
console.log('[SSO] exchange exitoso, tokens recibidos:', {
hasAccessToken: !!tokens.access_token,
accessTokenLen: tokens.access_token?.length,
@@ -84,6 +85,36 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
tenant_slug: tokens.tenant_slug,
});
// ── Refresh proactivo ────────────────────────────────────────────────────
// 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) {
try {
const internalApiUrl = (
process.env.INTERNAL_API_URL ||
process.env.VITE_API_URL ||
'http://backend:8000/api/'
).replace(/\/+$/, '');
const refreshRes = await fetch(`${internalApiUrl}/v1/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: tokens.refresh_token }),
});
if (refreshRes.ok) {
const refreshed = await refreshRes.json();
if (refreshed.access_token && refreshed.refresh_token) {
tokens = { ...tokens, ...refreshed };
console.log('[SSO] tokens refrescados exitosamente (iss normalizado)');
}
} else {
console.warn('[SSO] refresh proactivo falló (status', refreshRes.status, ') — usando tokens originales del relay');
}
} catch (err) {
console.warn('[SSO] refresh proactivo error (non-blocking):', err);
}
}
const isProduction = process.env.NODE_ENV === 'production';
console.log('[SSO] NODE_ENV:', process.env.NODE_ENV, '→ isProduction:', isProduction);

View File

@@ -7,21 +7,22 @@ import {
getUserCompanies,
clearAuthTokens
} from '$lib/server/api';
import {
redirectToWorkspaceLogin
} from '$lib/server/workspace-auth';
export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
// Verificar si existe el token en las cookies
const { accessToken } = getAuthTokens(cookies);
console.log('[dashboard layout] access_token presente:', !!accessToken, '| url:', url.pathname);
// Si no hay token, redirigir al login, pero excluir la ruta /login para evitar bucle
if (!accessToken && url.pathname !== '/login') {
const redirectUrl = `/login?redirect=${encodeURIComponent(url.pathname)}`;
throw redirect(303, redirectUrl);
if (!accessToken) {
redirectToWorkspaceLogin(cookies, url);
}
// Validar el token con el backend y obtener datos del usuario
// La función validateAuth maneja automáticamente el refresh de tokens
const redirectOnFail = `/login?redirect=${encodeURIComponent(url.pathname)}`;
const redirectOnFail = undefined;
try {
// Primero my-companies: ejecuta get_current_user y puede crear tenant/empresa/usuario
@@ -75,15 +76,8 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
throw error;
}
// Si estamos ya en la página de login, no intentar redirigir de nuevo
if (url.pathname === '/login') {
console.error('🔐 [Dashboard] Error validando token en login page, limpiando cookies.');
clearAuthTokens(cookies);
return { authenticated: false, error: error };
}
// Para cualquier otro error (conexión, etc), limpiar token y redirigir
clearAuthTokens(cookies);
throw redirect(303, redirectOnFail);
redirectToWorkspaceLogin(cookies, url);
}
};

View File

@@ -21,7 +21,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
const accessToken = tokens.accessToken;
if (!accessToken) {
throw redirect(302, '/auth/login');
throw redirect(302, '/login');
}
try {
@@ -75,14 +75,14 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
{},
cookies,
fetch,
'/auth/login'
'/login'
),
authenticatedFetch(
'v1/public/reference_data/invoice-types?page=1&page_size=100',
{},
cookies,
fetch,
'/auth/login'
'/login'
)
]);

View File

@@ -1,85 +1,37 @@
import { redirect, fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { clearAuthTokens, setAuthTokens, getServerApiUrl } from '$lib/server/api';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { clearAuthTokens } from '$lib/server/api';
import {
getWorkspaceLoginUrl,
readWorkspaceReturnPath,
storeReturnPath,
redirectToKeycloakAuthorization
} from '$lib/server/workspace-auth';
export const load: PageServerLoad = async ({ cookies, url }) => {
// Si hay un parámetro 'logout' en la URL, limpiar las cookies
if (url.searchParams.has('logout')) {
clearAuthTokens(cookies);
return {};
}
// Limpiar siempre las cookies de sesión anterior al cargar login
// Esto evita que se queden datos del tenant anterior
clearAuthTokens(cookies);
// Permitir acceso al login sin redirigir automáticamente
// Esto evita bucles de redirección cuando el token existe pero puede estar expirado
return {};
};
// Workspace redirige de vuelta aquí con ?sso_verified=1 después de que el usuario
// se autenticó en Workspace (que usa el mismo Keycloak central).
// En ese momento la sesión KC ya existe en el browser → prompt=none funciona sin
// mostrar ninguna pantalla de login.
if (url.searchParams.get('sso_verified') === '1') {
const existingReturnPath = readWorkspaceReturnPath(cookies, '');
const intendedPath =
existingReturnPath && existingReturnPath !== '/login'
? existingReturnPath
: (url.searchParams.get('redirect') || '/dashboard');
export const actions = {
default: async ({ request, cookies, url, fetch }) => {
const data = await request.formData();
const username = data.get('username')?.toString();
const password = data.get('password')?.toString();
const tenant_slug = data.get('tenant_slug')?.toString();
if (!username || !password || !tenant_slug) {
return fail(400, { error: 'Credenciales incorrectas' });
}
try {
const apiUrl = getServerApiUrl();
const loginUrl = `${apiUrl}v1/auth/login`;
const requestBody = {
username,
password,
tenant_slug
};
const response = await fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
const result = await response.json();
if (!response.ok) {
return fail(response.status, {
error: result.detail || 'Error de autenticación',
username,
tenant_slug
});
}
if (result.access_token) {
// Establecer tokens usando la función centralizada
setAuthTokens(cookies, result.access_token, result.refresh_token);
// Redirigir al dashboard o a la URL original
const redirectUrl = url.searchParams.get('redirect') || '/dashboard';
throw redirect(303, redirectUrl);
}
return fail(500, { error: 'No se recibió token de autenticación' });
} catch (error) {
// Si es un redirect de SvelteKit, re-lanzarlo
if (error && typeof error === 'object' && 'status' in error && 'location' in error) {
throw error;
}
return fail(500, {
error: 'Error de conexión con el servidor: ' + (error instanceof Error ? error.message : String(error)),
username,
tenant_slug
});
}
storeReturnPath(cookies, intendedPath);
redirectToKeycloakAuthorization(url.origin, intendedPath);
}
} satisfies Actions;
// 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') {
storeReturnPath(cookies, intendedPath);
}
throw redirect(303, getWorkspaceLoginUrl(url.origin));
};

View File

@@ -1,9 +1 @@
<script lang="ts">
import LoginForm from "$lib/components/login-form.svelte";
</script>
<div class="bg-gradient-to-br from-slate-100 via-blue-50 to-slate-200 dark:from-slate-950 dark:via-blue-950/30 dark:to-slate-900 flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div class="w-full max-w-sm md:max-w-3xl">
<LoginForm />
</div>
</div>
<!-- Esta página nunca se renderiza: el load SSR siempre redirige al workspace. -->

View File

@@ -1,16 +1,10 @@
import { redirect } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
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 refreshToken = cookies.get('refresh_token');
// Redirigir al workspace (Hub) — es el sistema central de autenticación.
const hubPublicUrl = (env.HUB_URL || '').replace(/\/+$/, '');
const postLogoutUrl = hubPublicUrl
? `${hubPublicUrl}/login`
: `${new URL(request.url).origin}/login`;
const systemBaseUrl = new URL(request.url).origin;
// Eliminar todas las cookies de autenticación (access_token puede estar fragmentado)
clearAccessTokenCookies(cookies);
@@ -18,23 +12,7 @@ export const POST: RequestHandler = async ({ cookies, request }) => {
cookies.delete('active_company_id', { path: '/' });
cookies.delete('sso_tenant_id', { path: '/' });
cookies.delete('sso_tenant_pub', { path: '/' });
clearWorkspaceReturnPath(cookies);
// Llamar al Hub para revocar el refresh token (best-effort).
if (refreshToken) {
try {
const hubUrl = (env.INTERNAL_HUB_URL || env.HUB_URL || 'http://localhost:8001').replace(/\/+$/, '');
await fetch(`${hubUrl}/api/v1/auth/logout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refresh_token: refreshToken,
post_logout_redirect_uri: postLogoutUrl,
}),
});
} catch {
// Si falla la llamada al Hub, continuar de todos modos
}
}
throw redirect(303, postLogoutUrl);
throw redirect(303, buildKeycloakLogoutUrl(systemBaseUrl));
};