From 0eace070f9b7523cb12f035d538f8491f9781dbd Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sun, 28 Dec 2025 15:26:26 +0100 Subject: [PATCH] Add multi-OIDC provider controllers and update app initialization - Create OAuth2ProvidersController to list available providers - Update OAuth2ConnectController to support provider parameter - Update OAuth2CallbackController to handle multi-provider callbacks - Update app.ts to initialize OAuth2ProviderManager on startup - Maintain backward compatibility with legacy single-provider mode - Add health monitoring for all providers (60s intervals) --- server/app.ts | 54 +++- .../controllers/OAuth2CallbackController.ts | 249 ++++++++++++++++-- server/controllers/OAuth2ConnectController.ts | 172 ++++++++++-- .../controllers/OAuth2ProvidersController.ts | 108 ++++++++ 4 files changed, 522 insertions(+), 61 deletions(-) create mode 100644 server/controllers/OAuth2ProvidersController.ts diff --git a/server/app.ts b/server/app.ts index 174d715..9ba2a25 100644 --- a/server/app.ts +++ b/server/app.ts @@ -37,6 +37,7 @@ import { Container } from 'typedi' import path from 'path' import { execSync } from 'child_process' import { OAuth2Service } from './services/OAuth2Service.js' +import { OAuth2ProviderManager } from './services/OAuth2ProviderManager.js' import { fileURLToPath } from 'url' import { dirname } from 'path' @@ -47,6 +48,7 @@ import { StatusController } from './controllers/StatusController.js' import { UserController } from './controllers/UserController.js' import { OAuth2CallbackController } from './controllers/OAuth2CallbackController.js' import { OAuth2ConnectController } from './controllers/OAuth2ConnectController.js' +import { OAuth2ProvidersController } from './controllers/OAuth2ProvidersController.js' // Import middlewares import OAuth2AuthorizationMiddleware from './middlewares/OAuth2AuthorizationMiddleware.js' @@ -148,11 +150,38 @@ const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL // Async IIFE to initialize OAuth2 and start server let instance: any ;(async function initializeAndStartServer() { + // Initialize Multi-Provider OAuth2 Manager + console.log('--- OAuth2 Multi-Provider Setup ---------------------------------') + const providerManager = Container.get(OAuth2ProviderManager) + + try { + const success = await providerManager.initializeProviders() + + if (success) { + const availableProviders = providerManager.getAvailableProviders() + console.log(`✓ Initialized ${availableProviders.length} OAuth2 providers:`) + availableProviders.forEach((name) => console.log(` - ${name}`)) + + // Start health monitoring + providerManager.startHealthCheck(60000) // Check every 60 seconds + console.log('✓ Provider health monitoring started (every 60s)') + } else { + console.warn('⚠ No OAuth2 providers initialized from OBP API') + console.warn('⚠ Falling back to legacy single-provider mode...') + } + } catch (error) { + console.error('✗ Failed to initialize OAuth2 multi-provider:', error) + console.warn('⚠ Falling back to legacy single-provider mode...') + } + console.log(`-----------------------------------------------------------------`) + + // Initialize Legacy OAuth2 Service (for backward compatibility) + console.log(`--- OAuth2/OIDC Legacy Setup (Backward Compatibility) -----------`) if (!wellKnownUrl) { - console.warn('VITE_OBP_OAUTH2_WELL_KNOWN_URL not set. OAuth2 will not function.') - console.warn('Server will start but OAuth2 authentication will be unavailable.') + console.warn('VITE_OBP_OAUTH2_WELL_KNOWN_URL not set. Legacy OAuth2 will not function.') + console.warn('Server will rely on multi-provider mode from OBP API.') } else { - console.log(`OIDC Well-Known URL: ${wellKnownUrl}`) + console.log(`OIDC Well-Known URL (legacy): ${wellKnownUrl}`) // Get OAuth2Service from container const oauth2Service = Container.get(OAuth2Service) @@ -163,27 +192,27 @@ let instance: any const initialDelay = 1000 // 1 second, then exponential backoff console.log( - 'Attempting OAuth2 initialization (will retry indefinitely with exponential backoff)...' + 'Attempting legacy OAuth2 initialization (will retry indefinitely with exponential backoff)...' ) const success = await oauth2Service.initializeWithRetry(wellKnownUrl, maxRetries, initialDelay) if (success) { - console.log('OAuth2Service: Initialization successful') + console.log('OAuth2Service (legacy): Initialization successful') console.log(' Client ID:', process.env.VITE_OBP_OAUTH2_CLIENT_ID || 'NOT SET') console.log(' Redirect URI:', process.env.VITE_OBP_OAUTH2_REDIRECT_URL || 'NOT SET') - console.log('OAuth2/OIDC ready for authentication') + console.log('Legacy OAuth2/OIDC ready for authentication') // Start continuous monitoring even when initially connected oauth2Service.startHealthCheck(1000, 240000) // Monitor every 4 minutes - console.log('OAuth2Service: Starting continuous monitoring (every 4 minutes)') + console.log('OAuth2Service (legacy): Starting continuous monitoring (every 4 minutes)') } else { - console.error('OAuth2Service: Initialization failed after all retries') + console.error('OAuth2Service (legacy): Initialization failed after all retries') // Use graceful degradation for both development and production const envMode = isProduction ? 'Production' : 'Development' - console.warn(`WARNING: ${envMode} mode: Server will start without OAuth2`) - console.warn('WARNING: Login will be unavailable until OIDC server is reachable') - console.warn('WARNING: Starting health check to reconnect automatically...') + console.warn(`WARNING: ${envMode} mode: Server will start without legacy OAuth2`) + console.warn('WARNING: Legacy login will be unavailable until OIDC server is reachable') + console.warn('WARNING: Multi-provider mode will be used if available') console.warn('Please check:') console.warn(' 1. OBP-OIDC server is running') console.warn(' 2. VITE_OBP_OAUTH2_WELL_KNOWN_URL is correct') @@ -205,7 +234,8 @@ let instance: any StatusController, UserController, OAuth2CallbackController, - OAuth2ConnectController + OAuth2ConnectController, + OAuth2ProvidersController ], middlewares: [OAuth2AuthorizationMiddleware, OAuth2CallbackMiddleware] }) diff --git a/server/controllers/OAuth2CallbackController.ts b/server/controllers/OAuth2CallbackController.ts index 44bc529..d6b7ac4 100644 --- a/server/controllers/OAuth2CallbackController.ts +++ b/server/controllers/OAuth2CallbackController.ts @@ -25,25 +25,30 @@ * */ -import { Controller, Req, Res, Get, UseBefore } from 'routing-controllers' +import { Controller, Req, Res, Get, QueryParam } from 'routing-controllers' import type { Request, Response } from 'express' -import { Service } from 'typedi' -import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js' +import { Service, Container } from 'typedi' +import { OAuth2Service } from '../services/OAuth2Service.js' +import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js' +import type { UserInfo } from '../types/oauth2.js' /** - * OAuth2 Callback Controller + * OAuth2 Callback Controller (Multi-Provider) * - * Handles the OAuth2/OIDC callback from the identity provider. + * Handles the OAuth2/OIDC callback from any configured identity provider. * This controller receives the authorization code and state parameter * after the user authenticates with the OIDC provider. * - * The OAuth2CallbackMiddleware handles: + * This controller 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): @@ -52,21 +57,23 @@ import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js * - error (optional): Error code if authentication failed * - error_description (optional): Human-readable error description * - * Flow: + * Multi-Provider Flow: * OIDC Provider → /oauth2/callback?code=XXX&state=YYY - * → OAuth2CallbackMiddleware → Original Page (with authenticated session) + * → Retrieve provider from session → Use correct OAuth2 client + * → Exchange code for tokens → Original Page (with authenticated session) * * Success Flow: * 1. Validate state parameter - * 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 + * 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 * * Error Flow: * 1. Parse error from query parameters - * 2. Display user-friendly error page - * 3. Allow user to retry authentication + * 2. Log error details + * 3. Redirect to home with error parameter * * @example * // Successful callback URL from OIDC provider @@ -77,22 +84,216 @@ import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.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 * - * The actual logic is handled by OAuth2CallbackMiddleware. - * This method exists only as the routing endpoint definition. + * Processes the callback from any configured OIDC provider. + * Supports both multi-provider mode and legacy single-provider mode. * - * @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) + * @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 */ @Get('/oauth2/callback') - 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 + 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.validateAuthorizationCode(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 } } diff --git a/server/controllers/OAuth2ConnectController.ts b/server/controllers/OAuth2ConnectController.ts index dc7f8aa..50d95fe 100644 --- a/server/controllers/OAuth2ConnectController.ts +++ b/server/controllers/OAuth2ConnectController.ts @@ -25,53 +25,175 @@ * */ -import { Controller, Req, Res, Get, UseBefore } from 'routing-controllers' +import { Controller, Req, Res, Get, QueryParam } from 'routing-controllers' import type { Request, Response } from 'express' -import { Service } from 'typedi' -import OAuth2AuthorizationMiddleware from '../middlewares/OAuth2AuthorizationMiddleware.js' +import { Service, Container } from 'typedi' +import { OAuth2Service } from '../services/OAuth2Service.js' +import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js' +import { PKCEUtils } from '../utils/pkce.js' /** - * OAuth2 Connect Controller + * OAuth2 Connect Controller (Multi-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. + * Handles the OAuth2/OIDC login initiation endpoint with support for multiple providers. + * This controller generates PKCE parameters and redirects to the selected 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 * - * Flow: - * User clicks login → /oauth2/connect → OAuth2AuthorizationMiddleware - * → OIDC Provider Authorization Endpoint + * 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) * * @example - * // User initiates login - * Login + * // Multi-provider login + * Login with OBP-OIDC * - * // JavaScript redirect - * window.location.href = '/oauth2/connect?redirect=' + encodeURIComponent(window.location.pathname) + * // Legacy single-provider login (backward compatible) + * Login */ @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 * - * The actual logic is handled by OAuth2AuthorizationMiddleware. - * This method exists only as the routing endpoint definition. + * Supports both multi-provider mode (with provider parameter) and legacy single-provider mode. * - * @param {Request} request - Express request object - * @param {Response} response - Express response object (redirected by middleware) - * @returns {Response} Response object (handled by middleware) + * @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 */ @Get('/oauth2/connect') - 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 + 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, codeVerifier, [ + 'openid', + 'profile', + 'email' + ]) + + console.log('OAuth2ConnectController: Redirecting to legacy OIDC provider') + return response.redirect(authUrl) + } + + /** + * 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.redirectURI, + response_type: 'code', + scope: 'openid profile email', + state: state, + code_challenge: codeChallenge, + code_challenge_method: 'S256' + }) + + return `${authEndpoint}?${params.toString()}` } } diff --git a/server/controllers/OAuth2ProvidersController.ts b/server/controllers/OAuth2ProvidersController.ts new file mode 100644 index 0000000..e295b2d --- /dev/null +++ b/server/controllers/OAuth2ProvidersController.ts @@ -0,0 +1,108 @@ +/* + * Open Bank Project - API Explorer II + * Copyright (C) 2023-2024, 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 { Controller, Get } from 'routing-controllers' +import { Service, Container } from 'typedi' +import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js' + +/** + * OAuth2 Providers Controller + * + * Provides endpoints to query available OIDC providers + * + * Endpoints: + * GET /api/oauth2/providers - List available OIDC providers + * + * @example + * // Fetch available providers + * const response = await fetch('/api/oauth2/providers') + * const data = await response.json() + * // { + * // providers: [ + * // { name: "obp-oidc", available: true, lastChecked: "2024-01-15T10:30:00Z" }, + * // { name: "keycloak", available: false, lastChecked: "2024-01-15T10:30:00Z", error: "Connection timeout" } + * // ], + * // count: 2, + * // availableCount: 1 + * // } + */ +@Service() +@Controller() +export class OAuth2ProvidersController { + private providerManager: OAuth2ProviderManager + + constructor() { + this.providerManager = Container.get(OAuth2ProviderManager) + } + + /** + * Get list of available OAuth2/OIDC providers + * + * Returns provider names and availability status for all configured providers. + * This endpoint is used by the frontend to display provider selection UI. + * + * @returns JSON response with providers array, total count, and available count + * + * @example + * GET /api/oauth2/providers + * + * Response: + * { + * "providers": [ + * { + * "name": "obp-oidc", + * "available": true, + * "lastChecked": "2024-01-15T10:30:00.000Z" + * }, + * { + * "name": "keycloak", + * "available": false, + * "lastChecked": "2024-01-15T10:30:00.000Z", + * "error": "Connection timeout" + * } + * ], + * "count": 2, + * "availableCount": 1 + * } + */ + @Get('/api/oauth2/providers') + async getProviders(): Promise { + console.log('OAuth2ProvidersController: Fetching provider list') + + const allStatus = this.providerManager.getAllProviderStatus() + const availableProviders = this.providerManager.getAvailableProviders() + + console.log(`OAuth2ProvidersController: Total providers: ${allStatus.length}`) + console.log(`OAuth2ProvidersController: Available providers: ${availableProviders.length}`) + + return { + providers: allStatus, + count: allStatus.length, + availableCount: availableProviders.length + } + } +}