Description
I used Claude to find and track all of its fixes, but I do not want to push AI generated code into a public repo, and felt it would be better to solve the issues in the way the project owners want to.
Hell these may be by design I dont know, but I think it points to valuable details to get the self-hosting side up and running faster.
I (Claude HA) have successfully deployed Fluxer in self-hosted monolith mode (Docker Compose,
refactor branch) and configured SSO via the admin panel with a Zitadel OIDC provider. SSO login does not work at all — there are 5 interacting bugs that prevent it from functioning. I've documented each below with symptoms and root cause analysis.
Environment
- Branch:
refactor - Deployment: Docker Compose, monolith mode, built from source
- Reverse proxy: Traefik with Cloudflare-terminated TLS
- OIDC provider: Zitadel (but these bugs are provider-agnostic)
- SSO configured via: Admin panel at
/admin (instance settings)
Bug 1: SSO callback route not in standalone route list — infinite login loop
Symptom: After authenticating with the OIDC provider, the browser redirects to
/auth/sso/callback?code=...&state=... but immediately redirects back to
/login, creating an infinite loop.
Root cause:
fluxer_app/src/router/components/RootComponent.tsx has an
isStandaloneRoute check that determines which routes render without requiring authentication. The SSO callback path (
/auth/sso/callback) is not in this list, so the router treats the unauthenticated user returning from the OIDC provider as needing to log in, redirecting them away before the callback can exchange the authorization code.
Expected:
/auth/sso/ paths should be in the standalone route list alongside other auth-related routes like
CONNECTION_CALLBACK.
Bug 2: Token exchange sends empty/malformed body — "grant_type missing" 400 error
Symptom: Even if Bug 1 is fixed, the OIDC token exchange fails with HTTP 400. The OIDC provider responds with "grant_type missing" or similar. Looking at the request, the body is
"{}" (a JSON-stringified empty object) despite being sent with
Content-Type: application/x-www-form-urlencoded.
Root cause: In
packages/api/src/auth/services/SsoService.tsx, the
exchangeCode() method constructs a
URLSearchParams object with
grant_type,
code,
redirect_uri,
client_id, and
code_verifier, then passes it as the request body. However,
FetchUtils.resolveRequestBody() does not have a handler for
URLSearchParams instances — it falls through to
JSON.stringify(), which serializes
URLSearchParams as
"{}". The OIDC provider receives an empty body.
Expected: The
URLSearchParams should be converted to a URL-encoded string (via
.toString()) before being passed to the fetch utility, or
FetchUtils.resolveRequestBody() should handle
URLSearchParams instances.
Bug 3: Client secret never loaded for token exchange — "empty client secret" 400 error
Symptom: Even if Bug 2 is fixed, the token exchange still fails. The OIDC provider responds with "empty client secret" or "invalid_client". No
Authorization header is sent with the token request.
Root cause: In
packages/api/src/auth/services/SsoService.tsx, the
getResolvedConfig() method calls
getSsoConfig() without passing
{includeSecret: true}. The
getSsoConfig() method defaults to excluding the client secret from the returned config object, so
config.clientSecret is always
undefined. When
exchangeCode() tries to build the Basic auth header, it gets
undefined and no Authorization header is sent.
Expected:
getResolvedConfig() should call
getSsoConfig({includeSecret: true}) since the resolved config is used for server-side token exchange which requires the client secret.
Bug 4: 30-second timeout masks the actual error message
Symptom: When SSO fails (due to any of the above bugs), the
SsoCallbackPage briefly shows the real error message (e.g., "grant_type missing"), but after 30 seconds it gets overwritten with "SSO sign-in timed out." This makes debugging very difficult because the useful error is only visible for a few seconds.
Root cause: In
fluxer_app/src/components/pages/SsoCallbackPage.tsx, a 30-second timeout is set via
setTimeout. The timeout is only cleared in the React useEffect cleanup function. When the SSO complete request fails quickly (e.g., 400 error), the catch block sets the real error message, but the still-pending timeout fires later and overwrites it with the generic timeout message. The
clearTimeout needs to be called in the error and success paths of the async function, not just in the React cleanup.
Bug 5: SSO users classified as "unclaimed" — all features restricted
Symptom: After fixing Bugs 1-4, SSO login finally works. However, the SSO user cannot do anything: profile updates fail with "Unclaimed Accounts can only set email via token", guild invite toggling fails, messages may be restricted, and many other features are blocked.
Root cause: In
packages/api/src/models/User.tsx, the
isUnclaimedAccount() method returns
true when
passwordHash === null && !isBot. SSO users are provisioned in
SsoService.provisionUserFromClaims() with
password_hash: null (they authenticate via OIDC, not password). This means every SSO user is incorrectly classified as an "unclaimed" (demo/preview) account. The
isUnclaimedAccount() check is used in ~15 places across the codebase to restrict features.
SSO-provisioned users receive
sso and
sso:{providerId} traits at creation time, so the fix would be to also check that the user doesn't have the
sso trait before classifying them as unclaimed.
Expected:
isUnclaimedAccount() should exclude users with SSO traits, since they are fully authenticated users who simply don't have a local password.
Steps to Reproduce
- Clone
refactor branch, build from source, deploy in monolith mode - Configure SSO via admin panel (
/admin > instance settings) with any OIDC provider - Set issuer URL, client ID, client secret (endpoints auto-discovered from
.well-known/openid-configuration) - Try to log in via SSO — observe infinite redirect loop (Bug 1)
- Fix Bug 1, retry — observe 400 error with "grant_type missing" (Bug 2)
- Fix Bug 2, retry — observe 400 error with "empty client secret" (Bug 3)
- Fix Bugs 1-3, retry — SSO login succeeds but error messages are masked by timeout (Bug 4)
- SSO user can log in but cannot use most features (Bug 5)
Impact
SSO/OIDC is a critical feature for self-hosted deployments where organizations want centralized authentication. Currently it is completely non-functional — 5 bugs must be fixed before a single SSO login can succeed.
Reference
Full deployment guide with all 20 gotchas:
https://gist.github.com/PaulMColeman/e7ef82e05035b24300d2ea1954527f10