Skip to Content
MaestroInstallationOIDC Setup

OIDC Setup

Maestro delegates all authentication to an external OIDC provider. There is no built-in password login. Every user who signs in must exist in your IdP first.

How Maestro uses OIDC

  • The browser performs the standard Authorization Code flow against your IdP.
  • Maestro validates the resulting ID token against the provider’s JWKS.
  • The token’s email claim identifies the user. email_verified must be true.
  • The token’s sub claim is the persistent account key — it’s what Maestro stores to recognise a returning user across email changes. Both the email and account-key claim names are remappable (see Claim mapping) for IdPs whose token shape differs from the Keycloak defaults.
  • Superadmin status is derived from either an OIDC group claim or an email allowlist. A user who matches either becomes a superadmin on first login.
  • Regular users start with no org membership. A superadmin (or an org owner) must place them in an org before they can do anything useful.

Required environment variables

Set these in maestro.env:

maestro: env: - name: MAESTRO_BASE_URL value: https://maestro.example.com - name: OIDC_ISSUER_URL value: https://auth.example.com/realms/cardinal - name: OIDC_AUDIENCE value: maestro-ui - name: OIDC_SUPERADMIN_EMAILS value: admin@example.com,ops@example.com
VariableNotes
MAESTRO_BASE_URLMust exactly match the redirect URI registered with your IdP
OIDC_ISSUER_URLUsed for JWKS discovery ({issuer}/.well-known/openid-configuration)
OIDC_AUDIENCEThe aud claim Maestro expects on incoming tokens. Defaults to maestro-ui
OIDC_CLIENT_IDOAuth client ID the SPA sends in /authorize requests. Defaults to OIDC_AUDIENCE. Set this explicitly when your IdP issues tokens whose aud differs from the client ID (e.g. Okta with a custom authorization server)
OIDC_SUPERADMIN_GROUPName of a groups claim value granting superadmin. Default maestro-superadmin
OIDC_SUPERADMIN_EMAILSComma-separated email allowlist for superadmin. Case-insensitive. Use this to bootstrap before groups are configured
OIDC_JWKS_URLOverride if your JWKS is at a non-standard path
OIDC_TRUST_UNVERIFIED_EMAILStrue treats all OIDC emails as verified. Only use with IdPs that gate email ownership in another way
OIDC_EMAIL_CLAIMToken claim to read the user’s email from. Default email
OIDC_EMAIL_VERIFIED_CLAIMToken claim to read the email-verified boolean from. Default email_verified
OIDC_DISPLAY_NAME_CLAIMSComma-separated fallback chain for the display name. Default name,preferred_username,email
OIDC_GROUPS_CLAIMToken claim to read the group array from. Default groups
OIDC_EXTERNAL_ID_CLAIMToken claim Maestro uses as the persistent account key. Default sub. Changing this on an existing deployment is a user migration event — see Claim mapping

All five claim-mapping variables are optional; leaving them unset preserves the pre-existing Keycloak-shaped behavior.

Redirect URI

Register the following redirect URI with your IdP (where MAESTRO_BASE_URL is whatever you set):

https://maestro.example.com/api/auth/callback

Required token claims

Maestro reads these claims out of the ID token. Configure your IdP to include them (or, if the claim names differ, remap them with the OIDC_*_CLAIM env vars — see Claim mapping):

Logical claimDefault token keyRequiredPurpose
Account keysubYesPersistent account identifier. Must be stable for a given user for the life of the install
EmailemailYesPrimary user key
Email verifiedemail_verifiedYes (or set OIDC_TRUST_UNVERIFIED_EMAILS=true)Prevents email spoofing
Display namenamepreferred_usernameemailRecommendedFirst non-empty value from the chain is shown in the UI
GroupsgroupsIf using group-based superadminArray of group names

Bootstrapping the first superadmin

On a fresh install, no users exist in the database. When the first person logs in:

  1. They authenticate with your IdP as usual.
  2. Maestro looks at their email (and groups, if any).
  3. If the email is listed in OIDC_SUPERADMIN_EMAILS or their groups contain OIDC_SUPERADMIN_GROUP, the user is auto-upserted with role=superadmin.
  4. Otherwise they land as a regular user with no org — and they’ll see a “contact your administrator” screen.

