@@ -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