From e34b939a0ee05b2b4b26966db77b6b93b9842177 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Mon, 29 Dec 2025 08:44:36 +0100 Subject: [PATCH] can login again --- server/app.ts | 21 +- .../controllers/OAuth2CallbackController.ts | 249 ++------------ server/controllers/OAuth2ConnectController.ts | 172 ++-------- server/routes/oauth2.ts | 311 ++++++++++++++++++ 4 files changed, 368 insertions(+), 385 deletions(-) create mode 100644 server/routes/oauth2.ts diff --git a/server/app.ts b/server/app.ts index 9ba2a25..4bcd7af 100644 --- a/server/app.ts +++ b/server/app.ts @@ -50,9 +50,8 @@ import { OAuth2CallbackController } from './controllers/OAuth2CallbackController import { OAuth2ConnectController } from './controllers/OAuth2ConnectController.js' import { OAuth2ProvidersController } from './controllers/OAuth2ProvidersController.js' -// Import middlewares -import OAuth2AuthorizationMiddleware from './middlewares/OAuth2AuthorizationMiddleware.js' -import OAuth2CallbackMiddleware from './middlewares/OAuth2CallbackMiddleware.js' +// Import OAuth2 routes (plain Express, not routing-controllers) +import oauth2Routes from './routes/oauth2.js' // ES module equivalent of __dirname const __filename = fileURLToPath(import.meta.url) @@ -226,18 +225,14 @@ let instance: any const routePrefix = '/api' + // Register OAuth2 routes BEFORE routing-controllers (plain Express) + app.use(routePrefix, oauth2Routes) + console.log('OAuth2 routes registered (plain Express)') + const server = useExpressServer(app, { routePrefix: routePrefix, - controllers: [ - OpeyController, - OBPController, - StatusController, - UserController, - OAuth2CallbackController, - OAuth2ConnectController, - OAuth2ProvidersController - ], - middlewares: [OAuth2AuthorizationMiddleware, OAuth2CallbackMiddleware] + controllers: [OpeyController, OBPController, StatusController, UserController], + middlewares: [] }) instance = server.listen(port) diff --git a/server/controllers/OAuth2CallbackController.ts b/server/controllers/OAuth2CallbackController.ts index 021faa3..44bc529 100644 --- a/server/controllers/OAuth2CallbackController.ts +++ b/server/controllers/OAuth2CallbackController.ts @@ -25,30 +25,25 @@ * */ -import { Controller, Req, Res, Get, QueryParam } from 'routing-controllers' +import { Controller, Req, Res, Get, UseBefore } from 'routing-controllers' import type { Request, Response } from 'express' -import { Service, Container } from 'typedi' -import { OAuth2Service } from '../services/OAuth2Service.js' -import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js' -import type { UserInfo } from '../types/oauth2.js' +import { Service } from 'typedi' +import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js' /** - * OAuth2 Callback Controller (Multi-Provider) + * OAuth2 Callback Controller * - * Handles the OAuth2/OIDC callback from any configured identity provider. + * Handles the OAuth2/OIDC callback from the identity provider. * This controller receives the authorization code and state parameter * after the user authenticates with the OIDC provider. * - * This controller handles: + * The OAuth2CallbackMiddleware handles: * - State validation (CSRF protection) * - Authorization code exchange for tokens * - User info retrieval * - Session storage * - Redirect to original page * - * Supports both multi-provider mode (retrieves provider from session) and - * legacy single-provider mode (uses existing OAuth2Service). - * * Endpoint: GET /oauth2/callback * * Query Parameters (from OIDC provider): @@ -57,23 +52,21 @@ import type { UserInfo } from '../types/oauth2.js' * - error (optional): Error code if authentication failed * - error_description (optional): Human-readable error description * - * Multi-Provider Flow: + * Flow: * OIDC Provider → /oauth2/callback?code=XXX&state=YYY - * → Retrieve provider from session → Use correct OAuth2 client - * → Exchange code for tokens → Original Page (with authenticated session) + * → OAuth2CallbackMiddleware → Original Page (with authenticated session) * * Success Flow: * 1. Validate state parameter - * 2. Retrieve provider from session (or use legacy service) - * 3. Exchange authorization code for tokens (access, refresh, ID) - * 4. Fetch user information from UserInfo endpoint - * 5. Store tokens, provider name, and user data in session - * 6. Redirect to original page or home + * 2. Exchange authorization code for tokens (access, refresh, ID) + * 3. Fetch user information from UserInfo endpoint + * 4. Store tokens and user data in session + * 5. Redirect to original page or home * * Error Flow: * 1. Parse error from query parameters - * 2. Log error details - * 3. Redirect to home with error parameter + * 2. Display user-friendly error page + * 3. Allow user to retry authentication * * @example * // Successful callback URL from OIDC provider @@ -84,216 +77,22 @@ import type { UserInfo } from '../types/oauth2.js' */ @Service() @Controller() +@UseBefore(OAuth2CallbackMiddleware) export class OAuth2CallbackController { - private providerManager: OAuth2ProviderManager - private legacyOAuth2Service: OAuth2Service - - constructor() { - this.providerManager = Container.get(OAuth2ProviderManager) - this.legacyOAuth2Service = Container.get(OAuth2Service) - } - /** * Handle OAuth2/OIDC callback * - * Processes the callback from any configured OIDC provider. - * Supports both multi-provider mode and legacy single-provider mode. + * The actual logic is handled by OAuth2CallbackMiddleware. + * This method exists only as the routing endpoint definition. * - * @param code - Authorization code from OIDC provider - * @param state - State parameter for CSRF validation - * @param error - Error code if authentication failed - * @param errorDescription - Human-readable error description - * @param request - Express request object - * @param response - Express response object - * @returns Response with redirect to original page or error page + * @param {Request} request - Express request object with query params (code, state) + * @param {Response} response - Express response object (redirected by middleware) + * @returns {Response} Response object (handled by middleware) */ @Get('/oauth2/callback') - async callback( - @QueryParam('code') code: string, - @QueryParam('state') state: string, - @QueryParam('error') error: string, - @QueryParam('error_description') errorDescription: string, - @Req() request: Request, - @Res() response: Response - ): Promise { - console.log('OAuth2CallbackController: Processing OAuth2 callback') - - const session = request.session as any - - // Handle error from provider - if (error) { - console.error(`OAuth2CallbackController: Error from provider: ${error}`) - console.error(`OAuth2CallbackController: Description: ${errorDescription || 'N/A'}`) - return response.redirect(`/?oauth2_error=${encodeURIComponent(error)}`) - } - - // Validate required parameters - if (!code) { - console.error('OAuth2CallbackController: Missing authorization code') - return response.redirect('/?oauth2_error=missing_code') - } - - if (!state) { - console.error('OAuth2CallbackController: Missing state parameter') - return response.redirect('/?oauth2_error=missing_state') - } - - // Validate state (CSRF protection) - const storedState = session.oauth2_state - if (!storedState || storedState !== state) { - console.error('OAuth2CallbackController: State mismatch (CSRF protection)') - console.error(` Expected: ${storedState}`) - console.error(` Received: ${state}`) - return response.redirect('/?oauth2_error=invalid_state') - } - - // Get code verifier from session (PKCE) - const codeVerifier = session.oauth2_code_verifier - if (!codeVerifier) { - console.error('OAuth2CallbackController: Code verifier not found in session') - return response.redirect('/?oauth2_error=missing_verifier') - } - - // Check if multi-provider mode (provider stored in session) - const provider = session.oauth2_provider - - try { - if (provider) { - // Multi-provider mode - await this.handleMultiProviderCallback(session, code, codeVerifier, provider) - } else { - // Legacy single-provider mode - await this.handleLegacyCallback(session, code, codeVerifier) - } - - // Clean up temporary session data - delete session.oauth2_code_verifier - delete session.oauth2_state - - // Redirect to original page - const redirectUrl = session.oauth2_redirect_page || '/' - delete session.oauth2_redirect_page - - console.log( - `OAuth2CallbackController: Authentication successful, redirecting to: ${redirectUrl}` - ) - return response.redirect(redirectUrl) - } catch (error) { - console.error('OAuth2CallbackController: Token exchange failed:', error) - const errorMessage = error instanceof Error ? error.message : 'Unknown error' - return response.redirect( - `/?oauth2_error=token_exchange_failed&details=${encodeURIComponent(errorMessage)}` - ) - } - } - - /** - * Handle multi-provider callback - */ - private async handleMultiProviderCallback( - session: any, - code: string, - codeVerifier: string, - provider: string - ): Promise { - console.log(`OAuth2CallbackController: Multi-provider mode - ${provider}`) - - const client = this.providerManager.getProvider(provider) - if (!client) { - throw new Error(`Provider not found: ${provider}`) - } - - // Exchange code for tokens - console.log(`OAuth2CallbackController: Exchanging authorization code for tokens`) - const tokens = await client.exchangeAuthorizationCode(code, codeVerifier) - - // Store tokens in session - session.oauth2_access_token = tokens.accessToken - session.oauth2_refresh_token = tokens.refreshToken - session.oauth2_id_token = tokens.idToken - session.oauth2_provider = provider - - console.log(`OAuth2CallbackController: Tokens received and stored`) - - // Fetch user info - console.log(`OAuth2CallbackController: Fetching user info`) - const userInfo = await this.fetchUserInfo(client, tokens.accessToken) - - // Store user in session - session.user = { - username: userInfo.preferred_username || userInfo.email || userInfo.sub, - email: userInfo.email, - name: userInfo.name, - provider: provider, - sub: userInfo.sub - } - - console.log( - `OAuth2CallbackController: User authenticated via ${provider}: ${session.user.username}` - ) - } - - /** - * Handle legacy single-provider callback - */ - private async handleLegacyCallback( - session: any, - code: string, - codeVerifier: string - ): Promise { - console.log('OAuth2CallbackController: Legacy single-provider mode') - - // Exchange code for tokens using legacy service - console.log(`OAuth2CallbackController: Exchanging authorization code for tokens`) - const tokens = await this.legacyOAuth2Service.exchangeCodeForTokens(code, codeVerifier) - - // Store tokens in session - session.oauth2_access_token = tokens.accessToken - session.oauth2_refresh_token = tokens.refreshToken - session.oauth2_id_token = tokens.idToken - - console.log(`OAuth2CallbackController: Tokens received and stored`) - - // Fetch user info - console.log(`OAuth2CallbackController: Fetching user info`) - const userInfo = await this.legacyOAuth2Service.getUserInfo(tokens.accessToken) - - // Store user in session - session.user = { - username: userInfo.preferred_username || userInfo.email || userInfo.sub, - email: userInfo.email, - name: userInfo.name, - sub: userInfo.sub - } - - console.log(`OAuth2CallbackController: User authenticated (legacy): ${session.user.username}`) - } - - /** - * Fetch user info from UserInfo endpoint - */ - private async fetchUserInfo(client: any, accessToken: string): Promise { - const userInfoEndpoint = client.getUserInfoEndpoint() - - console.log(`OAuth2CallbackController: Calling UserInfo endpoint: ${userInfoEndpoint}`) - - const response = await fetch(userInfoEndpoint, { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json' - } - }) - - if (!response.ok) { - const errorText = await response.text() - throw new Error( - `UserInfo request failed: ${response.status} ${response.statusText} - ${errorText}` - ) - } - - const userInfo = await response.json() - console.log(`OAuth2CallbackController: UserInfo retrieved: ${userInfo.sub}`) - - return userInfo as UserInfo + callback(@Req() request: Request, @Res() response: Response): Response { + // The middleware handles all the logic and redirects the user + // This method should never actually execute + return response } } diff --git a/server/controllers/OAuth2ConnectController.ts b/server/controllers/OAuth2ConnectController.ts index 183e018..dc7f8aa 100644 --- a/server/controllers/OAuth2ConnectController.ts +++ b/server/controllers/OAuth2ConnectController.ts @@ -25,175 +25,53 @@ * */ -import { Controller, Req, Res, Get, QueryParam } from 'routing-controllers' +import { Controller, Req, Res, Get, UseBefore } from 'routing-controllers' import type { Request, Response } from 'express' -import { Service, Container } from 'typedi' -import { OAuth2Service } from '../services/OAuth2Service.js' -import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js' -import { PKCEUtils } from '../utils/pkce.js' +import { Service } from 'typedi' +import OAuth2AuthorizationMiddleware from '../middlewares/OAuth2AuthorizationMiddleware.js' /** - * OAuth2 Connect Controller (Multi-Provider) + * OAuth2 Connect Controller * - * Handles the OAuth2/OIDC login initiation endpoint with support for multiple providers. - * This controller generates PKCE parameters and redirects to the selected OIDC provider. + * Handles the OAuth2/OIDC login initiation endpoint. + * This controller triggers the OAuth2 authorization flow by delegating to + * the OAuth2AuthorizationMiddleware which generates PKCE parameters and + * redirects to the OIDC provider. * * Endpoint: GET /oauth2/connect * * Query Parameters: - * - provider (optional): Provider name (e.g., "obp-oidc", "keycloak") * - redirect (optional): URL to redirect to after successful authentication * - * Multi-Provider Flow: - * User selects provider → /oauth2/connect?provider=obp-oidc&redirect=/resource-docs - * → Generate PKCE → Store in session → Redirect to OIDC provider - * - * Legacy Flow (backward compatible): - * User clicks login → /oauth2/connect → Uses existing OAuth2Service (single provider) + * Flow: + * User clicks login → /oauth2/connect → OAuth2AuthorizationMiddleware + * → OIDC Provider Authorization Endpoint * * @example - * // Multi-provider login - * Login with OBP-OIDC - * - * // Legacy single-provider login (backward compatible) + * // User initiates login * Login + * + * // JavaScript redirect + * window.location.href = '/oauth2/connect?redirect=' + encodeURIComponent(window.location.pathname) */ @Service() @Controller() +@UseBefore(OAuth2AuthorizationMiddleware) export class OAuth2ConnectController { - private providerManager: OAuth2ProviderManager - private legacyOAuth2Service: OAuth2Service - - constructor() { - this.providerManager = Container.get(OAuth2ProviderManager) - this.legacyOAuth2Service = Container.get(OAuth2Service) - } - /** * Initiate OAuth2/OIDC authentication flow * - * Supports both multi-provider mode (with provider parameter) and legacy single-provider mode. + * The actual logic is handled by OAuth2AuthorizationMiddleware. + * This method exists only as the routing endpoint definition. * - * @param provider - Provider name (e.g., "obp-oidc", "keycloak") - optional for backward compatibility - * @param redirect - URL to redirect after authentication - optional - * @param request - Express request object - * @param response - Express response object - * @returns Response with redirect to OIDC provider + * @param {Request} request - Express request object + * @param {Response} response - Express response object (redirected by middleware) + * @returns {Response} Response object (handled by middleware) */ @Get('/oauth2/connect') - connect( - @QueryParam('provider') provider: string, - @QueryParam('redirect') redirect: string, - @Req() request: Request, - @Res() response: Response - ): Response { - console.log('OAuth2ConnectController: Starting authentication flow') - console.log(` Provider: ${provider || '(legacy mode)'}`) - console.log(` Redirect: ${redirect || '/'}`) - - const session = request.session as any - - // Store redirect URL in session - session.oauth2_redirect_page = redirect || '/' - - // Multi-provider mode: Use provider from query param - if (provider) { - return this.handleMultiProviderLogin(provider, session, response) - } - - // Legacy single-provider mode: Use existing OAuth2Service - return this.handleLegacyLogin(session, response) - } - - /** - * Handle multi-provider login - */ - private handleMultiProviderLogin(provider: string, session: any, response: Response): Response { - console.log(`OAuth2ConnectController: Multi-provider mode - ${provider}`) - - const client = this.providerManager.getProvider(provider) - - if (!client) { - console.error(`OAuth2ConnectController: Provider not found: ${provider}`) - const availableProviders = this.providerManager.getAvailableProviders() - console.error( - `OAuth2ConnectController: Available providers: ${availableProviders.join(', ') || 'none'}` - ) - return response.status(400).json({ - error: 'invalid_provider', - message: `Provider "${provider}" is not available`, - availableProviders: availableProviders - }) - } - - // Store provider name in session for callback - session.oauth2_provider = provider - - // Generate PKCE parameters - const codeVerifier = PKCEUtils.generateCodeVerifier() - const codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier) - const state = PKCEUtils.generateState() - - // Store in session - session.oauth2_code_verifier = codeVerifier - session.oauth2_state = state - - // Build authorization URL - const authUrl = this.buildAuthorizationUrl(client, state, codeChallenge) - - console.log(`OAuth2ConnectController: Redirecting to ${provider} authorization endpoint`) - return response.redirect(authUrl) - } - - /** - * Handle legacy single-provider login - */ - private handleLegacyLogin(session: any, response: Response): Response { - console.log('OAuth2ConnectController: Legacy single-provider mode') - - if (!this.legacyOAuth2Service.isInitialized()) { - console.error('OAuth2ConnectController: OAuth2 service not initialized') - return response.status(503).json({ - error: 'oauth2_unavailable', - message: 'OAuth2 authentication is not available' - }) - } - - // Generate PKCE parameters - const codeVerifier = PKCEUtils.generateCodeVerifier() - const codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier) - const state = PKCEUtils.generateState() - - // Store in session - session.oauth2_code_verifier = codeVerifier - session.oauth2_state = state - - // Use legacy service to create authorization URL - const authUrl = this.legacyOAuth2Service.createAuthorizationURL(state, [ - 'openid', - 'profile', - 'email' - ]) - - console.log('OAuth2ConnectController: Redirecting to legacy OIDC provider') - return response.redirect(authUrl.toString()) - } - - /** - * Build authorization URL for multi-provider - */ - private buildAuthorizationUrl(client: any, state: string, codeChallenge: string): string { - const authEndpoint = client.getAuthorizationEndpoint() - const params = new URLSearchParams({ - client_id: client.clientId, - redirect_uri: client.getRedirectUri(), - response_type: 'code', - scope: 'openid profile email', - state: state, - code_challenge: codeChallenge, - code_challenge_method: 'S256' - }) - - return `${authEndpoint}?${params.toString()}` + connect(@Req() request: Request, @Res() response: Response): Response { + // The middleware handles all the logic and redirects the user + // This method should never actually execute + return response } } diff --git a/server/routes/oauth2.ts b/server/routes/oauth2.ts new file mode 100644 index 0000000..ac54840 --- /dev/null +++ b/server/routes/oauth2.ts @@ -0,0 +1,311 @@ +/* + * Open Bank Project - API Explorer II + * Copyright (C) 2023-2025, TESOBE GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * Email: contact@tesobe.com + * TESOBE GmbH + * Osloerstrasse 16/17 + * Berlin 13359, Germany + * + * This product includes software developed at + * TESOBE (http://www.tesobe.com/) + * + */ + +import { Router } from 'express' +import type { Request, Response } from 'express' +import { Container } from 'typedi' +import { OAuth2Service } from '../services/OAuth2Service.js' +import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js' +import { PKCEUtils } from '../utils/pkce.js' +import type { UserInfo } from '../types/oauth2.js' + +const router = Router() + +// Get services from container +const providerManager = Container.get(OAuth2ProviderManager) +const legacyOAuth2Service = Container.get(OAuth2Service) + +/** + * GET /oauth2/providers + * Get list of available OAuth2 providers + */ +router.get('/oauth2/providers', async (req: Request, res: Response) => { + try { + const availableProviders = providerManager.getAvailableProviders() + const providerList = availableProviders.map((name) => { + const status = providerManager.getProviderStatus(name) + return { + name, + status: status?.status || 'unknown', + available: status?.available || false + } + }) + + res.json({ providers: providerList }) + } catch (error) { + console.error('Error fetching providers:', error) + res.status(500).json({ error: 'Failed to fetch providers' }) + } +}) + +/** + * GET /oauth2/connect + * Initiate OAuth2 authentication flow + * Query params: + * - provider: Provider name (optional, uses legacy if not specified) + * - redirect: URL to redirect after auth (optional) + */ +router.get('/oauth2/connect', async (req: Request, res: Response) => { + try { + const provider = req.query.provider as string | undefined + const redirect = (req.query.redirect as string) || '/' + const session = req.session as any + + console.log('OAuth2 Connect: Starting authentication flow') + console.log(` Provider: ${provider || '(legacy mode)'}`) + console.log(` Redirect: ${redirect}`) + + // Store redirect URL in session + session.oauth2_redirect_page = redirect + + // Generate PKCE parameters + const codeVerifier = PKCEUtils.generateCodeVerifier() + const codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier) + const state = PKCEUtils.generateState() + + // Store in session + session.oauth2_code_verifier = codeVerifier + session.oauth2_state = state + + let authUrl: string + + if (provider) { + // Multi-provider mode + console.log(`OAuth2 Connect: Using multi-provider mode - ${provider}`) + + const client = providerManager.getProvider(provider) + if (!client) { + const availableProviders = providerManager.getAvailableProviders() + console.error(`OAuth2 Connect: Provider not found: ${provider}`) + return res.status(400).json({ + error: 'invalid_provider', + message: `Provider "${provider}" is not available`, + availableProviders + }) + } + + // Store provider name for callback + session.oauth2_provider = provider + + // Build authorization URL + const authEndpoint = client.getAuthorizationEndpoint() + const params = new URLSearchParams({ + client_id: client.clientId, + redirect_uri: client.getRedirectUri(), + response_type: 'code', + scope: 'openid profile email', + state: state, + code_challenge: codeChallenge, + code_challenge_method: 'S256' + }) + + authUrl = `${authEndpoint}?${params.toString()}` + } else { + // Legacy single-provider mode + console.log('OAuth2 Connect: Using legacy single-provider mode') + + if (!legacyOAuth2Service.isInitialized()) { + console.error('OAuth2 Connect: OAuth2 service not initialized') + return res.status(503).json({ + error: 'oauth2_unavailable', + message: 'OAuth2 authentication is not available' + }) + } + + authUrl = legacyOAuth2Service + .createAuthorizationURL(state, ['openid', 'profile', 'email']) + .toString() + } + + // Save session before redirect + session.save((err: any) => { + if (err) { + console.error('OAuth2 Connect: Failed to save session:', err) + return res.status(500).json({ error: 'session_error' }) + } + + console.log('OAuth2 Connect: Redirecting to authorization endpoint') + res.redirect(authUrl) + }) + } catch (error) { + console.error('OAuth2 Connect: Error:', error) + res.status(500).json({ + error: 'authentication_failed', + message: error instanceof Error ? error.message : 'Unknown error' + }) + } +}) + +/** + * GET /oauth2/callback + * Handle OAuth2 callback after user authentication + * Query params: + * - code: Authorization code + * - state: State parameter for CSRF validation + * - error: Error code (if auth failed) + * - error_description: Error description + */ +router.get('/oauth2/callback', async (req: Request, res: Response) => { + try { + const code = req.query.code as string + const state = req.query.state as string + const error = req.query.error as string + const errorDescription = req.query.error_description as string + const session = req.session as any + + console.log('OAuth2 Callback: Processing callback') + + // Handle error from provider + if (error) { + console.error(`OAuth2 Callback: Error from provider: ${error}`) + console.error(`OAuth2 Callback: Description: ${errorDescription || 'N/A'}`) + return res.redirect(`/?oauth2_error=${encodeURIComponent(error)}`) + } + + // Validate required parameters + if (!code) { + console.error('OAuth2 Callback: Missing authorization code') + return res.redirect('/?oauth2_error=missing_code') + } + + if (!state) { + console.error('OAuth2 Callback: Missing state parameter') + return res.redirect('/?oauth2_error=missing_state') + } + + // Validate state (CSRF protection) + const storedState = session.oauth2_state + if (!storedState || storedState !== state) { + console.error('OAuth2 Callback: State mismatch (CSRF protection)') + return res.redirect('/?oauth2_error=invalid_state') + } + + // Get code verifier from session (PKCE) + const codeVerifier = session.oauth2_code_verifier + if (!codeVerifier) { + console.error('OAuth2 Callback: Code verifier not found in session') + return res.redirect('/?oauth2_error=missing_verifier') + } + + // Check if multi-provider mode + const provider = session.oauth2_provider + + let tokens: any + let userInfo: UserInfo + + if (provider) { + // Multi-provider mode + console.log(`OAuth2 Callback: Multi-provider mode - ${provider}`) + + const client = providerManager.getProvider(provider) + if (!client) { + console.error(`OAuth2 Callback: Provider not found: ${provider}`) + return res.redirect('/?oauth2_error=provider_not_found') + } + + // Exchange code for tokens + console.log('OAuth2 Callback: Exchanging authorization code for tokens') + tokens = await client.exchangeAuthorizationCode(code, codeVerifier) + + // Fetch user info + console.log('OAuth2 Callback: Fetching user info') + const userInfoEndpoint = client.getUserInfoEndpoint() + const userInfoResponse = await fetch(userInfoEndpoint, { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + Accept: 'application/json' + } + }) + + if (!userInfoResponse.ok) { + throw new Error(`UserInfo request failed: ${userInfoResponse.status}`) + } + + userInfo = (await userInfoResponse.json()) as UserInfo + + // Store provider in session + session.oauth2_provider = provider + } else { + // Legacy single-provider mode + console.log('OAuth2 Callback: Legacy single-provider mode') + + // Exchange code for tokens + tokens = await legacyOAuth2Service.exchangeCodeForTokens(code, codeVerifier) + + // Fetch user info + userInfo = await legacyOAuth2Service.getUserInfo(tokens.accessToken) + } + + // Store tokens in session + session.oauth2_access_token = tokens.accessToken + session.oauth2_refresh_token = tokens.refreshToken + session.oauth2_id_token = tokens.idToken + + console.log('OAuth2 Callback: Tokens received and stored') + + // Store user in session (using oauth2_user key to match UserController) + session.oauth2_user = { + username: userInfo.preferred_username || userInfo.email || userInfo.sub, + email: userInfo.email, + email_verified: userInfo.email_verified || false, + name: userInfo.name, + given_name: userInfo.given_name, + family_name: userInfo.family_name, + provider: provider || 'obp-oidc', + sub: userInfo.sub + } + + // Also store clientConfig for OBP API calls + session.clientConfig = { + oauth2: { + accessToken: tokens.accessToken, + tokenType: 'Bearer' + } + } + + console.log( + `OAuth2 Callback: User authenticated: ${session.oauth2_user.username} via ${session.oauth2_user.provider}` + ) + + // Clean up temporary session data + delete session.oauth2_code_verifier + delete session.oauth2_state + + // Redirect to original page + const redirectUrl = session.oauth2_redirect_page || '/' + delete session.oauth2_redirect_page + + console.log(`OAuth2 Callback: Authentication successful, redirecting to: ${redirectUrl}`) + res.redirect(redirectUrl) + } catch (error) { + console.error('OAuth2 Callback: Error:', error) + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + res.redirect(`/?oauth2_error=token_exchange_failed&details=${encodeURIComponent(errorMessage)}`) + } +}) + +export default router