diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..2baf577 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,5 @@ +{ + "format_on_save": "off", + "remove_trailing_whitespace_on_save": false, + "ensure_final_newline_on_save": false +} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index c7c4a28..aace3e3 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -56,16 +56,16 @@ const redisClient = redisService.getClient(); function initHealthChecks() { healthCheckRegistry.register({ serviceName: 'OBP API', - url: `${PUBLIC_OBP_BASE_URL}/obp/v5.1.0/root`, + url: `${PUBLIC_OBP_BASE_URL}/obp/v5.1.0/root` }); healthCheckRegistry.register({ serviceName: 'Opey II', - url: `${env.OPEY_BASE_URL}/status`, + url: `${env.OPEY_BASE_URL}/status` }); const redisHealthCheck = new RedisHealthCheckService(); - healthCheckRegistry.register(redisHealthCheck) + healthCheckRegistry.register(redisHealthCheck); const oauthHealthChecks = oauth2ProviderManager.getHealthCheckEntries(); for (const check of oauthHealthChecks) { @@ -79,7 +79,6 @@ await oauth2ProviderManager.start(); initHealthChecks(); - async function initWebUIProps() { try { const webuiProps = await obp_requests.get('/obp/v5.1.0/webui-props'); @@ -91,58 +90,58 @@ async function initWebUIProps() { } } - function needsAuthorization(routeId: string): boolean { // protected routes are put in the /(protected)/ route group return routeId.startsWith('/(protected)/'); } const checkSessionValidity: Handle = async ({ event, resolve }) => { - const session = event.locals.session; - if (session.data.user) { - // Here you can add additional checks if needed - // For example, check if the session has expired based on your own logic - // or if certain required data is present in the session - const sessionOAuth = SessionOAuthHelper.getSessionOAuth(session); - if (!sessionOAuth) { - logger.warn('No valid OAuth data found in session. Destroying session.'); - await session.destroy(); - - // Redirect to trigger a fresh load instead of just resolving - throw redirect(302, event.url.pathname); - } - - const sessionExpired = await sessionOAuth.client.checkAccessTokenExpiration(sessionOAuth.accessToken) - // Check if the access token is expired, - // if it is, attempt to refresh it - if (sessionExpired) { - // will return true if the token is expired - try { - await SessionOAuthHelper.refreshAccessToken(session); - return await resolve(event); - } catch (error) { - logger.info( - 'Token refresh failed - redirecting user to login (normal OAuth behavior):', - error - ); - // If the refresh fails, redirect to login - // Destroy the session - logger.info('Destroying expired session.'); - await session.destroy(); - // Redirect to trigger a fresh load and clear client-side cache - throw redirect(302, event.url.pathname); - } - } - - // If we reach here, the session is valid (either not expired or successfully refreshed) - logger.debug('Session is valid for user:', session.data.user?.username); - return await resolve(event); - - } - - // Always return a response, even when there's no session - return await resolve(event); -} + const session = event.locals.session; + if (session.data.user) { + // Here you can add additional checks if needed + // For example, check if the session has expired based on your own logic + // or if certain required data is present in the session + const sessionOAuth = SessionOAuthHelper.getSessionOAuth(session); + if (!sessionOAuth) { + logger.warn('No valid OAuth data found in session. Destroying session.'); + await session.destroy(); + + // Redirect to trigger a fresh load instead of just resolving + throw redirect(302, event.url.pathname); + } + + const sessionExpired = await sessionOAuth.client.checkAccessTokenExpiration( + sessionOAuth.accessToken + ); + // Check if the access token is expired, + // if it is, attempt to refresh it + if (sessionExpired) { + // will return true if the token is expired + try { + await SessionOAuthHelper.refreshAccessToken(session); + return await resolve(event); + } catch (error) { + logger.info( + 'Token refresh failed - redirecting user to login (normal OAuth behavior):', + error + ); + // If the refresh fails, redirect to login + // Destroy the session + logger.info('Destroying expired session.'); + await session.destroy(); + // Redirect to trigger a fresh load and clear client-side cache + throw redirect(302, event.url.pathname); + } + } + + // If we reach here, the session is valid (either not expired or successfully refreshed) + logger.debug('Session is valid for user:', session.data.user?.username); + return await resolve(event); + } + + // Always return a response, even when there's no session + return await resolve(event); +}; // Middleware to check user authorization const checkAuthorization: Handle = async ({ event, resolve }) => { @@ -152,10 +151,9 @@ const checkAuthorization: Handle = async ({ event, resolve }) => { if (!!routeId && needsAuthorization(routeId)) { logger.debug('Checking authorization for user route:', event.url.pathname); if (!oauth2ProviderManager.isReady()) { - logger.warn('OAuth2 providers not ready:', oauth2ProviderManager.getStatus().error); + logger.warn('OAuth2 providers not ready'); throw error(503, 'Service Unavailable. Please try again later.'); } - if (!session || !session.data.user) { // Redirect to login page if not authenticated @@ -177,7 +175,7 @@ const checkAuthorization: Handle = async ({ event, resolve }) => { // Init SvelteKitSessions export const handle: Handle = sequence( - sveltekitSessionHandle({ + sveltekitSessionHandle({ secret: 'secret', store: new RedisStore({ client: redisClient }) }), @@ -193,16 +191,16 @@ declare module 'svelte-kit-sessions' { user_id: string; email: string; username: string; - entitlements: { + entitlements: { list: Array<{ entitlement_id: string; role_name: string; bank_id: string; - }> - } + }>; + }; views: { list: object[]; - } + }; }; oauth?: { access_token: string; diff --git a/src/lib/components/OpeyChat.svelte b/src/lib/components/OpeyChat.svelte index 45d23fd..e4cd85e 100644 --- a/src/lib/components/OpeyChat.svelte +++ b/src/lib/components/OpeyChat.svelte @@ -176,7 +176,7 @@ let authPipOpenState = $state(false); // async function formatAuthStatusPip(session: SessionSnapshot, consentInfo?: OBPConsentInfo): { - // const + // const // } async function sendMessage(text: string) { @@ -255,7 +255,7 @@ */ async function upgradeSession() { if (!userAuthenticated) { - window.location.href = '/login/obp'; + window.location.href = '/login'; return; } @@ -376,7 +376,7 @@ {#if options.displayConnectionPips}
- - - + - + {#if messageInput.length > 0 && !isMultiline} - + {@render statusPips(session, options.currentConsentInfo)}
diff --git a/src/lib/oauth/client.ts b/src/lib/oauth/client.ts index 6952a9e..f33c9ae 100644 --- a/src/lib/oauth/client.ts +++ b/src/lib/oauth/client.ts @@ -1,17 +1,30 @@ import { createLogger } from '$lib/utils/logger'; const logger = createLogger('OAuth2Client'); -import { OAuth2Client } from "arctic"; -import type { OpenIdConnectConfiguration, OAuth2AccessTokenPayload } from "$lib/oauth/types"; -import { jwtDecode } from "jwt-decode"; +import { OAuth2Client } from 'arctic'; +import type { OpenIdConnectConfiguration, OAuth2AccessTokenPayload } from '$lib/oauth/types'; +import { jwtDecode } from 'jwt-decode'; export class OAuth2ClientWithConfig extends OAuth2Client { - OIDCConfig?: OpenIdConnectConfiguration; + OIDCConfig?: OpenIdConnectConfiguration; + private readonly storedClientId: string; + private readonly storedClientSecret: string; + private readonly storedRedirectURI: string; + private readonly providerType: string; - constructor(clientId: string, clientSecret: string, redirectUri: string) { - super(clientId, clientSecret, redirectUri); + constructor( + clientId: string, + clientSecret: string, + redirectUri: string, + providerType: string = 'default' + ) { + super(clientId, clientSecret, redirectUri); - // get the OIDC configuration from the well-known URL if provided - } + // Store credentials for our custom methods to access private properties + this.storedClientId = clientId; + this.storedClientSecret = clientSecret; + this.storedRedirectURI = redirectUri; + this.providerType = providerType; + } async initOIDCConfig(OIDCConfigUrl: string): Promise { logger.info('Initializing OIDC configuration from OIDC Config URL:', OIDCConfigUrl); @@ -66,19 +79,36 @@ export class OAuth2ClientWithConfig extends OAuth2Client { return super.createAuthorizationURL(authEndpoint, state, scopes); } - async validateAuthorizationCode(tokenEndpoint: string, code: string, codeVerifier: string | null): Promise { - logger.debug('Validating authorization code with explicit client_id'); - + async validateAuthorizationCode( + tokenEndpoint: string, + code: string, + codeVerifier: string | null + ): Promise { + // Use legacy flow for OBP-OIDC, new flow for KeyCloak + if (this.providerType === 'obp-oidc') { + return this.validateAuthorizationCodeLegacy(tokenEndpoint, code, codeVerifier); + } else { + return this.validateAuthorizationCodeModern(tokenEndpoint, code, codeVerifier); + } + } + + private async validateAuthorizationCodeLegacy( + tokenEndpoint: string, + code: string, + codeVerifier: string | null + ): Promise { + logger.debug('Validating authorization code with legacy method (OBP-OIDC)'); + const body = new URLSearchParams(); body.set('grant_type', 'authorization_code'); body.set('code', code); - body.set('redirect_uri', this.redirectURI); - body.set('client_id', this.clientId); - - if (this.clientSecret) { - body.set('client_secret', this.clientSecret); + body.set('redirect_uri', this.storedRedirectURI); + body.set('client_id', this.storedClientId); + + if (this.storedClientSecret) { + body.set('client_secret', this.storedClientSecret); } - + if (codeVerifier) { body.set('code_verifier', codeVerifier); } @@ -89,7 +119,7 @@ export class OAuth2ClientWithConfig extends OAuth2Client { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json' + Accept: 'application/json' }, body: body.toString() }); @@ -102,11 +132,112 @@ export class OAuth2ClientWithConfig extends OAuth2Client { const tokens = await response.json(); logger.debug('Token response received successfully'); - + + return { + accessToken: () => tokens.access_token, + refreshToken: () => tokens.refresh_token, + accessTokenExpiresAt: () => + tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : null + }; + } + + private async validateAuthorizationCodeModern( + tokenEndpoint: string, + code: string, + codeVerifier: string | null + ): Promise { + logger.debug('Validating authorization code with modern method (KeyCloak)'); + + const body = new URLSearchParams(); + body.set('grant_type', 'authorization_code'); + body.set('code', code); + body.set('redirect_uri', this.storedRedirectURI); + + // Prepare headers + const headers: Record = { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json' + }; + + // Use HTTP Basic Authentication for client credentials (RFC 6749 Section 2.3.1) + if (this.storedClientSecret) { + const credentials = Buffer.from(`${this.storedClientId}:${this.storedClientSecret}`).toString( + 'base64' + ); + headers['Authorization'] = `Basic ${credentials}`; + logger.debug('Using Basic Authentication for client credentials'); + } else { + // Public client - include client_id in body + body.set('client_id', this.storedClientId); + logger.debug('Using client_id in request body (public client)'); + } + + if (codeVerifier) { + body.set('code_verifier', codeVerifier); + } + + logger.debug(`Token request body: ${body.toString()}`); + + const response = await fetch(tokenEndpoint, { + method: 'POST', + headers, + body: body.toString() + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + logger.error(`Token endpoint error - Status: ${response.status}, Data:`, errorData); + + // If Basic Auth failed and we have a client secret, try with credentials in body as fallback + if (response.status === 401 && this.storedClientSecret && !body.has('client_id')) { + logger.warn('Basic Auth failed, retrying with credentials in request body'); + + // Add client credentials to body for retry + body.set('client_id', this.storedClientId); + body.set('client_secret', this.storedClientSecret); + + // Remove Authorization header + delete headers['Authorization']; + + const retryResponse = await fetch(tokenEndpoint, { + method: 'POST', + headers, + body: body.toString() + }); + + if (!retryResponse.ok) { + const retryErrorData = await retryResponse.json().catch(() => ({})); + logger.error( + `Token endpoint retry error - Status: ${retryResponse.status}, Data:`, + retryErrorData + ); + throw new Error( + `Token request failed after retry: ${retryResponse.status} ${retryResponse.statusText}` + ); + } + + const retryTokens = await retryResponse.json(); + logger.debug('Token response received successfully after retry'); + + return { + accessToken: () => retryTokens.access_token, + refreshToken: () => retryTokens.refresh_token, + accessTokenExpiresAt: () => + retryTokens.expires_in ? new Date(Date.now() + retryTokens.expires_in * 1000) : null + }; + } + + throw new Error(`Token request failed: ${response.status} ${response.statusText}`); + } + + const tokens = await response.json(); + logger.debug('Token response received successfully'); + return { accessToken: () => tokens.access_token, refreshToken: () => tokens.refresh_token, - accessTokenExpiresAt: () => tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : null + accessTokenExpiresAt: () => + tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : null }; } } diff --git a/src/lib/oauth/providerFactory.ts b/src/lib/oauth/providerFactory.ts index 3f0c1ca..6222860 100644 --- a/src/lib/oauth/providerFactory.ts +++ b/src/lib/oauth/providerFactory.ts @@ -1,20 +1,20 @@ import { createLogger } from '$lib/utils/logger'; const logger = createLogger('OAuthProviderFactory'); -import { OAuth2ClientWithConfig } from "./client"; -import { env } from "$env/dynamic/private"; +import { OAuth2ClientWithConfig } from './client'; +import { env } from '$env/dynamic/private'; export interface WellKnownUri { - provider: string; - url: string; + provider: string; + url: string; } // Implement this for other OAuth2 providers as needed // Then register them in the OAuth2ProviderFactory interface OAuth2ProviderStrategy { - providerName: string; - initialize(config: WellKnownUri): Promise; - supports(provider: string): boolean; - getProviderName(): string; + providerName: string; + initialize(config: WellKnownUri): Promise; + supports(provider: string): boolean; + getProviderName(): string; } class KeyCloakStrategy implements OAuth2ProviderStrategy { @@ -32,7 +32,8 @@ class KeyCloakStrategy implements OAuth2ProviderStrategy { const client = new OAuth2ClientWithConfig( env.KEYCLOAK_OAUTH_CLIENT_ID, env.KEYCLOAK_OAUTH_CLIENT_SECRET, - env.APP_CALLBACK_URL + env.APP_CALLBACK_URL, + 'keycloak' ); await client.initOIDCConfig(config.url); @@ -42,7 +43,7 @@ class KeyCloakStrategy implements OAuth2ProviderStrategy { } class OBPOIDCStrategy implements OAuth2ProviderStrategy { - providerName = 'obp-oidc' + providerName = 'obp-oidc'; supports(provider: string): boolean { return provider === this.providerName; @@ -63,7 +64,8 @@ class OBPOIDCStrategy implements OAuth2ProviderStrategy { const client = new OAuth2ClientWithConfig( env.OBP_OAUTH_CLIENT_ID, env.OBP_OAUTH_CLIENT_SECRET, - env.APP_CALLBACK_URL + env.APP_CALLBACK_URL, + 'obp-oidc' ); await client.initOIDCConfig(config.url); @@ -145,4 +147,4 @@ export class OAuth2ProviderFactory { } } -export const oauth2ProviderFactory = new OAuth2ProviderFactory(); \ No newline at end of file +export const oauth2ProviderFactory = new OAuth2ProviderFactory(); diff --git a/src/lib/oauth/providerManager.ts b/src/lib/oauth/providerManager.ts index 1f8fe96..bd973f3 100644 --- a/src/lib/oauth/providerManager.ts +++ b/src/lib/oauth/providerManager.ts @@ -5,161 +5,333 @@ import { PUBLIC_OBP_BASE_URL } from '$env/static/public'; const logger = createLogger('OAuthProviderManager'); +export interface ProviderStatus { + provider: string; + url?: string; + status: 'available' | 'unavailable'; + error?: string; +} + interface OAuthProviderStatus { - ready: boolean; - error?: any; - initializedProviders: WellKnownUri[] + ready: boolean; + error?: any; + providers: ProviderStatus[]; } class OAuth2ProviderManager { - private status: OAuthProviderStatus = { - ready: false, - initializedProviders: [] - }; - private retryIntervalId: NodeJS.Timeout | null = null; - private retryIntervalMs: number = 30000; // Retry every 30 seconds - - /** - * Fetches all well-known URIs from the OBP API - */ - async fetchWellKnownUris(): Promise { - try { - const response = await obp_requests.get('/obp/v5.1.0/well-known'); - return response.well_known_uris; - } catch (error) { - logger.error('Failed to fetch well-known URIs:', error); - throw error; - } - } - - /** - * Initializes all available OAuth2 providers from well-known URIs - */ - async initOauth2Providers() { - const initializedProviders: WellKnownUri[] = []; - - try { - const wellKnownUris: WellKnownUri[] = await this.fetchWellKnownUris(); - logger.debug('Well-known URIs fetched successfully:', wellKnownUris); - - for (const providerUri of wellKnownUris) { - const oauth2Client = await oauth2ProviderFactory.initializeProvider(providerUri); - if (oauth2Client) { - initializedProviders.push(providerUri); - } - } - - for (const registeredStrategy of oauth2ProviderFactory.getSupportedProviders()) { - if (!initializedProviders.find((p) => p.provider === registeredStrategy)) { - logger.warn( - `No OAuth2 provider initialized for registered strategy: ${registeredStrategy}` - ); - } - } - - // If no providers were found, log error and return - if (initializedProviders.length === 0) { - logger.error( - 'Could not initialize any OAuth2 provider. Please check your OBP configuration.' - ); - throw new Error('No OAuth2 providers could be initialized'); - } - - return initializedProviders; - } catch (error) { - logger.error('Failed to init OAuth2 providers: ', error); - throw error; - } - } - - /** - * Attempts to initialize OAuth2 providers and updates status - */ - async tryInitOauth2Providers() { - try { - const providers = await this.initOauth2Providers(); - this.status = { - ready: true, - error: null, - initializedProviders: providers - }; - logger.info('OAuth2 providers initialized successfully.'); - - // Clear retry interval if it exists - if (this.retryIntervalId) { - clearInterval(this.retryIntervalId); - this.retryIntervalId = null; - } - - return providers; - } catch (error) { - this.status = { - ready: false, - error: error, - initializedProviders: [] - }; - logger.error('Error initializing OAuth2 providers:', error); - return []; - } - } - - /** - * Starts the initialization process with automatic retry - */ - async start() { - await this.tryInitOauth2Providers(); - - // Start retry interval if initialization failed - if (!this.status.ready && !this.retryIntervalId) { - this.retryIntervalId = setInterval(async () => { - if (!this.status.ready) { - logger.info('Retrying OAuth2 providers initialization...'); - await this.tryInitOauth2Providers(); - } - }, this.retryIntervalMs); - } - } - - /** - * Returns the current status of OAuth2 providers - */ - getStatus(): OAuthProviderStatus { - return { ...this.status }; - } - - /** - * Returns if the OAuth2 providers are ready - */ - isReady(): boolean { - return this.status.ready; - } - - /** - * Returns the list of initialized providers - */ - getInitializedProviders(): WellKnownUri[] { - return [...this.status.initializedProviders]; - } - - /** - * Returns a list of health check entries for the initialized providers - */ - getHealthCheckEntries() { - return this.status.initializedProviders.map(wellKnown => ({ - serviceName: `OAuth2: ${wellKnown.provider}`, - url: wellKnown.url, - })); - } - - /** - * Cleans up resources, like the retry interval - */ - shutdown() { - if (this.retryIntervalId) { - clearInterval(this.retryIntervalId); - this.retryIntervalId = null; - } - } + private status: OAuthProviderStatus = { + ready: false, + providers: [] + }; + private retryIntervalId: NodeJS.Timeout | null = null; + private retryIntervalMs: number = 30000; // Retry every 30 seconds + private refreshIntervalId: NodeJS.Timeout | null = null; + private refreshIntervalMs: number = 60000; // Refresh provider status every 60 seconds + private definedProviders: string[] = []; + + constructor() { + // Initialize with all known/configured providers from the factory + this.definedProviders = oauth2ProviderFactory.getSupportedProviders(); + this.initializeProviderStatuses(); + } + + /** + * Initialize provider statuses for all defined providers + */ + private initializeProviderStatuses() { + this.status.providers = this.definedProviders.map((provider) => ({ + provider, + status: 'unavailable' as const, + error: 'Provider not yet checked' + })); + } + + /** + * Fetches all well-known URIs from the OBP API + */ + async fetchWellKnownUris(): Promise { + try { + const response = await obp_requests.get('/obp/v5.1.0/well-known'); + return response.well_known_uris; + } catch (error) { + logger.error('Failed to fetch well-known URIs:', error); + throw error; + } + } + + /** + * Initializes all available OAuth2 providers from well-known URIs + * Also tracks providers that failed to initialize + */ + async initOauth2Providers() { + const initializedProviders: WellKnownUri[] = []; + let wellKnownUris: WellKnownUri[] = []; + + try { + wellKnownUris = await this.fetchWellKnownUris(); + logger.debug('Well-known URIs fetched successfully:', wellKnownUris); + } catch (error) { + logger.error('Failed to fetch well-known URIs, marking all providers as unavailable'); + // Mark all defined providers as unavailable due to API connectivity issues + this.definedProviders.forEach((provider) => { + const existingProvider = this.status.providers.find((p) => p.provider === provider); + if (existingProvider) { + existingProvider.status = 'unavailable'; + existingProvider.error = 'Unable to fetch provider configuration from OBP API'; + } + }); + throw error; + } + + // Process each provider from well-known URIs + for (const providerUri of wellKnownUris) { + const existingProvider = this.status.providers.find( + (p) => p.provider === providerUri.provider + ); + + try { + const oauth2Client = await oauth2ProviderFactory.initializeProvider(providerUri); + if (oauth2Client) { + initializedProviders.push(providerUri); + if (existingProvider) { + existingProvider.status = 'available'; + existingProvider.url = providerUri.url; + existingProvider.error = undefined; + } else { + this.status.providers.push({ + provider: providerUri.provider, + url: providerUri.url, + status: 'available' + }); + } + logger.info(`Successfully initialized OAuth2 provider: ${providerUri.provider}`); + } else { + if (existingProvider) { + existingProvider.status = 'unavailable'; + existingProvider.url = providerUri.url; + existingProvider.error = 'No strategy found for this provider'; + } else { + this.status.providers.push({ + provider: providerUri.provider, + url: providerUri.url, + status: 'unavailable', + error: 'No strategy found for this provider' + }); + } + logger.warn(`No strategy available for provider: ${providerUri.provider}`); + } + } catch (error) { + if (existingProvider) { + existingProvider.status = 'unavailable'; + existingProvider.url = providerUri.url; + existingProvider.error = error instanceof Error ? error.message : 'Unknown error'; + } else { + this.status.providers.push({ + provider: providerUri.provider, + url: providerUri.url, + status: 'unavailable', + error: error instanceof Error ? error.message : 'Unknown error' + }); + } + logger.error(`Failed to initialize OAuth2 provider ${providerUri.provider}:`, error); + } + } + + // Mark defined providers not found in well-known URIs as unavailable + for (const definedProvider of this.definedProviders) { + const foundInWellKnown = wellKnownUris.find((uri) => uri.provider === definedProvider); + if (!foundInWellKnown) { + const existingProvider = this.status.providers.find((p) => p.provider === definedProvider); + if (existingProvider) { + existingProvider.status = 'unavailable'; + existingProvider.error = 'Provider not configured in OBP API well-known endpoints'; + } + } + } + + const availableCount = this.status.providers.filter((p) => p.status === 'available').length; + const unavailableCount = this.status.providers.filter((p) => p.status === 'unavailable').length; + + logger.info( + `OAuth2 Provider Summary: ${availableCount} available, ${unavailableCount} unavailable` + ); + + if (availableCount === 0) { + logger.error( + 'Could not initialize any OAuth2 provider. Please check your OBP configuration.' + ); + throw new Error('No OAuth2 providers could be initialized'); + } + + if (availableCount === 1) { + const availableProvider = this.status.providers.find((p) => p.status === 'available'); + logger.info(`Single OAuth2 provider available: ${availableProvider?.provider}`); + } + + if (availableCount > 1) { + const availableProviders = this.status.providers + .filter((p) => p.status === 'available') + .map((p) => p.provider); + logger.info(`Multiple OAuth2 providers available: ${availableProviders.join(', ')}`); + } + + return initializedProviders; + } + + /** + * Attempts to initialize OAuth2 providers and updates status + */ + async tryInitOauth2Providers() { + try { + const providers = await this.initOauth2Providers(); + this.status.ready = providers.length > 0; + this.status.error = null; + + if (this.status.ready) { + logger.info('OAuth2 providers initialized successfully.'); + if (this.retryIntervalId) { + clearInterval(this.retryIntervalId); + this.retryIntervalId = null; + } + // Start periodic refresh to check for provider status changes + this.startPeriodicRefresh(); + } + + return providers; + } catch (error) { + this.status.ready = false; + this.status.error = error; + logger.error('Error initializing OAuth2 providers:', error); + return []; + } + } + + /** + * Starts the initialization process with automatic retry + */ + async start() { + await this.tryInitOauth2Providers(); + + // Start retry interval if initialization failed + if (!this.status.ready && !this.retryIntervalId) { + this.retryIntervalId = setInterval(async () => { + if (!this.status.ready) { + logger.info('Retrying OAuth2 providers initialization...'); + await this.tryInitOauth2Providers(); + } + }, this.retryIntervalMs); + } else if (this.status.ready) { + // Start periodic refresh for status monitoring + this.startPeriodicRefresh(); + } + } + + /** + * Returns all providers with their status + */ + getAllProviders(): ProviderStatus[] { + return [...this.status.providers]; + } + + /** + * Returns only available providers + */ + getAvailableProviders(): ProviderStatus[] { + return this.status.providers.filter((p) => p.status === 'available'); + } + + /** + * Returns only unavailable providers + */ + getUnavailableProviders(): ProviderStatus[] { + return this.status.providers.filter((p) => p.status === 'unavailable'); + } + + /** + * Returns the legacy initialized providers format for backward compatibility + */ + getInitializedProviders(): WellKnownUri[] { + return this.getAvailableProviders() + .filter((p) => p.url) + .map((p) => ({ + provider: p.provider, + url: p.url! + })); + } + + /** + * Returns if the OAuth2 providers are ready + */ + isReady(): boolean { + return this.status.ready; + } + + /** + * Returns a list of health check entries for the available providers + */ + getHealthCheckEntries() { + return this.getAvailableProviders() + .filter((p) => p.url) + .map((p) => ({ + serviceName: `OAuth2: ${p.provider}`, + url: p.url! + })); + } + + /** + * Returns the number of available providers + */ + getProviderCount(): number { + return this.getAvailableProviders().length; + } + + /** + * Returns true if there's exactly one available provider + */ + hasSingleProvider(): boolean { + return this.getProviderCount() === 1; + } + + /** + * Returns true if there are multiple available providers + */ + hasMultipleProviders(): boolean { + return this.getProviderCount() > 1; + } + + /** + * Starts periodic refresh to monitor provider status changes + */ + private startPeriodicRefresh() { + if (this.refreshIntervalId) { + return; // Already started + } + + this.refreshIntervalId = setInterval(async () => { + logger.debug('Refreshing OAuth2 provider statuses...'); + try { + await this.initOauth2Providers(); + } catch (error) { + logger.warn('Provider status refresh failed:', error); + } + }, this.refreshIntervalMs); + + logger.info(`Started periodic provider status refresh every ${this.refreshIntervalMs}ms`); + } + + /** + * Cleans up resources, like the retry and refresh intervals + */ + shutdown() { + if (this.retryIntervalId) { + clearInterval(this.retryIntervalId); + this.retryIntervalId = null; + } + if (this.refreshIntervalId) { + clearInterval(this.refreshIntervalId); + this.refreshIntervalId = null; + } + } } -export const oauth2ProviderManager = new OAuth2ProviderManager(); \ No newline at end of file +export const oauth2ProviderManager = new OAuth2ProviderManager(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index acfd882..e59882a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -225,7 +225,7 @@ {:else} Register Login {/if} diff --git a/src/routes/api/opey/auth/+server.ts b/src/routes/api/opey/auth/+server.ts index 73af6c7..6009a6c 100644 --- a/src/routes/api/opey/auth/+server.ts +++ b/src/routes/api/opey/auth/+server.ts @@ -1,116 +1,122 @@ import { createLogger } from '$lib/utils/logger'; const logger = createLogger('OpeyAuthServer'); import { extractUsernameFromJWT } from '$lib/utils/jwt'; -import { json } from "@sveltejs/kit"; -import type { RequestEvent } from "./$types"; -import { obpIntegrationService } from "$lib/opey/services/OBPIntegrationService"; -import { env } from "$env/dynamic/private"; -import type { Session } from "svelte-kit-sessions"; - +import { json } from '@sveltejs/kit'; +import type { RequestEvent } from './$types'; +import { obpIntegrationService } from '$lib/opey/services/OBPIntegrationService'; +import { env } from '$env/dynamic/private'; +import type { Session } from 'svelte-kit-sessions'; export async function POST(event: RequestEvent) { - try { - const session = event.locals.session; - const accessToken = session?.data?.oauth?.access_token; - - // Check if this is an authenticated request - if (accessToken) { - - const opeyConsumerId = env.OPEY_CONSUMER_ID; - if (!opeyConsumerId) { - // Opey consumer ID is not configured - // We will return an anonymous session instead, with a warning/error - - logger.warn('Opey consumer ID not configured, returning anonymous session'); - return await _getAnonymousSession('Opey consumer ID not configured, returning anonymous session instead.'); - } - - try { - // AUTHENTICATED FLOW - Create consent and authenticated Opey session - return await _getAuthenticatedSession(opeyConsumerId, session); - } catch (error: any) { - logger.info('JWT expired for Opey session - user needs to re-authenticate:', error); - return json({ error: error.message || 'Internal Server Error' }, { status: 500 }); - } - - } else { - // ANONYMOUS FLOW - Create anonymous Opey session - return await _getAnonymousSession(); - } - - } catch (error: any) { - logger.error('Opey Auth error:', error); - return json({ error: error.message || 'Internal Server Error' }, { status: 500 }); - } + try { + const session = event.locals.session; + const accessToken = session?.data?.oauth?.access_token; + + // Check if this is an authenticated request + if (accessToken) { + const opeyConsumerId = env.OPEY_CONSUMER_ID; + if (!opeyConsumerId) { + // Opey consumer ID is not configured + // We will return an anonymous session instead, with a warning/error + + logger.warn('Opey consumer ID not configured, returning anonymous session'); + return await _getAnonymousSession( + 'Opey consumer ID not configured, returning anonymous session instead.' + ); + } + + try { + // AUTHENTICATED FLOW - Create consent and authenticated Opey session + return await _getAuthenticatedSession(opeyConsumerId, session); + } catch (error: any) { + logger.info('JWT expired for Opey session - user needs to re-authenticate:', error); + return json({ error: error.message || 'Internal Server Error' }, { status: 500 }); + } + } else { + // ANONYMOUS FLOW - Create anonymous Opey session + return await _getAnonymousSession(); + } + } catch (error: any) { + logger.error('Opey Auth error:', error); + return json({ error: error.message || 'Internal Server Error' }, { status: 500 }); + } } - async function _getAuthenticatedSession(opeyConsumerId: string, portalSession: Session) { - // AUTHENTICATED FLOW - Create consent and authenticated Opey session - - const consent = await obpIntegrationService.getOrCreateOpeyConsent(portalSession); - const consentJwt = consent.jwt; - - // Extract and log user identifier from consent JWT - const userIdentifier = extractUsernameFromJWT(consentJwt); - logger.info(`_getAuthenticatedSession says: Sending consent JWT to Opey - Making request to ${env.OPEY_BASE_URL}/create-session - Primary user: ${userIdentifier}`); - - const opeyResponse = await fetch(`${env.OPEY_BASE_URL}/create-session`, { - method: 'POST', - headers: { - 'Consent-JWT': consentJwt, - 'Content-Type': 'application/json' - } - }); - - if (!opeyResponse.ok) { - const errorText = await opeyResponse.text(); - logger.error(`_getAuthenticatedSession says: Failed to create authenticated Opey session - Primary user: ${userIdentifier} - Error: ${errorText}`); - throw new Error(`Failed to create authenticated Opey session: ${errorText}`); - } - - logger.info(`_getAuthenticatedSession says: Successfully created authenticated Opey session - Primary user: ${userIdentifier}`); - - // Forward the session cookie to the client - const setCookieHeaders = opeyResponse.headers.get('set-cookie'); - logger.info(`setCookieHeaders: ${setCookieHeaders}`); - return json( - { success: true, authenticated: true }, - setCookieHeaders ? { headers: { 'Set-Cookie': setCookieHeaders } } : {} - ); + // AUTHENTICATED FLOW - Create consent and authenticated Opey session + + const consent = await obpIntegrationService.getOrCreateOpeyConsent(portalSession); + const consentJwt = consent.jwt; + + // Extract and log user identifier from consent JWT + const userIdentifier = extractUsernameFromJWT(consentJwt); + logger.info( + `_getAuthenticatedSession says: Sending consent JWT to Opey - Making request to ${env.OPEY_BASE_URL}/create-session - Primary user: ${userIdentifier}` + ); + + const opeyResponse = await fetch(`${env.OPEY_BASE_URL}/create-session`, { + method: 'POST', + headers: { + 'Consent-JWT': consentJwt, + 'Content-Type': 'application/json' + } + }); + + if (!opeyResponse.ok) { + const errorText = await opeyResponse.text(); + logger.error( + `_getAuthenticatedSession says: Failed to create authenticated Opey session - Primary user: ${userIdentifier} - Error: ${errorText}` + ); + throw new Error(`Failed to create authenticated Opey session: ${errorText}`); + } + + logger.info( + `_getAuthenticatedSession says: Successfully created authenticated Opey session - Primary user: ${userIdentifier}` + ); + + // Forward the session cookie to the client + const setCookieHeaders = opeyResponse.headers.get('set-cookie'); + logger.info(`setCookieHeaders: ${setCookieHeaders}`); + return json( + { success: true, authenticated: true }, + setCookieHeaders ? { headers: { 'Set-Cookie': setCookieHeaders } } : {} + ); } - async function _getAnonymousSession(error?: string) { - // ANONYMOUS FLOW - Create anonymous Opey session - logger.info(`_getAnonymousSession says: Creating anonymous Opey session - Making request to ${env.OPEY_BASE_URL}/create-session (no consent JWT)`); - - const opeyResponse = await fetch(`${env.OPEY_BASE_URL}/create-session`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - // No Consent-JWT header = anonymous session - }); - - if (!opeyResponse.ok) { - const errorText = await opeyResponse.text(); - logger.error(`_getAnonymousSession says: Failed to create anonymous Opey session - Error: ${errorText}`); - throw new Error(`Failed to create anonymous Opey session: ${errorText}`); - } - - logger.info(`_getAnonymousSession says: Successfully created anonymous Opey session`); - - // Forward the session cookie to the client - const setCookieHeaders = opeyResponse.headers.get('set-cookie'); - const responseData: any = { success: true, authenticated: false }; - - if (error) { - responseData.error = error; - } - - return json( - responseData, - setCookieHeaders ? { headers: { 'Set-Cookie': setCookieHeaders } } : {} - ); -} \ No newline at end of file + // ANONYMOUS FLOW - Create anonymous Opey session + logger.info( + `_getAnonymousSession says: Creating anonymous Opey session - Making request to ${env.OPEY_BASE_URL}/create-session (no consent JWT)` + ); + + const opeyResponse = await fetch(`${env.OPEY_BASE_URL}/create-session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + // No Consent-JWT header = anonymous session + }); + + if (!opeyResponse.ok) { + const errorText = await opeyResponse.text(); + logger.error( + `_getAnonymousSession says: Failed to create anonymous Opey session - Error: ${errorText}` + ); + throw new Error(`Failed to create anonymous Opey session: ${errorText}`); + } + + logger.info(`_getAnonymousSession says: Successfully created anonymous Opey session`); + + // Forward the session cookie to the client + const setCookieHeaders = opeyResponse.headers.get('set-cookie'); + const responseData: any = { success: true, authenticated: false }; + + if (error) { + responseData.error = error; + } + + return json( + responseData, + setCookieHeaders ? { headers: { 'Set-Cookie': setCookieHeaders } } : {} + ); +} diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 0000000..fe016f8 --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -0,0 +1,23 @@ +import { oauth2ProviderManager, type ProviderStatus } from '$lib/oauth/providerManager'; +import { redirect } from '@sveltejs/kit'; +import type { ServerLoad } from '@sveltejs/kit'; + +export const load: ServerLoad = async () => { + const allProviders = oauth2ProviderManager.getAllProviders(); + const availableProviders = oauth2ProviderManager.getAvailableProviders(); + const unavailableProviders = oauth2ProviderManager.getUnavailableProviders(); + + // If we have exactly 1 available provider, redirect directly to it + if (availableProviders.length === 1) { + throw redirect(302, `/login/${availableProviders[0].provider}`); + } + + // Return all providers for user selection (0, 2+ available providers) + return { + allProviders, + availableProviders, + unavailableProviders, + loading: false, + lastUpdated: new Date().toISOString() + }; +}; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index a12661d..885a04b 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -1,8 +1,128 @@ + +
-

Login

- +
+

Login

+
+ {formatLastUpdated()} +
+
+ + {#if data.availableProviders.length === 0} +
+

No authentication providers available. Please contact your administrator.

+ + + {#if data.unavailableProviders.length > 0} +
+

Currently unavailable:

+ {#each data.unavailableProviders as provider} +
+
+ + + {formatProviderName(provider.provider)} + + Unavailable +
+ {#if provider.error} +
+ {provider.error} +
+ {/if} +
+ {/each} +
+ {/if} +
+ {:else} + +
+

Choose your authentication provider:

+ {#each data.availableProviders as provider} + + {/each} + + + {#if data.unavailableProviders.length > 0} +
+

Currently unavailable:

+ {#each data.unavailableProviders as provider} +
+
+ + + {formatProviderName(provider.provider)} + + Unavailable +
+ {#if provider.error} +
+ {provider.error} +
+ {/if} +
+ {/each} +
+ {/if} +
+ {/if}
-
\ No newline at end of file + diff --git a/src/routes/login/[provider]/+server.ts b/src/routes/login/[provider]/+server.ts new file mode 100644 index 0000000..ecb23bb --- /dev/null +++ b/src/routes/login/[provider]/+server.ts @@ -0,0 +1,68 @@ +import { createLogger } from '$lib/utils/logger'; +const logger = createLogger('ProviderLogin'); +import { generateState } from 'arctic' +import { oauth2ProviderFactory } from '$lib/oauth/providerFactory' +import type { RequestEvent } from '@sveltejs/kit' +import { error } from "@sveltejs/kit"; + +export function GET(event: RequestEvent) { + const { provider } = event.params; + + if (!provider) { + logger.error('No provider specified in URL'); + throw error(400, 'Provider not specified'); + } + + // Check if the requested provider is available + const oauthClient = oauth2ProviderFactory.getClient(provider); + if (!oauthClient) { + logger.error( + `OAuth client for provider "${provider}" not found. Available providers: ${Array.from(oauth2ProviderFactory.getAllClients().keys()).join(', ')}` + ); + throw error(404, 'OAuth provider not found'); + } + + const state = generateState(); + // Encode provider in the state - format "state:provider" + const encodedState = `${state}:${provider}`; + + const scopes = ['openid']; + + logger.debug(`OAuth client found for provider: ${provider}`); + logger.debug(`OIDC Config present: ${!!oauthClient.OIDCConfig}`); + if (oauthClient.OIDCConfig) { + logger.debug(`Authorization endpoint: ${oauthClient.OIDCConfig.authorization_endpoint}`); + logger.debug(`Token endpoint: ${oauthClient.OIDCConfig.token_endpoint}`); + } else { + logger.info('OIDC Config not present on OAuth client. Retry to get config form OIDC well-known endpoint.'); + } + + const auth_endpoint = oauthClient.OIDCConfig?.authorization_endpoint; + if (!auth_endpoint) { + logger.error('Authorization endpoint not found in OIDC configuration.'); + logger.error(`Full OIDC config: ${JSON.stringify(oauthClient.OIDCConfig, null, 2)}`); + throw error(500, 'OAuth configuration error'); + } + + try { + const url = oauthClient.createAuthorizationURL(auth_endpoint, encodedState, scopes); + + event.cookies.set('obp_oauth_state', encodedState, { + httpOnly: true, + maxAge: 60 * 10, + secure: import.meta.env.PROD, + path: '/', + sameSite: 'lax' + }); + + return new Response(null, { + status: 302, + headers: { + Location: url.toString() + } + }); + } catch (err) { + logger.error(`Error during ${provider} OAuth login:`, err); + throw error(500, 'Internal Server Error'); + } +} diff --git a/src/routes/login/[provider]/callback/+server.ts b/src/routes/login/[provider]/callback/+server.ts new file mode 100644 index 0000000..6ffd728 --- /dev/null +++ b/src/routes/login/[provider]/callback/+server.ts @@ -0,0 +1,109 @@ +import { createLogger } from '$lib/utils/logger'; +const logger = createLogger('ProviderLoginCallback'); +import { oauth2ProviderFactory } from '$lib/oauth/providerFactory'; +import type { OAuth2Tokens } from 'arctic'; +import type { RequestEvent } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; +import { env } from '$env/dynamic/public'; + +export async function GET(event: RequestEvent): Promise { + const { provider: urlProvider } = event.params; + const storedState = event.cookies.get('obp_oauth_state'); + const code = event.url.searchParams.get('code'); + const recievedState = event.url.searchParams.get('state'); + + if (!urlProvider) { + throw error(400, 'Provider not specified'); + } + + if (storedState === null || code === null || recievedState === null) { + throw error(400, 'Please restart the process.'); + } + if (storedState !== recievedState) { + logger.warn('State mismatch:', storedState, recievedState); + // State does not match, likely a CSRF attack or user error + throw error(400, 'Please restart the process.'); + } + + const [actualState, provider] = storedState.split(':'); + logger.debug('Received state:', recievedState); + if (!provider || provider !== urlProvider) { + throw error(400, 'Invalid state format or provider mismatch'); + } + + const oauthClient = oauth2ProviderFactory.getClient(provider); + if (!oauthClient) { + logger.error(`OAuth client for provider "${provider}" not found.`); + throw error(400, 'Invalid OAuth provider'); + } + + logger.debug(`Processing callback for provider: ${provider}`); + + // Validate the authorization code and exchange it for tokens + const token_endpoint = oauthClient.OIDCConfig?.token_endpoint; + if (!token_endpoint) { + logger.error('Token endpoint not found in OIDC configuration.'); + throw error(500, 'OAuth configuration error'); + } + + let tokens: OAuth2Tokens; + try { + tokens = await oauthClient.validateAuthorizationCode(token_endpoint, code, null); + } catch (e) { + logger.error('Error validating authorization code:', e); + throw error(400, 'Log in failed, please restart the process.'); + } + + // Get rid of the state cookie + event.cookies.delete('obp_oauth_state', { + path: '/' + }); + + const obpAccessToken = tokens.accessToken(); + + logger.debug(`PUBLIC_OBP_BASE_URL from env: ${env.PUBLIC_OBP_BASE_URL}`); + const currentUserUrl = `${env.PUBLIC_OBP_BASE_URL}/obp/v5.1.0/users/current`; + logger.info('Fetching current user from OBP:', currentUserUrl); + const currentUserRequest = new Request(currentUserUrl); + + currentUserRequest.headers.set('Authorization', `Bearer ${obpAccessToken}`); + logger.debug('Making OBP current user request with access token'); + + const currentUserResponse = await fetch(currentUserRequest); + if (!currentUserResponse.ok) { + const errorText = await currentUserResponse.text(); + logger.error( + `OBP current user request failed - Status: ${currentUserResponse.status}, Response: ${errorText}` + ); + throw error(500, 'Failed to fetch current user'); + } + const user = await currentUserResponse.json(); + logger.info( + `Successfully fetched current user from OBP - User ID: ${user.user_id}, Email: ${user.email}, Username: ${user.username || 'N/A'}` + ); + logger.debug('Full current user data:', user); + + if (user.user_id && user.email) { + // Store user data in session + const { session } = event.locals; + await session.setData({ + user: user, + oauth: { + access_token: obpAccessToken, + refresh_token: tokens.refreshToken(), + provider: provider + } + }); + await session.save(); + logger.debug('Session data set:', session.data); + return new Response(null, { + status: 302, + headers: { + Location: `/` + } + }); + } else { + logger.error('Invalid user data received from OBP - missing user_id or email'); + throw error(400, 'Authentication failed - invalid user data'); + } +} diff --git a/src/routes/login/obp/+server.ts b/src/routes/login/obp/+server.ts index 13c5cd5..3e1c434 100644 --- a/src/routes/login/obp/+server.ts +++ b/src/routes/login/obp/+server.ts @@ -1,68 +1,18 @@ import { createLogger } from '$lib/utils/logger'; const logger = createLogger('OBPLogin'); -import { generateState } from 'arctic' -import { oauth2ProviderFactory } from '$lib/oauth/providerFactory' -import type { RequestEvent } from '@sveltejs/kit' -import { error } from "@sveltejs/kit"; +import { oauth2ProviderFactory } from '$lib/oauth/providerFactory'; +import type { RequestEvent } from '@sveltejs/kit'; +import { error, redirect } from '@sveltejs/kit'; export function GET(event: RequestEvent) { - // Use first available provider discovered from OBP well-known endpoint - const provider = oauth2ProviderFactory.getFirstAvailableProvider(); - if (!provider) { - logger.error('No OAuth providers available. Check OBP configuration and well-known endpoints.'); - throw error(500, 'OAuth provider not configured'); - } - - const oauthClient = oauth2ProviderFactory.getClient(provider); - if (!oauthClient) { - logger.error( - `OAuth client for provider "${provider}" not found. Available providers: ${Array.from(oauth2ProviderFactory.getAllClients().keys()).join(', ')}` - ); - throw error(500, 'OAuth provider not configured'); - } - - const state = generateState(); - // Encode provider in the state - format "state:provider" - const encodedState = `${state}:${provider}`; - - const scopes = ['openid']; - - logger.debug(`OAuth client found for provider: ${provider}`); - logger.debug(`OIDC Config present: ${!!oauthClient.OIDCConfig}`); - if (oauthClient.OIDCConfig) { - logger.debug(`Authorization endpoint: ${oauthClient.OIDCConfig.authorization_endpoint}`); - logger.debug(`Token endpoint: ${oauthClient.OIDCConfig.token_endpoint}`); - } else { - logger.info('OIDC Config not present on OAuth client. Retry to get config form OIDC well-known endpoint.'); - } - - const auth_endpoint = oauthClient.OIDCConfig?.authorization_endpoint; - if (!auth_endpoint) { - logger.error('Authorization endpoint not found in OIDC configuration.'); - logger.error(`Full OIDC config: ${JSON.stringify(oauthClient.OIDCConfig, null, 2)}`); - throw error(500, 'OAuth configuration error'); - } - try { - const url = oauthClient.createAuthorizationURL(auth_endpoint, encodedState, scopes); - - event.cookies.set('obp_oauth_state', encodedState, { - httpOnly: true, - maxAge: 60 * 10, - secure: import.meta.env.PROD, - path: '/', - sameSite: 'lax' - }); - - return new Response(null, { - status: 302, - headers: { - Location: url.toString() - } - }); - } catch (err) { - logger.error('Error during OBP OAuth login:', err); - throw error(500, 'Internal Server Error'); - } - - // can also use createAuthorizationURLWithPKCE method + // Use first available provider discovered from OBP well-known endpoint + const provider = oauth2ProviderFactory.getFirstAvailableProvider(); + if (!provider) { + logger.error('No OAuth providers available. Check OBP configuration and well-known endpoints.'); + throw error(500, 'OAuth provider not configured'); + } + + // Redirect to the generic provider route for backward compatibility + logger.debug(`Redirecting to generic provider route: /login/${provider}`); + throw redirect(302, `/login/${provider}`); } diff --git a/src/routes/login/obp/callback/+server.ts b/src/routes/login/obp/callback/+server.ts index d98dc66..2d1769f 100644 --- a/src/routes/login/obp/callback/+server.ts +++ b/src/routes/login/obp/callback/+server.ts @@ -1,98 +1,102 @@ import { createLogger } from '$lib/utils/logger'; const logger = createLogger('OBPLoginCallback'); -import { oauth2ProviderFactory } from "$lib/oauth/providerFactory"; -import type { OAuth2Tokens } from "arctic"; -import type { RequestEvent } from "@sveltejs/kit"; -import { error } from "@sveltejs/kit"; -import { env } from "$env/dynamic/public"; +import { oauth2ProviderFactory } from '$lib/oauth/providerFactory'; +import type { OAuth2Tokens } from 'arctic'; +import type { RequestEvent } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; +import { env } from '$env/dynamic/public'; export async function GET(event: RequestEvent): Promise { - const storedState = event.cookies.get("obp_oauth_state"); - const code = event.url.searchParams.get("code"); - const recievedState = event.url.searchParams.get("state"); + const storedState = event.cookies.get('obp_oauth_state'); + const code = event.url.searchParams.get('code'); + const recievedState = event.url.searchParams.get('state'); - - if (storedState === null || code === null || recievedState === null) { - throw error(400, 'Please restart the process.'); - } - if (storedState !== recievedState) { - logger.warn("State mismatch:", storedState, recievedState); - // State does not match, likely a CSRF attack or user error - throw error(400, 'Please restart the process.'); - } + if (storedState === null || code === null || recievedState === null) { + throw error(400, 'Please restart the process.'); + } + if (storedState !== recievedState) { + logger.warn('State mismatch:', storedState, recievedState); + // State does not match, likely a CSRF attack or user error + throw error(400, 'Please restart the process.'); + } - const [actualState, provider] = storedState.split(":"); - logger.debug("Received state:", recievedState); - if (!provider) { - throw error(400, 'Invalid state format'); - } + const [actualState, provider] = storedState.split(':'); + logger.debug('Received state:', recievedState); + if (!provider) { + throw error(400, 'Invalid state format'); + } - const oauthClient = oauth2ProviderFactory.getClient(provider); - if (!oauthClient) { - logger.error(`OAuth client for provider "${provider}" not found.`); - throw error(400, 'Invalid OAuth provider'); - } + const oauthClient = oauth2ProviderFactory.getClient(provider); + if (!oauthClient) { + logger.error(`OAuth client for provider "${provider}" not found.`); + throw error(400, 'Invalid OAuth provider'); + } - // Validate the authorization code and exchange it for tokens - const token_endpoint = oauthClient.OIDCConfig?.token_endpoint; - if (!token_endpoint) { - logger.error("Token endpoint not found in OIDC configuration."); - throw error(500, 'OAuth configuration error'); - } + // Validate the authorization code and exchange it for tokens + const token_endpoint = oauthClient.OIDCConfig?.token_endpoint; + if (!token_endpoint) { + logger.error('Token endpoint not found in OIDC configuration.'); + throw error(500, 'OAuth configuration error'); + } - let tokens: OAuth2Tokens; - try { - tokens = await oauthClient.validateAuthorizationCode(token_endpoint, code, null); - } catch (e) { - logger.error("Error validating authorization code:", e); - throw error(400, 'Log in failed, please restart the process.'); - } + let tokens: OAuth2Tokens; + try { + tokens = await oauthClient.validateAuthorizationCode(token_endpoint, code, null); + } catch (e) { + logger.error('Error validating authorization code:', e); + throw error(400, 'Log in failed, please restart the process.'); + } - // Get rid of the state cookie - event.cookies.delete("obp_oauth_state", { - path: "/", - }) + // Get rid of the state cookie + event.cookies.delete('obp_oauth_state', { + path: '/' + }); - const obpAccessToken = tokens.accessToken(); + const obpAccessToken = tokens.accessToken(); - logger.debug(`PUBLIC_OBP_BASE_URL from env: ${env.PUBLIC_OBP_BASE_URL}`); - const currentUserUrl = `${env.PUBLIC_OBP_BASE_URL}/obp/v5.1.0/users/current`; - logger.info("Fetching current user from OBP:", currentUserUrl); - const currentUserRequest = new Request(currentUserUrl) + logger.debug(`PUBLIC_OBP_BASE_URL from env: ${env.PUBLIC_OBP_BASE_URL}`); + const currentUserUrl = `${env.PUBLIC_OBP_BASE_URL}/obp/v5.1.0/users/current`; + logger.info('Fetching current user from OBP:', currentUserUrl); + const currentUserRequest = new Request(currentUserUrl); - currentUserRequest.headers.set("Authorization", `Bearer ${obpAccessToken}`); - logger.debug("Making OBP current user request with access token"); - const currentUserResponse = await fetch(currentUserRequest); - if (!currentUserResponse.ok) { - const errorText = await currentUserResponse.text(); - logger.error(`OBP current user request failed - Status: ${currentUserResponse.status}, Response: ${errorText}`); - throw error(500, 'Failed to fetch current user'); - } - const user = await currentUserResponse.json(); - logger.info(`Successfully fetched current user from OBP - User ID: ${user.user_id}, Email: ${user.email}, Username: ${user.username || "N/A"}`); - logger.debug("Full current user data:", user); + currentUserRequest.headers.set('Authorization', `Bearer ${obpAccessToken}`); + logger.debug('Making OBP current user request with access token'); - if (user.user_id && user.email) { - // Store user data in session - const { session } = event.locals; - await session.setData({ - user: user, - oauth: { - access_token: obpAccessToken, - refresh_token: tokens.refreshToken(), - provider: provider, - } - }); - await session.save(); - logger.debug("Session data set:", session.data); - return new Response(null, { - status: 302, - headers: { - Location: `/` - } - }); - } else { - logger.error("Invalid user data received from OBP - missing user_id or email"); - throw error(400, 'Authentication failed - invalid user data'); - } -} \ No newline at end of file + const currentUserResponse = await fetch(currentUserRequest); + if (!currentUserResponse.ok) { + const errorText = await currentUserResponse.text(); + logger.error( + `OBP current user request failed - Status: ${currentUserResponse.status}, Response: ${errorText}` + ); + throw error(500, 'Failed to fetch current user'); + } + const user = await currentUserResponse.json(); + logger.info( + `Successfully fetched current user from OBP - User ID: ${user.user_id}, Email: ${user.email}, Username: ${user.username || 'N/A'}` + ); + logger.debug('Full current user data:', user); + + if (user.user_id && user.email) { + // Store user data in session + const { session } = event.locals; + await session.setData({ + user: user, + oauth: { + access_token: obpAccessToken, + refresh_token: tokens.refreshToken(), + provider: provider + } + }); + await session.save(); + logger.debug('Session data set:', session.data); + return new Response(null, { + status: 302, + headers: { + Location: `/` + } + }); + } else { + logger.error('Invalid user data received from OBP - missing user_id or email'); + throw error(400, 'Authentication failed - invalid user data'); + } +} diff --git a/src/routes/register/success/+page.svelte b/src/routes/register/success/+page.svelte index d3d2e8b..cbe3f29 100644 --- a/src/routes/register/success/+page.svelte +++ b/src/routes/register/success/+page.svelte @@ -1,6 +1,7 @@
User registration success
-
-
    - {#each Object.entries(userData) as [key, value]} - {#if key === 'created_by_user'} -
  • - {key}: - {JSON.stringify(value)} -
  • - {:else if key === 'views' || key === 'entitlements'} -
  • - {key}: - {#if typeof value === 'object' && value !== null && 'list' in value && Array.isArray(value.list) && value.list.length === 0} - None - {:else if Array.isArray(value) && value.length === 0} - None - {:else if Array.isArray(value)} - {JSON.stringify(value, null, 2)} - {:else if typeof value === 'object' && value !== null && Object.keys(value).length === 0} - None - {:else if typeof value === 'object' && value !== null} - {JSON.stringify(value, null, 2)} - {:else} - {value} - {/if} -
  • - {:else} -
  • - {key}: - {value} -
  • - {/if} - {/each} -
-
+
+
    + {#each Object.entries(userData) as [key, value]} + {#if key === 'created_by_user'} +
  • + {key}: {JSON.stringify(value)} +
  • + {:else} +
  • + {key}: {value} +
  • + {/if} + {/each} +
+
-
+ \ No newline at end of file