Skip to content

Commit cd9ba26

Browse files
committed
OIDC checking / retrying
1 parent ca923f7 commit cd9ba26

File tree

4 files changed

+312
-13
lines changed

4 files changed

+312
-13
lines changed

server/app.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,21 +147,36 @@ let instance: any
147147
// Get OAuth2Service from container
148148
const oauth2Service = Container.get(OAuth2Service)
149149

150-
// Initialize OAuth2 service from OIDC discovery document (await it!)
151-
try {
152-
await oauth2Service.initializeFromWellKnown(wellKnownUrl)
150+
// Initialize OAuth2 service with retry logic
151+
const isProduction = process.env.NODE_ENV === 'production'
152+
const maxRetries = Infinity // Retry indefinitely
153+
const initialDelay = 1000 // 1 second, then exponential backoff
154+
155+
console.log(
156+
'Attempting OAuth2 initialization (will retry indefinitely with exponential backoff)...'
157+
)
158+
const success = await oauth2Service.initializeWithRetry(wellKnownUrl, maxRetries, initialDelay)
159+
160+
if (success) {
153161
console.log('OAuth2Service: Initialization successful')
154162
console.log(' Client ID:', process.env.VITE_OBP_OAUTH2_CLIENT_ID || 'NOT SET')
155163
console.log(' Redirect URI:', process.env.VITE_OBP_OAUTH2_REDIRECT_URL || 'NOT SET')
156164
console.log('OAuth2/OIDC ready for authentication')
157-
} catch (error: any) {
158-
console.error('OAuth2Service: Initialization failed:', error.message)
159-
console.error('OAuth2/OIDC authentication will not be available')
160-
console.error('Please check:')
161-
console.error(' 1. OBP-OIDC server is running')
162-
console.error(' 2. VITE_OBP_OAUTH2_WELL_KNOWN_URL is correct')
163-
console.error(' 3. Network connectivity to OIDC provider')
164-
console.warn('Server will start but OAuth2 authentication will fail.')
165+
} else {
166+
console.error('OAuth2Service: Initialization failed after all retries')
167+
168+
// Use graceful degradation for both development and production
169+
const envMode = isProduction ? 'Production' : 'Development'
170+
console.warn(`WARNING: ${envMode} mode: Server will start without OAuth2`)
171+
console.warn('WARNING: Login will be unavailable until OIDC server is reachable')
172+
console.warn('WARNING: Starting health check to reconnect automatically...')
173+
console.warn('Please check:')
174+
console.warn(' 1. OBP-OIDC server is running')
175+
console.warn(' 2. VITE_OBP_OAUTH2_WELL_KNOWN_URL is correct')
176+
console.warn(' 3. Network connectivity to OIDC provider')
177+
178+
// Start periodic health check to reconnect when OIDC becomes available
179+
oauth2Service.startHealthCheck(1000) // Start with 1 second, then exponential backoff
165180
}
166181
}
167182
console.log(`-----------------------------------------------------------------`)

