Skip to content

fix(oidc): retry provider init and decouple status from provider state#219

Merged
s0up4200 merged 6 commits into
developfrom
fix/oidc-retry-init
Apr 11, 2026
Merged

fix(oidc): retry provider init and decouple status from provider state#219
s0up4200 merged 6 commits into
developfrom
fix/oidc-retry-init

Conversation

@s0up4200
Copy link
Copy Markdown
Collaborator

@s0up4200 s0up4200 commented Apr 5, 2026

OIDC silently disables itself when the container restarts while the identity provider is temporarily unreachable — NewOIDC fails once and the server continues with h.oidc = nil, causing /api/auth/status to report oidcEnabled: false permanently until manual restart. This adds a retry loop (10 attempts, 5s intervals) to OIDC provider initialization and decouples the oidcEnabled response from runtime provider state by tracking whether OIDC was configured, matching autobrr's approach.

OIDC routes are now registered whenever an issuer is configured rather than only when the provider initialized successfully, with nil guards in the handlers returning a graceful oidc_unavailable redirect if the provider isn't ready.

Closes #214

Summary by CodeRabbit

  • New Features

    • Added retry/backoff for OIDC provider initialization to improve resilience.
  • Bug Fixes

    • OIDC endpoints and status now reflect issuer configuration; API reports separate oidcConfigured and oidcReady flags.
    • Improved OIDC-unavailable handling with early redirects, consistent redirect URLs, and clearer logging on initialization failures.
  • Tests

    • Added tests for OIDC readiness reporting and redirect behavior under subpath base URLs.

#214)

OIDC silently disabled after container restarts when the identity
provider was temporarily unreachable. Add retry loop (10 attempts,
5s intervals) and report oidcEnabled based on config rather than
runtime provider pointer, matching autobrr's approach.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added retry/backoff to OIDC initialization; introduced oidcConfigured on AuthHandler and updated NewAuthHandler signature; OIDC login/callback handlers now guard for missing provider and redirect to <base>/login?error=oidc_unavailable; route registration and frontend API/UI updated to report oidcConfigured/oidcReady; tests added.

Changes

Cohort / File(s) Summary
OIDC Initialization Resilience
internal/auth/oidc.go
Added retry/backoff loop for OIDC discovery and provider initialization (max attempts/delay); logs warnings per failed attempt and returns a formatted error after exhausting attempts; success path unchanged.
Auth Handler State & Routes
internal/server/auth.go, internal/server/server.go
Added oidcConfigured bool field and updated NewAuthHandler signature to accept it; CheckRegistrationStatus now reports oidcConfigured and oidcReady; NewServer passes cfg.OIDC.Issuer != "" and registers OIDC routes based on s.auth.oidcConfigured.
OIDC Handlers & Redirects
internal/server/auth_oidc.go
Removed previous InitOIDCRoutes wiring; added loginErrorRedirectURL; handleOIDCLogin/handleOIDCCallback compute base_url, early-guard when h.oidc == nil (log + redirect oidc_unavailable), and use helper for consistent error redirects.
Tests & Test Infrastructure
internal/server/auth_test.go
Added stubDB test scaffolding and new tests asserting registration status reports oidcConfigured/oidcReady and that login/callback redirect to <base>/login?error=oidc_unavailable under a subpath.
Frontend API & UI
web/src/api/auth.ts, web/src/components/auth/Login.tsx, web/src/context/auth.tsx, web/src/routes.tsx
Replaced oidcEnabled with oidcConfigured and oidcReady in API typings and mapping; UI logic and AuthContext updated to use the two-field model; Login UI shows OIDC button only when oidcReady and shows an informational banner when oidcConfigured but not oidcReady.

Sequence Diagram(s)

sequenceDiagram
    participant Init as NewOIDC()
    participant Disc as Discovery (.well-known)
    participant ProviderLib as oidc.NewProvider
    participant Logger as Logger
    participant Backoff as Backoff/Sleep

    Init->>Disc: attempt getProviderEndpoints()
    alt discovery success
        Disc-->>Init: endpoints
        Init->>ProviderLib: oidc.NewProvider(endpoints)
        alt provider init success
            ProviderLib-->>Init: provider
            Init->>Logger: configure verifier & scopes
            Init-->>Init: return configured OIDC
        else provider init fails
            ProviderLib-->>Init: error
            Init->>Logger: warn attempt N failed
            Init->>Backoff: sleep(retryDelay)
            Init->>Init: retry (until maxAttempts)
        end
    else discovery fails
        Disc-->>Init: error
        Init->>Logger: warn attempt N failed
        Init->>Backoff: sleep(retryDelay)
        Init->>Init: retry (until maxAttempts)
    end
    alt all attempts exhausted
        Init->>Logger: error final failure
        Init-->>Init: return error (attempts, last error)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐇 I hop and retry through issuer mist,
