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:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
]);
|
||||
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user