diff --git a/frontend/src/lib/server/workspace-auth.ts b/frontend/src/lib/server/workspace-auth.ts index 34004463..278795f2 100644 --- a/frontend/src/lib/server/workspace-auth.ts +++ b/frontend/src/lib/server/workspace-auth.ts @@ -239,6 +239,20 @@ export function redirectToKeycloakLogin(systemBaseUrl: string, redirectPath: str throw redirect(303, buildKeycloakLoginUrl(systemBaseUrl, redirectPath)); } +/** + * URL del Hub FastAPI para llamadas server-to-server (ej. sso-exchange). + * No aplica isDevOnlyUrl: las URLs internas de Docker son válidas aquí. + * Lee HUB_BACKEND_URL (override explícito) → INTERNAL_HUB_URL (ya en docker-compose) + * → fallback a URL pública del workspace (vía proxy SvelteKit del Hub). + */ +export function getHubBackendUrl(): string { + const direct = + (env.HUB_BACKEND_URL || '').trim() || + (env.INTERNAL_HUB_URL || '').trim(); + if (direct) return stripTrailingSlashes(direct); + return getWorkspaceBaseUrl(); +} + export function buildKeycloakLogoutUrl(systemBaseUrl: string, idTokenHint?: string): string { const keycloakBaseUrl = getPublicKeycloakBaseUrl(); const postLogoutRedirectUri = `${systemBaseUrl}/auth/post-logout`; diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 2560714a..361a1c05 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -1,16 +1,64 @@ -import { redirect } from '@sveltejs/kit'; +import { redirect, isRedirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; import { clearAuthTokens } from '$lib/server/api'; +import { setAccessTokenCookies } from '$lib/server/access-token-cookie'; import { getWorkspaceLoginUrl, readWorkspaceReturnPath, + clearWorkspaceReturnPath, storeReturnPath, redirectToKeycloakAuthorization, - redirectToKeycloakLogin + redirectToKeycloakLogin, + getHubBackendUrl, + isSecureContext, } from '$lib/server/workspace-auth'; export const load: PageServerLoad = async ({ cookies, url }) => { console.error('[LOGIN] url.pathname:', url.pathname, '| params:', Object.fromEntries(url.searchParams)); + + // Relay SSO: el Hub App Launcher redirige aquí con ?relay=UUID4. + // Se canjea server-to-server (endpoint público, sin Bearer) por access_token + refresh_token. + const relayToken = url.searchParams.get('relay'); + if (relayToken) { + try { + const hubBackendUrl = getHubBackendUrl(); + const exchangeRes = await fetch(`${hubBackendUrl}/api/v1/auth/sso-exchange`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ relay_token: relayToken }) + }); + + if (exchangeRes.ok) { + const data = await exchangeRes.json(); + const isProduction = isSecureContext(); + + setAccessTokenCookies(cookies, data.access_token, { + secure: isProduction, + maxAge: 60 * 60 * 24 * 7 + }); + if (data.refresh_token) { + cookies.set('refresh_token', data.refresh_token, { + path: '/', + httpOnly: true, + secure: isProduction, + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 30 + }); + } + + const redirectTo = + url.searchParams.get('redirect') || + readWorkspaceReturnPath(cookies, '/dashboard'); + clearWorkspaceReturnPath(cookies); + throw redirect(303, redirectTo); + } + // relay inválido/expirado (410) o Hub no disponible → continúa al flujo normal + } catch (err) { + if (isRedirect(err)) throw err; + // silencio: cae al flujo normal de login + } + } + clearAuthTokens(cookies); // Workspace redirige de vuelta aquí con ?sso_verified=1 después de que el usuario