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}
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
LoginLogin
{/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 with Open Bank Project
-
+
+
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}
+
+
+
+ ●
+ {formatProviderName(provider.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