Skip to content

SSO Callback Returns 400 / 401

The "Sign in with X" button on the login page redirects you to the IdP, you authenticate, you get bounced back to MFCloud — and the console shows an error instead of the dashboard.

The signed cookie that holds (state, nonce, pkce_verifier) is missing or expired.

Causes: - The flow took longer than 10 minutes (cookie TTL). Click the SSO button again — fresh state. - The browser stripped the cookie because you came back to a different hostname than you started from (e.g. started at https://mfconsole.com, came back to https://192.168.10.24). Always use the same hostname for start and callback. Caddy's site blocks are case-sensitive on hostname. - A third-party cookie blocker (or strict tracking-protection mode) is dropping the cookie. The cookie is SameSite=Lax, which IdP redirects DO carry — but some "block all cross-site cookies" toggles still nuke it.

400: state mismatch — possible CSRF

The state parameter the IdP echoed back doesn't match the one in the cookie. Almost always a stale browser tab — you started the flow in tab A, then clicked SSO again in tab B which overwrote the cookie, then came back to tab A's callback. Close the extra tabs and try once cleanly.

400: IdP returned error 'invalid_grant' / similar

The IdP itself is rejecting our token exchange. The error_description from the IdP is included verbatim — read it.

Common cases: - Wrong redirect URI registered. The redirect URI MUST match byte-for-byte between /start, /callback, and what's listed in your IdP app. If the master is behind Caddy, make sure Uvicorn is started with --proxy-headers so X-Forwarded-Proto: https is honored. - Stale auth code. You hit Back/Forward in the browser and the IdP rejected the re-use. Click the SSO button again.

401: ID token rejected: nonce mismatch

You started one flow, abandoned it, started another — and the new IdP response is being checked against the new cookie's nonce. Same fix as state mismatch: close extra tabs, try once.

401: ID token rejected: ... 'aud' is required

The IdP isn't including your client_id in the token's aud claim. Check the OIDC app at the IdP — Entra has a "Token configuration → optional claims" section; Okta has "Sign On → OpenID Connect ID Token". Add the audience and retry.

404: OIDC is not enabled

Self-explanatory — the SSO settings toggle is off. Settings → Single Sign-On → Enable and Save.

Diagnostic — read the master logs

docker compose logs app -n 200 | grep -i oidc

The router prints request shape, IdP response status, and any token validation error in plaintext. Far faster than reverse-engineering from the browser.

Cache invalidation

If you've changed the Issuer URL in Settings → SSO, the discovery doc + JWKS were cached for up to 60 min and might still serve the old endpoints. The save flow now calls oidc_auth.clear_caches() automatically, but if you're using curl to test the settings endpoint directly you'll skip that — restart the app container to force-flush.

See also