Authentication Troubleshooting
403 Forbidden on API calls
When auth is enabled (almost always), any endpoint that requires a session returns 403 if:
- You're not authenticated.
- Your session is in a broken state (expired, signed with an old secret, missing CSRF).
Fix: clear cookies for the host and sign in again.
Forbidden (403) CSRF verification failed
CSRF protection is on by default, so a mismatched or missing csrftoken cookie causes this.
- Reload the page, and a fresh CSRF cookie is set on GET requests, which should fix it on the next POST.
- Still broken? Clear cookies for the host and hard-reload (
CMD+SHIFT+R/CTRL+F5). - As a last resort, disable CSRF verification with
DISABLE_CSRF_PROTECTION=truein your env, but we strongly discourage this as it opens you up to CSRF attacks.
If you're behind a reverse proxy and CSRF keeps failing, the proxy is probably stripping the csrftoken cookie or the X-CSRFToken header. See the Reverse Proxy recipes. Every one of them forwards Cookie and all custom headers by default, so if yours doesn't, fix the proxy config.
400 Bad Request on the WebSocket endpoint
Your reverse proxy is stripping the WebSocket upgrade, and live updates (scan progress, Netplay) use socket.io. Fixes for each proxy:
- Nginx/NPM: enable WebSockets Support (the Reverse Proxy snippets already do this).
- Traefik: add
proxy_set_header Upgrade $http_upgrade(or use the Traefik middleware equivalent). - Caddy: WebSockets work out of the box with
reverse_proxy. - Cloudflare: enable WebSockets under Network settings.
Error: Could not get twitch auth token: check client_id and client_secret
IGDB creds are wrong or revoked on the Twitch side.
- Go to dev.twitch.tv/console/apps.
- Verify your application still exists.
- Regenerate the Client Secret, and copy both Client ID and Client Secret.
- Update
IGDB_CLIENT_IDandIGDB_CLIENT_SECRETin your env. docker compose up -dto pick up the new values.
Password logins are disabled, OIDC is broken, I'm locked out
You set DISABLE_USERPASS_LOGIN=true and now OIDC isn't working.
- Edit your compose/env to unset
DISABLE_USERPASS_LOGIN(or set it tofalse). docker compose up -dto restart with the new config.- Log in with your local admin and fix OIDC with the steps below.
- Re-enable
DISABLE_USERPASS_LOGINonly after confirming OIDC works end-to-end.
This is the reason OIDC Setup tells you to verify OIDC before turning off local login.
OIDC
redirect_uri_mismatch
The OIDC_REDIRECT_URI in the env doesn't exactly match what's registered at the IdP. Check for:
- Trailing slashes:
/api/oauth/openidvs/api/oauth/openid/are different to the IdP. - Scheme:
http://vshttps:// - Host:
demo.romm.appvswww.demo.romm.appvs the bare IP - Port: implied
80/443on HTTPS vs an explicit port
User is created but stays Viewer, even though they should be Admin
You configured OIDC_CLAIM_ROLES but it's not being honoured.
- Is the claim actually in the token? Decode your IdP's ID token at jwt.io and verify the claim name (e.g.
groups,realm_access.roles) is present and non-empty. - Does the value match?
OIDC_ROLE_ADMIN=romm-adminwill only match if the claim contains exactly the stringromm-admin, and it's case-sensitive. - Is the claim mapper on the IdP side configured to include the claim? On Keycloak, for example, you need a Client Scope with a Group Membership mapper added to the client.
Roles are re-evaluated on every login with no cache to bust, so log out and back in to test the fix.
"Email is missing from token" (Zitadel-specific)
On Zitadel, open the application → Token Settings → tick User Info inside ID Token → Save.
See OIDC with Zitadel → Enable claims for the full walkthrough.
Authentik 2025.10: login succeeds but the user is rejected
Authentik 2025.10 changed the default email_verified claim from true to false but a verified email is required so the claim must arrive as true.
Fix: add the property mapping documented in OIDC with Authentik → Create a property mapping.
Keycloak: user created locally but can't log in
Two possibilities:
- Email not verified in Keycloak: Admin Console → Users → open the user → Email Verified: on. Unverified emails are rejected.
- Email mismatch between Keycloak and a pre-existing local user: if a local account
alice@example.comalready exists, the first OIDC login foralice@example.comsigns into that account. If the emails don't match exactly, a second account is created. Fix: edit the local user to set the correct email, then log in via OIDC.
OAuthException: expired token on callback
Your host and the IdP have significant clock drift, so run NTP on both.
Autologin loops forever
You set OIDC_AUTOLOGIN=true and your IdP keeps bouncing you back, which bounces you back to the IdP.
Usually because something else in the chain (a CSRF check, a cookie domain mismatch, a reverse-proxy rewrite) is breaking the post-callback handoff. To escape:
- Hit
/login?bypass_autologin=truedirectly to land on the normal login page. - Sign in as a local admin.
- Disable
OIDC_AUTOLOGIN, restart, and debug the IdP config with autologin off.
If bypass_autologin doesn't work in your version, shell into the container and unset OIDC_AUTOLOGIN in the env, or edit your compose and restart.
Still stuck?
- Check the container logs:
docker logs romm 2>&1 | grep -iE 'auth|oidc|oauth'. - Cross-reference your IdP's audit logs, which often show exactly why a login was rejected on their side.
- Ask on Discord
#helpwith the IdP name and the exact error text.