I nudge, I wait, I don't desist,
When providers nap I warn and steer,
Redirecting gently, calming fear,
A hopeful hop brings login near.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: retrying OIDC provider initialization and decoupling configuration status from runtime state.
Description check ✅ Passed The description adequately covers the problem, solution approach, and references the closed issue #214. All template sections are present with meaningful content.
Linked Issues check ✅ Passed The PR successfully addresses #214's requirements: implements retry logic for OIDC initialization, decouples status reporting from provider state, gracefully handles unavailable providers, and ensures OIDC routes are registered when configured.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing issue #214: adding retry/backoff logic, decoupling configuration state, adding nil guards, and updating frontend to reflect the new status fields.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/oidc-retry-init

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/server/auth_oidc.go (1)

28-42: ⚠️ Potential issue | 🟡 Minor

Inconsistent redirect URL construction for error cases.

The nil guard redirects correctly use baseURL+"login?error=...", but the PKCE and state generation error redirects use hardcoded "/?error=...". This would break navigation if the application is deployed with a non-root base URL.

🐛 Proposed fix for consistent redirect URLs
 	// Generate state parameter
 	state, err := utils.GenerateSecureToken(32)
 	if err != nil {
 		log.Error().Err(err).Msg("Failed to generate state parameter")
-		c.Redirect(http.StatusTemporaryRedirect, "/?error=state_generation_failed")
+		c.Redirect(http.StatusTemporaryRedirect, baseURL+"login?error=state_generation_failed")
 		return
 	}

 	// Generate PKCE parameters
 	pkceParams, err := auth.GeneratePKCEParams()
 	if err != nil {
 		log.Error().Err(err).Msg("Failed to generate PKCE parameters")
-		c.Redirect(http.StatusTemporaryRedirect, "/?error=pkce_generation_failed")
+		c.Redirect(http.StatusTemporaryRedirect, baseURL+"login?error=pkce_generation_failed")
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/auth_oidc.go` around lines 28 - 42, The state and PKCE error
handlers use hardcoded c.Redirect(..., "/?error=...") which is inconsistent with
other nil-guards that use baseURL+"login?error=..."; update the error redirects
in the utils.GenerateSecureToken and auth.GeneratePKCEParams failure branches to
build the redirect using the same baseURL+"login?error=..." pattern (preserving
the existing error query values) so navigation works when the app is hosted on a
non-root base URL.
🧹 Nitpick comments (1)
internal/auth/oidc.go (1)

157-179: Consider respecting context cancellation during retry delays.

The retry loop uses time.Sleep which doesn't respect context cancellation. If the application is shutting down or the context is cancelled during initialization, the loop will continue sleeping until completion.

♻️ Proposed fix to use context-aware sleep
 	for attempt := 1; attempt <= oidcInitMaxAttempts; attempt++ {
 		endpoints, _, lastErr = getProviderEndpoints(ctx, http.DefaultClient, cfg.Issuer)
 		if lastErr != nil {
 			log.Warn().Err(lastErr).Int("attempt", attempt).Int("max", oidcInitMaxAttempts).
 				Str("issuer", cfg.Issuer).Msg("Failed to discover OIDC provider endpoints")
 			if attempt < oidcInitMaxAttempts {
-				time.Sleep(oidcInitRetryDelay)
+				select {
+				case <-ctx.Done():
+					return nil, fmt.Errorf("OIDC initialization cancelled: %w", ctx.Err())
+				case <-time.After(oidcInitRetryDelay):
+				}
 			}
 			continue
 		}

 		provider, lastErr = oidc.NewProvider(ctx, cfg.Issuer)
 		if lastErr != nil {
 			log.Warn().Err(lastErr).Int("attempt", attempt).Int("max", oidcInitMaxAttempts).
 				Str("issuer", cfg.Issuer).Msg("Failed to initialize OIDC provider")
 			if attempt < oidcInitMaxAttempts {
-				time.Sleep(oidcInitRetryDelay)
+				select {
+				case <-ctx.Done():
+					return nil, fmt.Errorf("OIDC initialization cancelled: %w", ctx.Err())
+				case <-time.After(oidcInitRetryDelay):
+				}
 			}
 			continue
 		}

 		break
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/oidc.go` around lines 157 - 179, The retry loop in the oidc
initialization (loop using oidcInitMaxAttempts and calling getProviderEndpoints
and oidc.NewProvider) currently uses time.Sleep(oidcInitRetryDelay), which
ignores context cancellation; change the retry delay to be context-aware by
replacing time.Sleep with a select that waits on either ctx.Done() or a
time.After(oidcInitRetryDelay) before retrying, and if ctx is cancelled return
the last error immediately instead of continuing to the next attempt; ensure
this behavior is applied in both retry branches where
time.Sleep(oidcInitRetryDelay) is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@internal/server/auth_oidc.go`:
- Around line 28-42: The state and PKCE error handlers use hardcoded
c.Redirect(..., "/?error=...") which is inconsistent with other nil-guards that
use baseURL+"login?error=..."; update the error redirects in the
utils.GenerateSecureToken and auth.GeneratePKCEParams failure branches to build
the redirect using the same baseURL+"login?error=..." pattern (preserving the
existing error query values) so navigation works when the app is hosted on a
non-root base URL.

---

Nitpick comments:
In `@internal/auth/oidc.go`:
- Around line 157-179: The retry loop in the oidc initialization (loop using
oidcInitMaxAttempts and calling getProviderEndpoints and oidc.NewProvider)
currently uses time.Sleep(oidcInitRetryDelay), which ignores context
cancellation; change the retry delay to be context-aware by replacing time.Sleep
with a select that waits on either ctx.Done() or a
time.After(oidcInitRetryDelay) before retrying, and if ctx is cancelled return
the last error immediately instead of continuing to the next attempt; ensure
this behavior is applied in both retry branches where
time.Sleep(oidcInitRetryDelay) is used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0143c26-afff-4bd9-b9d4-6151cab64ce5

📥 Commits

Reviewing files that changed from the base of the PR and between bf756a6 and 93d6c41.

📒 Files selected for processing (4)
  • internal/auth/oidc.go
  • internal/server/auth.go
  • internal/server/auth_oidc.go
  • internal/server/server.go

s0up4200 added 2 commits April 6, 2026 00:55
Pre-existing hardcoded root redirects broke non-root base URL
deployments.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/server/auth_oidc.go (1)

18-25: Remove dead code: no-op conditional.

The conditional on lines 20-22 does nothing—assigning baseURL = "" when it's already empty is a no-op. Either remove it or, if a fallback was intended, implement it properly.

🧹 Proposed fix
 func loginErrorRedirectURL(baseURL, errorCode string) string {
 	baseURL = strings.TrimRight(baseURL, "/")
-	if baseURL == "" {
-		baseURL = ""
-	}
 
 	return baseURL + "/login?error=" + url.QueryEscape(errorCode)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/auth_oidc.go` around lines 18 - 25, The conditional in
loginErrorRedirectURL that sets baseURL = "" when baseURL is already empty is
dead code; remove the no-op if-block or replace it with a proper fallback
behavior. Locate the function loginErrorRedirectURL and either delete the lines
checking if baseURL == "" / assigning baseURL = "" so it simply trims and
returns baseURL + "/login?error="+url.QueryEscape(errorCode), or, if a fallback
host was intended, implement that fallback (e.g., set baseURL to a specific
default value) and use that variable in the return.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/server/auth_test.go`:
- Around line 105-146: Change the CheckRegistrationStatus response to use the
AuthHandler.oidcConfigured field rather than testing h.oidc != nil: return
h.oidcConfigured for the oidcEnabled JSON field inside CheckRegistrationStatus.
Then update the test TestCheckRegistrationStatusReportsOIDCProviderReadiness to
drive and assert on that configuration flag (pass the intended oidcConfigured
boolean into NewAuthHandler for each case and set expectedOID to that value)
instead of relying on tt.oidc presence.

---

Nitpick comments:
In `@internal/server/auth_oidc.go`:
- Around line 18-25: The conditional in loginErrorRedirectURL that sets baseURL
= "" when baseURL is already empty is dead code; remove the no-op if-block or
replace it with a proper fallback behavior. Locate the function
loginErrorRedirectURL and either delete the lines checking if baseURL == "" /
assigning baseURL = "" so it simply trims and returns baseURL +
"/login?error="+url.QueryEscape(errorCode), or, if a fallback host was intended,
implement that fallback (e.g., set baseURL to a specific default value) and use
that variable in the return.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ad8e3ad-e31c-4515-bcf5-fa0a35c2ae66

📥 Commits

Reviewing files that changed from the base of the PR and between f31f463 and 6b77294.

📒 Files selected for processing (3)
  • internal/server/auth.go
  • internal/server/auth_oidc.go
  • internal/server/auth_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/server/auth.go

Comment thread internal/server/auth_test.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/server/auth_oidc.go (1)

23-27: Optional: extract shared base URL normalization into a tiny helper.

The duplicated base_url fallback logic is small but repeated; extracting it would make future adjustments safer.

Also applies to: 65-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/auth_oidc.go` around lines 23 - 27, Extract the repeated
base_url normalization into a small helper method on AuthHandler (e.g.,
BaseURL(c *gin.Context) string) and replace the duplicated logic in
handleOIDCLogin and the corresponding callback handler (the other block at lines
~65-69) to call this helper; the helper should read c.GetString("base_url") and
return "/" when empty so all callers use a single source of truth for base URL
fallback.
internal/server/auth_test.go (1)

157-187: Consider merging these two redirect tests into one table-driven test.

Both cases share setup/assertions and differ only by route+handler, so a single table would reduce duplication and future drift.

As per coding guidelines, "**/*_test.go: Prefer table-driven tests for new backend logic."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/auth_test.go` around lines 157 - 187, Merge the two nearly
identical tests TestHandleOIDCLoginRedirectUsesLoginPathUnderSubpathBaseURL and
TestHandleOIDCCallbackRedirectUsesLoginPathUnderSubpathBaseURL into a single
table-driven test that iterates over the two routes/handlers; create a table
with entries for request path ("/api/auth/oidc/login" and
"/api/auth/oidc/callback") and a function pointer or identifier for the handler
method (handler.handleOIDCLogin and handler.handleOIDCCallback), perform the
shared setup (gin.TestMode, NewAuthHandler, recorder, context, set "base_url"),
invoke the handler from the table entry, and assert the same redirect status and
Location header for each case to remove duplication and follow the project test
style.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/server/auth_oidc.go`:
- Around line 23-27: Extract the repeated base_url normalization into a small
helper method on AuthHandler (e.g., BaseURL(c *gin.Context) string) and replace
the duplicated logic in handleOIDCLogin and the corresponding callback handler
(the other block at lines ~65-69) to call this helper; the helper should read
c.GetString("base_url") and return "/" when empty so all callers use a single
source of truth for base URL fallback.

In `@internal/server/auth_test.go`:
- Around line 157-187: Merge the two nearly identical tests
TestHandleOIDCLoginRedirectUsesLoginPathUnderSubpathBaseURL and
TestHandleOIDCCallbackRedirectUsesLoginPathUnderSubpathBaseURL into a single
table-driven test that iterates over the two routes/handlers; create a table
with entries for request path ("/api/auth/oidc/login" and
"/api/auth/oidc/callback") and a function pointer or identifier for the handler
method (handler.handleOIDCLogin and handler.handleOIDCCallback), perform the
shared setup (gin.TestMode, NewAuthHandler, recorder, context, set "base_url"),
invoke the handler from the table entry, and assert the same redirect status and
Location header for each case to remove duplication and follow the project test
style.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64d068a9-9fd1-4625-a1d2-b687a3a03fae

📥 Commits

Reviewing files that changed from the base of the PR and between 6b77294 and 5fd9762.

📒 Files selected for processing (3)
  • internal/server/auth.go
  • internal/server/auth_oidc.go
  • internal/server/auth_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/server/auth.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
web/src/api/auth.ts (1)

148-153: Remove the unused registrationEnabled field from the RegistrationStatus interface.

The server endpoint does not return registrationEnabled, so data.registrationEnabled ?? false always evaluates to false. Additionally, no UI component actually reads this field—all consumers only access hasUsers, oidcConfigured, and oidcReady. Removing this field will eliminate dead code and reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/api/auth.ts` around lines 148 - 153, Remove the unused
registrationEnabled field from the RegistrationStatus type and from the object
returned by the function in web/src/api/auth.ts: delete the registrationEnabled
property from the RegistrationStatus interface (or type) and stop mapping
data.registrationEnabled in the return value (leave hasUsers, oidcConfigured,
oidcReady only). Also search for any direct references to
RegistrationStatus.registrationEnabled and remove or update them so types and
callers compile cleanly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/api/auth.ts`:
- Around line 148-153: Remove the unused registrationEnabled field from the
RegistrationStatus type and from the object returned by the function in
web/src/api/auth.ts: delete the registrationEnabled property from the
RegistrationStatus interface (or type) and stop mapping data.registrationEnabled
in the return value (leave hasUsers, oidcConfigured, oidcReady only). Also
search for any direct references to RegistrationStatus.registrationEnabled and
remove or update them so types and callers compile cleanly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9523a9a3-d795-4683-a090-6e7f43e906f4

📥 Commits

Reviewing files that changed from the base of the PR and between 5fd9762 and a883d6e.

📒 Files selected for processing (6)
  • internal/server/auth.go
  • internal/server/auth_test.go
  • web/src/api/auth.ts
  • web/src/components/auth/Login.tsx
  • web/src/context/auth.tsx
  • web/src/routes.tsx
✅ Files skipped from review due to trivial changes (1)
  • internal/server/auth_test.go

@s0up4200 s0up4200 merged commit 6b7f0f4 into develop Apr 11, 2026
11 checks passed
@s0up4200 s0up4200 deleted the fix/oidc-retry-init branch April 11, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OIDC breaks after a period of time

1 participant