server/controllers/StatusController.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import OBPClientService from '../services/OBPClientService.js'
3232
import { Service, Container } from 'typedi'
3333
import { OAuthConfig } from 'obp-typescript'
3434
import { commitId } from '../app.js'
35+
import { OAuth2Service } from '../services/OAuth2Service.js'
3536
import {
3637
RESOURCE_DOCS_API_VERSION,
3738
MESSAGE_DOCS_API_VERSION,
@@ -139,4 +140,75 @@ export class StatusController {
139140
return false
140141
}
141142
}
143+
144+
@Get('/oauth2')
145+
getOAuth2Status(@Res() response: Response): Response {
146+
try {
147+
const oauth2Service = Container.get(OAuth2Service)
148+
const isInitialized = oauth2Service.isInitialized()
149+
const oidcConfig = oauth2Service.getOIDCConfiguration()
150+
const healthCheckActive = oauth2Service.isHealthCheckActive()
151+
const healthCheckAttempts = oauth2Service.getHealthCheckAttempts()
152+
153+
return response.json({
154+
available: isInitialized,
155+
message: isInitialized
156+
? 'OAuth2/OIDC is ready for authentication'
157+
: 'OAuth2/OIDC is not available',
158+
issuer: oidcConfig?.issuer || null,
159+
authorizationEndpoint: oidcConfig?.authorization_endpoint || null,
160+
wellKnownUrl: process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL || null,
161+
healthCheck: {
162+
active: healthCheckActive,
163+
attempts: healthCheckAttempts
164+
}
165+
})
166+
} catch (error) {
167+
return response.status(500).json({
168+
available: false,
169+
message: 'Error checking OAuth2 status',
170+
error: error instanceof Error ? error.message : 'Unknown error'
171+
})
172+
}
173+
}
174+
175+
@Get('/oauth2/reconnect')
176+
async reconnectOAuth2(@Res() response: Response): Promise<Response> {
177+
try {
178+
const oauth2Service = Container.get(OAuth2Service)
179+
180+
if (oauth2Service.isInitialized()) {
181+
return response.json({
182+
success: true,
183+
message: 'OAuth2 is already connected',
184+
alreadyConnected: true
185+
})
186+
}
187+
188+
const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL
189+
if (!wellKnownUrl) {
190+
return response.status(400).json({
191+
success: false,
192+
message: 'VITE_OBP_OAUTH2_WELL_KNOWN_URL not configured'
193+
})
194+
}
195+
196+
console.log('Manual OAuth2 reconnection attempt triggered...')
197+
await oauth2Service.initializeFromWellKnown(wellKnownUrl)
198+
199+
console.log('Manual OAuth2 reconnection successful!')
200+
return response.json({
201+
success: true,
202+
message: 'OAuth2 reconnection successful',
203+
issuer: oauth2Service.getOIDCConfiguration()?.issuer || null
204+
})
205+
} catch (error) {
206+
console.error('Manual OAuth2 reconnection failed:', error)
207+
return response.status(500).json({
208+
success: false,
209+
message: 'OAuth2 reconnection failed',
210+
error: error instanceof Error ? error.message : 'Unknown error'
211+
})
212+
}
213+
}
142214
}

