Skip to content

fix(dashboard): RP-initiated logout fallback for non-compliant IdPs#526

Merged
DorianZheng merged 1 commit into
mainfrom
fix/dashboard-oidc-logout-fallback
May 14, 2026
Merged

fix(dashboard): RP-initiated logout fallback for non-compliant IdPs#526
DorianZheng merged 1 commit into
mainfrom
fix/dashboard-oidc-logout-fallback

Conversation

@DorianZheng

Copy link
Copy Markdown
Member

Summary

The dashboard's logout flow inherits Daytona upstream's signoutRedirect() call (react-oidc-context / oidc-client-ts), which navigates the browser to the IdP's end_session_endpoint from OIDC discovery. Auth0 advertises that endpoint, so upstream Just Works.

BoxLite's default IdP is Dex, which does not advertise end_session_endpoint (dexidp/dex#1697, open since 2019). oidc-client-ts throws Error("No end session endpoint") and the dashboard gets stuck on its Authentication Error / Go Back dialog at https://<stack>/?code=…&state=…. The "Go Back" button re-calls the same failing signoutRedirect() — the dialog is sticky.

Root cause

Two distinct bugs, one tenant-config issue:

  1. Dex's discovery doc is non-compliant with OIDC RP-Initiated Logout 1.0.
  2. The dashboard's error-recovery handler re-triggered the same failing call.
  3. Auth0 tenants created before 14 Nov 2023 default the "RP-Initiated Logout End Session Endpoint Discovery" toggle to OFF — same symptom, different cause.

Fix

Three-layer fix that's zero-config for compliant IdPs and self-healing for the rest.

API

  • New LogoutController at /api/auth/end-session — implements OIDC RP-Initiated Logout 1.0. Open-redirect prevention is strict origin-match against DASHBOARD_URL + an operator-set OIDC_POST_LOGOUT_REDIRECT_ALLOWLIST. State param preserved across the redirect.
  • New OidcMetadataService — singleton that probes the IdP's discovery doc once at startup, caches the result, and exposes getEndSessionState(): 'present' | 'absent' | 'unknown'. Replaces the inline fetch previously embedded in auth.module.ts. Fails closed on probe error so a transient blip doesn't override Auth0's real endpoint.
  • ConfigurationDto.oidc.endSessionEndpoint is only emitted when the probe returns 'absent'. Validates the env-set URL before exposing it (typos don't propagate as metadataSeed values).

Dashboard

  • ConfigProvider.tsx conditionally passes metadataSeed: { end_session_endpoint } to AuthProvider. oidc-client-ts.js:827 merges this LAST, after discovery — so the fallback wins iff the field is set.
  • App.tsx auth-error dialog's "Go Back" handler now calls removeUser() and navigates to / instead of re-calling signoutRedirect(). Breaks the sticky-error loop.

Infra

  • sst.config.ts wires OIDC_END_SESSION_ENDPOINT=https://<stackDomain>/api/auth/end-session unconditionally. Safe because the API gates exposure on the discovery probe — Auth0/Okta stacks see no behaviour change.
  • apps/infra/README.md documents the Auth0 toggle for pre-Nov-2023 tenants + a troubleshooting entry for the failure mode.

Test plan

  • Dex/self-hosted: clicking "Sign out" routes through /api/auth/end-session → 302 to dashboard, session cleared.
  • Auth0 (tenant dev-j60pjpmu6neaeaga.us.auth0.com, toggle enabled): curl /api/config | jq .oidc.endSessionEndpoint returns null. Sign out navigates to /oidc/logout on Auth0 directly, terminates the session cookie, no silent re-auth.
  • Auth0 (toggle disabled, pre-fix): reproduces the original "page-refresh loops" symptom. Confirms the toggle is the root cause for Auth0 stacks.
  • Sticky-error dialog: simulating an auth error with stale state, "Go Back" now lands on / cleanly instead of re-triggering the same error.
  • Negative test: curl -i '/api/auth/end-session?post_logout_redirect_uri=https://evil.example.com' → 400.

Known limitations / follow-ups

  • apps/libs/api-client/src/models/oidc-config.ts is hand-patched to add the optional endSessionEndpoint field. The next OpenAPI codegen pass will regenerate it from the API schema; the hand edit keeps types and runtime in sync until then. Follow-up: run nx run api-client:generate:api-client.
  • Stricter security posture would also POST to the IdP's /token/revoke to invalidate the refresh token. Skipped in this PR — the security delta is small once removeUser() clears local storage and the IdP terminates its own session. Tracked for a follow-up.

The dashboard's logout flow inherits Daytona upstream's call to
`signoutRedirect()` (react-oidc-context / oidc-client-ts), which navigates
the browser to the IdP's `end_session_endpoint` from the OIDC discovery
document. Auth0 advertises this endpoint, so the upstream flow Just Works.

BoxLite ships Dex as the default IdP. Dex does not advertise
`end_session_endpoint` (dexidp/dex#1697, open since 2019), so
oidc-client-ts throws `Error("No end session endpoint")` and the dashboard
gets stuck on its "Authentication Error / Go Back" dialog at
`https://<stack>/?code=…&state=…`.

Fix in three layers:

1. API hosts an OIDC-compatible logout endpoint at `/api/auth/end-session`.
   Open-redirect prevention is a strict origin-match against `DASHBOARD_URL`
   plus an operator-set `OIDC_POST_LOGOUT_REDIRECT_ALLOWLIST`. State param
   preserved. New `apps/api/src/auth/logout.controller.ts`.

2. API decides at runtime whether to expose the fallback. New
   `OidcMetadataService` probes the IdP's discovery doc once at startup
   (replaces the inline fetch in `auth.module.ts`) and exposes a tri-state
   `getEndSessionState(): 'present' | 'absent' | 'unknown'`. The
   `ConfigurationDto` only emits `endSessionEndpoint` when the IdP
   definitively lacks one (`'absent'`); on probe failure ('unknown') it
   stays undefined — fail-closed prevents an Auth0/Okta stack from
   silently routing logout through BoxLite if discovery has a transient
   blip. `oidc-client-ts.js:827` applies `metadataSeed` LAST, so this
   would otherwise override Auth0's real endpoint.

3. Dashboard `ConfigProvider.tsx` passes `metadataSeed: { end_session_endpoint }`
   to AuthProvider only when the API reports the field. `App.tsx`
   auth-error dialog's "Go Back" handler now calls `removeUser()` and
   navigates to `/` instead of re-calling the same `signoutRedirect()`
   that just failed — breaks the sticky-error loop.

SST wires `OIDC_END_SESSION_ENDPOINT` unconditionally to
`https://<stackDomain>/api/auth/end-session`. Safe because the API gates
its exposure on the discovery probe; Auth0/Okta stacks see no behaviour
change. README documents the Auth0 "RP-Initiated Logout End Session
Endpoint Discovery" toggle that pre-Nov-2023 tenants need to flip.

Note: `apps/libs/api-client/src/models/oidc-config.ts` is hand-patched
to add the optional `endSessionEndpoint` field. The next OpenAPI
codegen pass will regenerate it from the API schema; the hand edit
keeps types and runtime in sync until then.
@DorianZheng DorianZheng merged commit 6acdb2b into main May 14, 2026
18 checks passed
@DorianZheng DorianZheng deleted the fix/dashboard-oidc-logout-fallback branch May 14, 2026 13:41
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.

1 participant