The practical pattern: set OIDC_SUPERADMIN_EMAILS to your own email during install so you can log in and configure the rest. Once group mappings are working end-to-end, you can remove the allowlist.

Keycloak

Maestro is developed against Keycloak and the docker-compose dev stack uses it as the default IdP.

  1. Create a new realm (e.g. cardinal).
  2. Create a client:
    • Client ID: maestro-ui
    • Client type: OpenID Connect
    • Client authentication: off (public client — the UI is a browser SPA)
    • Standard flow: on
    • Valid redirect URIs: https://maestro.example.com/*
    • Web origins: https://maestro.example.com
  3. Under Client scopes → maestro-ui-dedicated → Add mapper, add a Group Membership mapper:
    • Token Claim Name: groups
    • Full group path: off
    • Add to ID token: on
    • Add to userinfo: on
  4. Create a group maestro-superadmin and add yourself to it.
  5. Set:
    - name: OIDC_ISSUER_URL value: https://keycloak.example.com/realms/cardinal - name: OIDC_AUDIENCE value: maestro-ui - name: OIDC_SUPERADMIN_GROUP value: maestro-superadmin

Okta

  1. Create an OIDC Single-Page Application.

  2. Sign-in redirect URI: https://maestro.example.com/api/auth/callback.

  3. Add a Groups claim to the ID token — filter to the groups you care about.

  4. Assign users to a group (e.g. maestro-superadmin) and assign that group to the app.

  5. Set:

    - name: OIDC_ISSUER_URL value: https://{your-okta-domain}/oauth2/default - name: OIDC_AUDIENCE value: {the app's client ID} - name: OIDC_SUPERADMIN_GROUP value: maestro-superadmin

    If your Okta tenant uses a custom authorization server whose audience is a resource URI (e.g. api://maestro) rather than the app’s client ID, split the two:

    - name: OIDC_AUDIENCE value: api://maestro # the `aud` on issued tokens (the auth server's audience) - name: OIDC_CLIENT_ID value: 0oabc123ClientIdXYZ # the SPA app's client ID (used in /authorize)

    For the default Okta authorization server (no custom audience), OIDC_CLIENT_ID equals OIDC_AUDIENCE and can be omitted.

Some Okta setups don’t populate email_verified. If users hit a “your email is not verified” error and you trust Okta’s identity proofing, add:

- name: OIDC_TRUST_UNVERIFIED_EMAILS value: "true"

Okta access tokens (non-standard claim shape)

Some Okta tenants issue access tokens rather than ID tokens, with a claim layout that doesn’t match the defaults: sub carries the user’s email (not an opaque id), the stable user id lives in a custom claim like uid, and email / email_verified / name / groups may be absent entirely.

Tell Maestro where to find each logical claim:

- name: OIDC_EMAIL_CLAIM value: "sub" - name: OIDC_EXTERNAL_ID_CLAIM value: "uid" - name: OIDC_DISPLAY_NAME_CLAIMS value: "name,preferred_username,sub" - name: OIDC_TRUST_UNVERIFIED_EMAILS value: "true" - name: OIDC_SUPERADMIN_EMAILS value: first-admin@example.com

A few things to know about this combination:

  • OIDC_EXTERNAL_ID_CLAIM must not resolve to the same token claim as OIDC_EMAIL_CLAIM. Maestro refuses to boot if they do — account keys must be stable, and emails aren’t. The literal case (OIDC_EXTERNAL_ID_CLAIM=email) and the subtle case (OIDC_EMAIL_CLAIM=sub + OIDC_EXTERNAL_ID_CLAIM=sub) both fail fast.
  • Remapping the email claim plus OIDC_TRUST_UNVERIFIED_EMAILS=true logs a warning at startup. It’s warn-only, not fatal, because this is a legitimate Okta configuration — but the warning is worth reading. The resolved email feeds five authorization paths: superadmin allowlist matching, domain auto-join, invite acceptance, email-collision account linking, and UI display. Anything upstream that can control the remapped claim effectively controls those paths.
  • Groups may be scoped out entirely in this shape. Use OIDC_SUPERADMIN_EMAILS to bootstrap the first superadmin and manage further promotions from the admin UI.

Google Workspace

Google OIDC does not issue a groups claim out of the box. Use the email allowlist:

- name: OIDC_ISSUER_URL value: https://accounts.google.com - name: OIDC_AUDIENCE value: {your-client-id}.apps.googleusercontent.com - name: OIDC_SUPERADMIN_EMAILS value: alice@example.com,bob@example.com

Claim mapping

Maestro reads five logical claims out of each token: email, email-verified, display-name chain, groups, and a persistent account key (called externalId internally, defaulted to sub). Each one can be remapped to a different token claim name via an env var. The defaults match Keycloak’s shape; operators on Okta, Auth0, Azure AD, and so on typically override one or two.

Env varMaps toDefaultNotes
OIDC_EMAIL_CLAIMemailemail
OIDC_EMAIL_VERIFIED_CLAIMemailVerifiedemail_verified
OIDC_DISPLAY_NAME_CLAIMSdisplayNamename,preferred_username,emailComma-separated fallback chain, evaluated left-to-right until a non-empty value is found
OIDC_GROUPS_CLAIMgroupsgroups
OIDC_EXTERNAL_ID_CLAIMexternalId (account key)subSingle claim; never a fallback chain

Rules the verifier enforces:

  • OIDC_EXTERNAL_ID_CLAIM and OIDC_EMAIL_CLAIM must not resolve to the same token claim. Maestro refuses to boot otherwise.
  • OIDC_DISPLAY_NAME_CLAIMS rejects empty entries (e.g. "name,,email" fails at startup) so the operator intent is unambiguous.
  • If OIDC_TRUST_UNVERIFIED_EMAILS=true and OIDC_EMAIL_CLAIM is non-default, startup logs a warning listing the authorization paths that consume the resolved email. This is intentionally not fatal — legitimate Okta configs need this combination — but operators should audit upstream control of the remapped claim.

Changing OIDC_EXTERNAL_ID_CLAIM on an existing deployment is a migration event

The account key is persisted per user. Switching OIDC_EXTERNAL_ID_CLAIM on a live install means every existing user’s stored key stops matching the one in their next token:

  • Email-matching users will be silently relinked to the new key (the same path used when an IdP issues a brand-new sub for an existing email).
  • Users whose email also changed at the same time will appear as brand-new accounts, losing org memberships and roles.

Do not toggle this variable on a populated database without a planned migration. On a fresh install it’s free; after the first login it isn’t.

Troubleshooting

SymptomCause
Redirect loop after loginMAESTRO_BASE_URL doesn’t match the redirect URI registered with the IdP, or it’s behind a different hostname than the browser is using
”Invalid token audience”OIDC_AUDIENCE doesn’t match the token’s aud claim (for some IdPs this is the client ID; for others — e.g. Okta with a custom authorization server — it’s a resource identifier distinct from the client ID, in which case set OIDC_CLIENT_ID separately)
“Your email is not verified”The IdP is omitting email_verified or setting it to false. Fix at the IdP, or set OIDC_TRUST_UNVERIFIED_EMAILS=true if appropriate
Logged in but no admin menuYou’re not in OIDC_SUPERADMIN_GROUP and not in OIDC_SUPERADMIN_EMAILS. Add yourself to one of them and log out / back in
JWKS fetch errors in logsOIDC_ISSUER_URL unreachable from the pod, or the IdP’s JWKS lives outside the standard discovery path — set OIDC_JWKS_URL explicitly
OIDC_EXTERNAL_ID_CLAIM ('x') resolves to the same claim as OIDC_EMAIL_CLAIM at startupYou pointed the email and account-key to the same token claim (either directly, or via defaults + one override). Pick a different stable claim for OIDC_EXTERNAL_ID_CLAIM — see Claim mapping

Reach out to support@cardinalhq.io for support or to ask questions not answered in our documentation.

Last updated on