Skip to content

Commit 8b90bb4

Browse files
committed
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)
1 parent 3dadca8 commit 8b90bb4

4 files changed

Lines changed: 522 additions & 61 deletions

File tree

server/app.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Container } from 'typedi'
3737
import path from 'path'
3838
import { execSync } from 'child_process'
3939
import { OAuth2Service } from './services/OAuth2Service.js'
40+
import { OAuth2ProviderManager } from './services/OAuth2ProviderManager.js'
4041
import { fileURLToPath } from 'url'
4142
import { dirname } from 'path'
4243

@@ -47,6 +48,7 @@ import { StatusController } from './controllers/StatusController.js'
4748
import { UserController } from './controllers/UserController.js'
4849
import { OAuth2CallbackController } from './controllers/OAuth2CallbackController.js'
4950
import { OAuth2ConnectController } from './controllers/OAuth2ConnectController.js'
51+
import { OAuth2ProvidersController } from './controllers/OAuth2ProvidersController.js'
5052

5153
// Import middlewares
5254
import OAuth2AuthorizationMiddleware from './middlewares/OAuth2AuthorizationMiddleware.js'
@@ -148,11 +150,38 @@ const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL
148150
// Async IIFE to initialize OAuth2 and start server
149151
let instance: any
150152
;(async function initializeAndStartServer() {
153+
// Initialize Multi-Provider OAuth2 Manager
154+
console.log('--- OAuth2 Multi-Provider Setup ---------------------------------')
155+
const providerManager = Container.get(OAuth2ProviderManager)
156+
157+
try {
158+
const success = await providerManager.initializeProviders()
159+
160+
if (success) {
161+
const availableProviders = providerManager.getAvailableProviders()
162+
console.log(`✓ Initialized ${availableProviders.length} OAuth2 providers:`)
163+
availableProviders.forEach((name) => console.log(` - ${name}`))
164+
165+
// Start health monitoring
166+
providerManager.startHealthCheck(60000) // Check every 60 seconds
167+
console.log('✓ Provider health monitoring started (every 60s)')
168+
} else {
169+
console.warn('⚠ No OAuth2 providers initialized from OBP API')
170+
console.warn('⚠ Falling back to legacy single-provider mode...')
171+
}
172+
} catch (error) {
173+
console.error('✗ Failed to initialize OAuth2 multi-provider:', error)
174+
console.warn('⚠ Falling back to legacy single-provider mode...')
175+
}
176+
console.log(`-----------------------------------------------------------------`)
177+
178+
// Initialize Legacy OAuth2 Service (for backward compatibility)
179+
console.log(`--- OAuth2/OIDC Legacy Setup (Backward Compatibility) -----------`)
151180
if (!wellKnownUrl) {
152-
console.warn('VITE_OBP_OAUTH2_WELL_KNOWN_URL not set. OAuth2 will not function.')
153-
console.warn('Server will start but OAuth2 authentication will be unavailable.')
181+
console.warn('VITE_OBP_OAUTH2_WELL_KNOWN_URL not set. Legacy OAuth2 will not function.')
182+
console.warn('Server will rely on multi-provider mode from OBP API.')
154183
} else {
155-
console.log(`OIDC Well-Known URL: ${wellKnownUrl}`)
184+
console.log(`OIDC Well-Known URL (legacy): ${wellKnownUrl}`)
156185

157186
// Get OAuth2Service from container
158187
const oauth2Service = Container.get(OAuth2Service)
@@ -163,27 +192,27 @@ let instance: any
163192
const initialDelay = 1000 // 1 second, then exponential backoff
164193

165194
console.log(
166-
'Attempting OAuth2 initialization (will retry indefinitely with exponential backoff)...'
195+
'Attempting legacy OAuth2 initialization (will retry indefinitely with exponential backoff)...'
167196
)
168197
const success = await oauth2Service.initializeWithRetry(wellKnownUrl, maxRetries, initialDelay)
169198

170199
if (success) {
171-
console.log('OAuth2Service: Initialization successful')
200+
console.log('OAuth2Service (legacy): Initialization successful')
172201
console.log(' Client ID:', process.env.VITE_OBP_OAUTH2_CLIENT_ID || 'NOT SET')
173202
console.log(' Redirect URI:', process.env.VITE_OBP_OAUTH2_REDIRECT_URL || 'NOT SET')
174-
console.log('OAuth2/OIDC ready for authentication')
203+
console.log('Legacy OAuth2/OIDC ready for authentication')
175204

176205
// Start continuous monitoring even when initially connected
177206
oauth2Service.startHealthCheck(1000, 240000) // Monitor every 4 minutes
178-
console.log('OAuth2Service: Starting continuous monitoring (every 4 minutes)')
207+
console.log('OAuth2Service (legacy): Starting continuous monitoring (every 4 minutes)')
179208
} else {
180-
console.error('OAuth2Service: Initialization failed after all retries')
209+
console.error('OAuth2Service (legacy): Initialization failed after all retries')
181210

182211
// Use graceful degradation for both development and production
183212
const envMode = isProduction ? 'Production' : 'Development'
184-
console.warn(`WARNING: ${envMode} mode: Server will start without OAuth2`)
185-
console.warn('WARNING: Login will be unavailable until OIDC server is reachable')
186-
console.warn('WARNING: Starting health check to reconnect automatically...')
213+
console.warn(`WARNING: ${envMode} mode: Server will start without legacy OAuth2`)
214+
console.warn('WARNING: Legacy login will be unavailable until OIDC server is reachable')
215+
console.warn('WARNING: Multi-provider mode will be used if available')
187216
console.warn('Please check:')
188217
console.warn(' 1. OBP-OIDC server is running')
189218
console.warn(' 2. VITE_OBP_OAUTH2_WELL_KNOWN_URL is correct')
@@ -205,7 +234,8 @@ let instance: any
205234
StatusController,
206235
UserController,
207236
OAuth2CallbackController,
208-
OAuth2ConnectController
237+
OAuth2ConnectController,
238+
OAuth2ProvidersController
209239
],
210240
middlewares: [OAuth2AuthorizationMiddleware, OAuth2CallbackMiddleware]
211241
})

server/controllers/OAuth2CallbackController.ts

Lines changed: 225 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,30 @@
2525
*
2626
*/
2727

28-
import { Controller, Req, Res, Get, UseBefore } from 'routing-controllers'
28+
import { Controller, Req, Res, Get, QueryParam } from 'routing-controllers'
2929
import type { Request, Response } from 'express'
30-
import { Service } from 'typedi'
31-
import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js'
30+
import { Service, Container } from 'typedi'
31+
import { OAuth2Service } from '../services/OAuth2Service.js'
32+
import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js'
33+
import type { UserInfo } from '../types/oauth2.js'
3234

3335
/**
34-
* OAuth2 Callback Controller
36+
* OAuth2 Callback Controller (Multi-Provider)
3537
*
36-
* Handles the OAuth2/OIDC callback from the identity provider.
38+
* Handles the OAuth2/OIDC callback from any configured identity provider.
3739
* This controller receives the authorization code and state parameter
3840
* after the user authenticates with the OIDC provider.
3941
*
40-
* The OAuth2CallbackMiddleware handles:
42+
* This controller handles:
4143
* - State validation (CSRF protection)
4244
* - Authorization code exchange for tokens
4345
* - User info retrieval
4446
* - Session storage
4547
* - Redirect to original page
4648
*
49+
* Supports both multi-provider mode (retrieves provider from session) and
50+
* legacy single-provider mode (uses existing OAuth2Service).
51+
*
4752
* Endpoint: GET /oauth2/callback
4853
*
4954
* Query Parameters (from OIDC provider):
@@ -52,21 +57,23 @@ import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js
5257
* - error (optional): Error code if authentication failed
5358
* - error_description (optional): Human-readable error description
5459
*
55-
* Flow:
60+
* Multi-Provider Flow:
5661
* OIDC Provider → /oauth2/callback?code=XXX&state=YYY
57-
* → OAuth2CallbackMiddleware → Original Page (with authenticated session)
62+
* → Retrieve provider from session → Use correct OAuth2 client
63+
* → Exchange code for tokens → Original Page (with authenticated session)
5864
*
5965
* Success Flow:
6066
* 1. Validate state parameter
61-
* 2. Exchange authorization code for tokens (access, refresh, ID)
62-
* 3. Fetch user information from UserInfo endpoint
63-
* 4. Store tokens and user data in session
64-
* 5. Redirect to original page or home
67+
* 2. Retrieve provider from session (or use legacy service)
68+
* 3. Exchange authorization code for tokens (access, refresh, ID)
69+
* 4. Fetch user information from UserInfo endpoint
70+
* 5. Store tokens, provider name, and user data in session
71+
* 6. Redirect to original page or home
6572
*
6673
* Error Flow:
6774
* 1. Parse error from query parameters
68-
* 2. Display user-friendly error page
69-
* 3. Allow user to retry authentication
75+
* 2. Log error details
76+
* 3. Redirect to home with error parameter
7077
*
7178
* @example
7279
* // Successful callback URL from OIDC provider
@@ -77,22 +84,216 @@ import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js
7784
*/
7885
@Service()
7986
@Controller()
80-
@UseBefore(OAuth2CallbackMiddleware)
8187
export class OAuth2CallbackController {
88+
private providerManager: OAuth2ProviderManager
89+
private legacyOAuth2Service: OAuth2Service
90+
91+
constructor() {
92+
this.providerManager = Container.get(OAuth2ProviderManager)
93+
this.legacyOAuth2Service = Container.get(OAuth2Service)
94+
}
95+
8296
/**
8397
* Handle OAuth2/OIDC callback
8498
*
85-
* The actual logic is handled by OAuth2CallbackMiddleware.
86-
* This method exists only as the routing endpoint definition.
99+
* Processes the callback from any configured OIDC provider.
100+
* Supports both multi-provider mode and legacy single-provider mode.
87101
*
88-
* @param {Request} request - Express request object with query params (code, state)
89-
* @param {Response} response - Express response object (redirected by middleware)
90-
* @returns {Response} Response object (handled by middleware)
102+
* @param code - Authorization code from OIDC provider
103+
* @param state - State parameter for CSRF validation
104+
* @param error - Error code if authentication failed
105+
* @param errorDescription - Human-readable error description
106+
* @param request - Express request object
107+
* @param response - Express response object
108+
* @returns Response with redirect to original page or error page
91109
*/
92110
@Get('/oauth2/callback')
93-
callback(@Req() request: Request, @Res() response: Response): Response {
94-
// The middleware handles all the logic and redirects the user
95-
// This method should never actually execute
96-
return response
111+
async callback(
112+
@QueryParam('code') code: string,
113+
@QueryParam('state') state: string,
114+
@QueryParam('error') error: string,
115+
@QueryParam('error_description') errorDescription: string,
116+
@Req() request: Request,
117+
@Res() response: Response
118+
): Promise<Response> {
119+
console.log('OAuth2CallbackController: Processing OAuth2 callback')
120+
121+
const session = request.session as any
122+
123+
// Handle error from provider
124+
if (error) {
125+
console.error(`OAuth2CallbackController: Error from provider: ${error}`)
126+
console.error(`OAuth2CallbackController: Description: ${errorDescription || 'N/A'}`)
127+
return response.redirect(`/?oauth2_error=${encodeURIComponent(error)}`)
128+
}
129+
130+
// Validate required parameters
131+
if (!code) {
132+
console.error('OAuth2CallbackController: Missing authorization code')
133+
return response.redirect('/?oauth2_error=missing_code')
134+
}
135+
136+
if (!state) {
137+
console.error('OAuth2CallbackController: Missing state parameter')
138+
return response.redirect('/?oauth2_error=missing_state')
139+
}
140+
141+
// Validate state (CSRF protection)
142+
const storedState = session.oauth2_state
143+
if (!storedState || storedState !== state) {
144+
console.error('OAuth2CallbackController: State mismatch (CSRF protection)')
145+
console.error(` Expected: ${storedState}`)
146+
console.error(` Received: ${state}`)
147+
return response.redirect('/?oauth2_error=invalid_state')
148+
}
149+
150+
// Get code verifier from session (PKCE)
151+
const codeVerifier = session.oauth2_code_verifier
152+
if (!codeVerifier) {
153+
console.error('OAuth2CallbackController: Code verifier not found in session')
154+
return response.redirect('/?oauth2_error=missing_verifier')
155+
}
156+
157+
// Check if multi-provider mode (provider stored in session)
158+
const provider = session.oauth2_provider
159+
160+
try {
161+
if (provider) {
162+
// Multi-provider mode
163+
await this.handleMultiProviderCallback(session, code, codeVerifier, provider)
164+
} else {
165+
// Legacy single-provider mode
166+
await this.handleLegacyCallback(session, code, codeVerifier)
167+
}
168+
169+
// Clean up temporary session data
170+
delete session.oauth2_code_verifier
171+
delete session.oauth2_state
172+
173+
// Redirect to original page
174+
const redirectUrl = session.oauth2_redirect_page || '/'
175+
delete session.oauth2_redirect_page
176+
177+
console.log(
178+
`OAuth2CallbackController: Authentication successful, redirecting to: ${redirectUrl}`
179+
)
180+
return response.redirect(redirectUrl)
181+
} catch (error) {
182+
console.error('OAuth2CallbackController: Token exchange failed:', error)
183+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
184+
return response.redirect(
185+
`/?oauth2_error=token_exchange_failed&details=${encodeURIComponent(errorMessage)}`
186+
)
187+
}
188+
}
189+
190+
/**
191+
* Handle multi-provider callback
192+
*/
193+
private async handleMultiProviderCallback(
194+
session: any,
195+
code: string,
196+
codeVerifier: string,
197+
provider: string
198+
): Promise<void> {
199+
console.log(`OAuth2CallbackController: Multi-provider mode - ${provider}`)
200+
201+
const client = this.providerManager.getProvider(provider)
202+
if (!client) {
203+
throw new Error(`Provider not found: ${provider}`)
204+
}
205+
206+
// Exchange code for tokens
207+
console.log(`OAuth2CallbackController: Exchanging authorization code for tokens`)
208+
const tokens = await client.validateAuthorizationCode(code, codeVerifier)
209+
210+
// Store tokens in session
211+
session.oauth2_access_token = tokens.accessToken
212+
session.oauth2_refresh_token = tokens.refreshToken
213+
session.oauth2_id_token = tokens.idToken
214+
session.oauth2_provider = provider
215+
216+
console.log(`OAuth2CallbackController: Tokens received and stored`)
217+
218+
// Fetch user info
219+
console.log(`OAuth2CallbackController: Fetching user info`)
220+
const userInfo = await this.fetchUserInfo(client, tokens.accessToken)
221+
222+
// Store user in session
223+
session.user = {
224+
username: userInfo.preferred_username || userInfo.email || userInfo.sub,
225+
email: userInfo.email,
226+
name: userInfo.name,
227+
provider: provider,
228+
sub: userInfo.sub
229+
}
230+
231+
console.log(
232+
`OAuth2CallbackController: User authenticated via ${provider}: ${session.user.username}`
233+
)
234+
}
235+
236+
/**
237+
* Handle legacy single-provider callback
238+
*/
239+
private async handleLegacyCallback(
240+
session: any,
241+
code: string,
242+
codeVerifier: string
243+
): Promise<void> {
244+
console.log('OAuth2CallbackController: Legacy single-provider mode')
245+
246+
// Exchange code for tokens using legacy service
247+
console.log(`OAuth2CallbackController: Exchanging authorization code for tokens`)
248+
const tokens = await this.legacyOAuth2Service.exchangeCodeForTokens(code, codeVerifier)
249+
250+
// Store tokens in session
251+
session.oauth2_access_token = tokens.accessToken
252+
session.oauth2_refresh_token = tokens.refreshToken
253+
session.oauth2_id_token = tokens.idToken
254+
255+
console.log(`OAuth2CallbackController: Tokens received and stored`)
256+
257+
// Fetch user info
258+
console.log(`OAuth2CallbackController: Fetching user info`)
259+
const userInfo = await this.legacyOAuth2Service.getUserInfo(tokens.accessToken)
260+
261+
// Store user in session
262+
session.user = {
263+
username: userInfo.preferred_username || userInfo.email || userInfo.sub,
264+
email: userInfo.email,
265+
name: userInfo.name,
266+
sub: userInfo.sub
267+
}
268+
269+
console.log(`OAuth2CallbackController: User authenticated (legacy): ${session.user.username}`)
270+
}
271+
272+
/**
273+
* Fetch user info from UserInfo endpoint
274+
*/
275+
private async fetchUserInfo(client: any, accessToken: string): Promise<UserInfo> {
276+
const userInfoEndpoint = client.getUserInfoEndpoint()
277+
278+
console.log(`OAuth2CallbackController: Calling UserInfo endpoint: ${userInfoEndpoint}`)
279+
280+
const response = await fetch(userInfoEndpoint, {
281+
headers: {
282+
Authorization: `Bearer ${accessToken}`,
283+
Accept: 'application/json'
284+
}
285+
})
286+
287+
if (!response.ok) {
288+
const errorText = await response.text()
289+
throw new Error(
290+
`UserInfo request failed: ${response.status} ${response.statusText} - ${errorText}`
291+
)
292+
}
293+
294+
const userInfo = await response.json()
295+
console.log(`OAuth2CallbackController: UserInfo retrieved: ${userInfo.sub}`)
296+
297+
return userInfo as UserInfo
97298
}
98299
}

0 commit comments

Comments
 (0)