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)
This commit is contained in:
simonredfern 2025-12-28 15:26:26 +01:00
parent 743038953d
commit 0eace070f9
4 changed files with 522 additions and 61 deletions

View File

@ -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]
})

View File

@ -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<Response> {
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<void> {
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<void> {
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<UserInfo> {
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
}
}

View File

@ -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
* <a href="/oauth2/connect?redirect=/messages">Login</a>
* // Multi-provider login
* <a href="/oauth2/connect?provider=obp-oidc&redirect=/messages">Login with OBP-OIDC</a>
*
* // JavaScript redirect
* window.location.href = '/oauth2/connect?redirect=' + encodeURIComponent(window.location.pathname)
* // Legacy single-provider login (backward compatible)
* <a href="/oauth2/connect?redirect=/messages">Login</a>
*/
@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()}`
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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<any> {
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
}
}
}