Skip to content

OIDC Setup

OpenID Connect (OIDC) lets users sign in through an external identity provider: Authelia, Authentik, Keycloak, PocketID, Zitadel, Okta, Auth0, VoidAuth, or anything standards-compliant. Single sign-on across your homelab, no app-specific password to manage, centralised MFA, and map OIDC groups/claims to roles.

OIDC is optional

The local user system works fine without OIDC. Enable OIDC when you already run an IdP and want auth to follow suit, or when you want to unify user management across multiple apps.

How it works

  1. User clicks the OIDC login button on /login.
  2. They're redirected to your provider.
  3. They authenticate (password, passkey, MFA, whatever your provider enforces).
  4. Provider redirects back to {ROMM_BASE_URL}/api/oauth/openid with an authorisation code.
  5. The code is exchanged for an ID token, the user's email and role claims are read, and either a matching local user is created on the fly (unless you've turned off registration), or an existing one is logged in.

Provider guides

Pick your provider and follow the step-by-step instructions. They all end with the same set of app-side env vars. The guides just differ on how to register the app and where to find the client ID/secret.

Not listed? Most standards-compliant OIDC providers work: Okta, Auth0, Google Workspace, Microsoft Entra, etc. Use one of the above as a template and consult your provider's docs for the registration side.

Minimum config

Whichever provider you pick, set these in the romm service's environment:

environment:
    - OIDC_ENABLED=true
    - OIDC_PROVIDER=<authelia|authentik|keycloak|pocket-id|zitadel|voidauth|generic>
    - OIDC_CLIENT_ID=<from your provider>
    - OIDC_CLIENT_SECRET=<from your provider>
    - OIDC_SERVER_APPLICATION_URL=https://auth.example.com
    - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid
    - ROMM_BASE_URL=https://demo.romm.app # must match your reverse-proxy URL

OIDC_REDIRECT_URI must exactly match what you register at the provider (same scheme, host, path, no trailing slash).

Auto-provisioning

By default, the first successful OIDC login for an email that has no matching account creates a local account automatically. To require accounts to exist beforehand (so only pre-provisioned users can sign in via OIDC), turn registration off:

environment:
    - OIDC_ALLOW_REGISTRATION=false # default: true

With it disabled, an unknown user is rejected at login instead of getting a fresh account. New accounts land in the default permission group unless a role claim maps them to Admin.

Role mapping

RomM has only two roles: User and Admin (see Users & Roles). New OIDC users are provisioned as Users. To let your IdP promote someone to Admin based on group membership, set:

environment:
    - OIDC_CLAIM_ROLES=groups # which claim to read
    - OIDC_ROLE_ADMIN=romm-admin,platform-admins # group values → Admin

On every login, the claim named by OIDC_CLAIM_ROLES is read (often groups, sometimes realm_access.roles on Keycloak, check your provider's token output). If a value matches OIDC_ROLE_ADMIN, the user becomes an Admin.

Roles are re-evaluated on every login, so demoting someone on the IdP side takes effect the next time they sign in.

Once OIDC_CLAIM_ROLES is set, users must match a mapped group

As soon as OIDC_CLAIM_ROLES is configured, RomM expects every user to match at least one mapped role group. A user whose claim matches none of the configured groups is rejected at login with:

{"detail":"User has not been granted any roles for this application."}

To let non-admin users in, map their group to the User role with OIDC_ROLE_VIEWER (or OIDC_ROLE_EDITOR):

environment:
    - OIDC_CLAIM_ROLES=groups
    - OIDC_ROLE_ADMIN=romm-admin,platform-admins # → Admin
    - OIDC_ROLE_VIEWER=platform-users # non-admins → User (grants access)

OIDC_ROLE_VIEWER and OIDC_ROLE_EDITOR no longer map to distinct roles — matching users all resolve to User — but they're still how you grant those users access when role claims are enabled. Point them at a group that all your non-admin users belong to. Use permission groups for finer-grained access; only OIDC_ROLE_ADMIN changes the role.

If you don't set OIDC_CLAIM_ROLES at all, role mapping is skipped entirely and everyone is provisioned as a User in the default permission group.

Autologin

To bypass the login page entirely and redirect straight to the IdP:

environment:
    - OIDC_AUTOLOGIN=true

Useful when you want this to feel like a native part of your SSO stack. Combine with DISABLE_USERPASS_LOGIN=true to lock out local accounts entirely.

Keep one local admin

Don't set DISABLE_USERPASS_LOGIN=true without first confirming an admin account exists on the IdP side and can log in. If OIDC breaks and you've disabled local login, you're locked out until you fix the container env.

RP-Initiated Logout

When set, hitting "Sign out" in RomM also signs the user out at the IdP:

environment:
    - OIDC_RP_INITIATED_LOGOUT=true
    - OIDC_END_SESSION_ENDPOINT=https://auth.example.com/application/o/end-session/

The endpoint URL is provider-specific, check the per-provider guides or your IdP's docs.

Username source

By default the local part of the email (the bit before @) becomes the username, but you can override it with:

environment:
    - OIDC_USERNAME_ATTRIBUTE=preferred_username

Important notes

  • Email must match between OIDC and any existing local account, otherwise OIDC creates a new account alongside the old one.
  • HTTPS is required in production, as OIDC will refuse to redirect to a plain-HTTP ROMM_BASE_URL.
  • Large drift between the RomM host and IdP will lead to clock skew and cause ID-token validation to fail.

Troubleshooting

Common failures and fixes live in Authentication Troubleshooting. Two of the usual suspects:

  • redirect_uri_mismatch: OIDC_REDIRECT_URI differs from what's registered at the provider. Even a trailing slash can matter!
  • User created but not made Admin: check OIDC_CLAIM_ROLES points at a claim that actually exists in the token, and that the group values match OIDC_ROLE_ADMIN exactly (case-sensitive).