server/services/OAuth2Service.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ export class OAuth2Service {
122122
private readonly clientSecret: string
123123
private readonly redirectUri: string
124124
private initialized: boolean = false
125+
private wellKnownUrl: string = ''
126+
private healthCheckInterval: NodeJS.Timeout | null = null
127+
private healthCheckAttempts: number = 0
128+
private healthCheckActive: boolean = false
125129

126130
constructor() {
127131
// Load OAuth2 configuration from environment
@@ -161,6 +165,9 @@ export class OAuth2Service {
161165
async initializeFromWellKnown(wellKnownUrl: string): Promise<void> {
162166
console.log('OAuth2Service: Fetching OIDC configuration from:', wellKnownUrl)
163167

168+
// Store the well-known URL for potential retries
169+
this.wellKnownUrl = wellKnownUrl
170+
164171
try {
165172
const response = await fetch(wellKnownUrl)
166173

@@ -203,6 +210,151 @@ export class OAuth2Service {
203210
}
204211
}
205212

213+
/**
214+
* Start periodic health check to reconnect if OIDC server becomes available
215+
* Uses exponential backoff: 1min, 2min, 4min, capped at 4min
216+
*
217+
* @param {number} initialIntervalMs - Initial interval in milliseconds (default: 1000 = 1 second)
218+
*
219+
* @example
220+
* oauth2Service.startHealthCheck(1000) // Start checking at 1 second, then exponential backoff
221+
*/
222+
startHealthCheck(initialIntervalMs: number = 1000): void {
223+
if (this.healthCheckInterval) {
224+
console.log('OAuth2Service: Health check already running')
225+
return
226+
}
227+
228+
if (!this.wellKnownUrl) {
229+
console.warn('OAuth2Service: Cannot start health check - no well-known URL configured')
230+
return
231+
}
232+
233+
this.healthCheckAttempts = 0
234+
this.healthCheckActive = true
235+
console.log('OAuth2Service: Starting health check with exponential backoff')
236+
237+
const scheduleNextCheck = () => {
238+
if (!this.initialized && this.wellKnownUrl) {
239+
// Calculate delay with exponential backoff, capped at 4 minutes
240+
const delay = Math.min(initialIntervalMs * Math.pow(2, this.healthCheckAttempts), 240000)
241+
const delayDisplay =
242+
delay < 60000
243+
? `${(delay / 1000).toFixed(0)} second(s)`
244+
: `${(delay / 60000).toFixed(1)} minute(s)`
245+
246+
console.log(
247+
`OAuth2Service: Health check scheduled in ${delayDisplay} (attempt ${this.healthCheckAttempts + 1})`
248+
)
249+
250+
this.healthCheckInterval = setTimeout(async () => {
251+
console.log('OAuth2Service: Health check - attempting to reconnect to OIDC server...')
252+
try {
253+
await this.initializeFromWellKnown(this.wellKnownUrl)
254+
console.log('OAuth2Service: Successfully reconnected to OIDC server!')
255+
// Stop health check once reconnected
256+
this.stopHealthCheck()
257+
} catch (error) {
258+
this.healthCheckAttempts++
259+
// Schedule next check with longer interval
260+
scheduleNextCheck()
261+
}
262+
}, delay)
263+
}
264+
}
265+
266+
// Start the first check
267+
scheduleNextCheck()
268+
}
269+
270+
/**
271+
* Stop the periodic health check
272+
*/
273+
stopHealthCheck(): void {
274+
if (this.healthCheckInterval) {
275+
clearTimeout(this.healthCheckInterval)
276+
this.healthCheckInterval = null
277+
this.healthCheckAttempts = 0
278+
this.healthCheckActive = false
279+
console.log('OAuth2Service: Health check stopped')
280+
}
281+
}
282+
283+
/**
284+
* Check if health check is currently active
285+
*
286+
* @returns {boolean} True if health check is running
287+
*/
288+
isHealthCheckActive(): boolean {
289+
return this.healthCheckActive
290+
}
291+
292+
/**
293+
* Get the number of health check attempts so far
294+
*
295+
* @returns {number} Number of health check attempts
296+
*/
297+
getHealthCheckAttempts(): number {
298+
return this.healthCheckAttempts
299+
}
300+
301+
/**
302+
* Attempt to initialize with exponential backoff retry (continues indefinitely)
303+
*
304+
* @param {number} maxRetries - Maximum number of retry attempts (default: Infinity for continuous retries)
305+
* @param {string} wellKnownUrl - The .well-known/openid-configuration URL
306+
* @param {number} maxRetries - Maximum number of retry attempts (default: Infinity for continuous retries)
307+
* @param {number} initialDelayMs - Initial delay in milliseconds (default: 1000 = 1 second)
308+
* @returns {Promise<boolean>} True if initialization succeeded, false if maxRetries reached
309+
*
310+
* @example
311+
* const success = await oauth2Service.initializeWithRetry('http://localhost:9000/.well-known/openid-configuration', Infinity, 1000)
312+
*/
313+
async initializeWithRetry(
314+
wellKnownUrl: string,
315+
maxRetries: number = Infinity,
316+
initialDelayMs: number = 1000
317+
): Promise<boolean> {
318+
if (!wellKnownUrl) {
319+
console.error('OAuth2Service: Cannot retry - no well-known URL configured')
320+
return false
321+
}
322+
323+
// Store the well-known URL for retries and health checks
324+
this.wellKnownUrl = wellKnownUrl
325+
326+
let attempt = 0
327+
while (attempt < maxRetries) {
328+
try {
329+
await this.initializeFromWellKnown(wellKnownUrl)
330+
console.log(`OAuth2Service: Initialized successfully on attempt ${attempt + 1}`)
331+
return true
332+
} catch (error: any) {
333+
const delay = Math.min(initialDelayMs * Math.pow(2, attempt), 240000) // Cap at 4 minutes
334+
const delayDisplay =
335+
delay < 60000
336+
? `${(delay / 1000).toFixed(0)} second(s)`
337+
: `${(delay / 60000).toFixed(1)} minute(s)`
338+
339+
if (maxRetries === Infinity || attempt < maxRetries - 1) {
340+
console.log(
341+
`OAuth2Service: Attempt ${attempt + 1} failed. Retrying in ${delayDisplay}...`
342+
)
343+
await new Promise((resolve) => setTimeout(resolve, delay))
344+
attempt++
345+
} else {
346+
console.error(
347+
`OAuth2Service: Failed to initialize after ${maxRetries} attempts:`,
348+
error.message
349+
)
350+
return false
351+
}
352+
}
353+
}
354+
355+
return false
356+
}
357+
206358
/**
207359
* Check if the service is initialized and ready to use
208360
*

0 commit comments

Comments
 (0)