OIDC / OAuth 2.0 Setup

The OIDC integration runs the Authorization Code flow with server-side token exchange (confidential client; the clientSecret never touches the browser). It is the recommended transport for Azure AD, Okta, Google Workspace, and AWS Cognito. One implementation note that dictates how you set issuer: endpoints are derived by string convention, not fetched from a discovery document, there is no .well-known/openid-configuration lookup (see below).

Configuration Reference

FieldTypeRequiredDescription
enabledbooleanYesSet to true to enable OIDC authentication
providerstringYesProvider identifier for logging/display: azure_ad, okta, google, auth0, keycloak, or any custom string
issuerstringYesThe OIDC issuer URL. This is the base URL used to derive the authorization, token, and userinfo endpoints. Must include the /authorize path or end with the base issuer URL.
clientIdstringYesThe client ID (application ID) registered with the IdP
clientSecretstringYesThe client secret generated by the IdP
callbackUrlstringYesThe redirect URI. Must be https://your-app.example.com/api/auth/idp/oidc/callback
scopestring[]NoOAuth scopes to request. Default: ["openid", "email", "profile"]. Add provider-specific scopes for groups if needed.
attributeMapping.emailstringYesClaim name for user email in the userinfo response (usually email)
attributeMapping.namestringYesClaim name for user display name (usually name)

Endpoint Derivation (read before setting issuer)

Endpoints are computed from issuer by suffix convention, not discovery:

Set issuer to the base that yields valid endpoints under those three suffixes, not the RFC 8414 issuer identifier if that identifier's paths differ. This is why the working Azure value below is the .../oauth2/v2.0 base (giving /authorize, /token) rather than the bare tenant issuer, and Okta uses the authorization-server base .../oauth2/default. Providers whose token/userinfo paths don't fit the {issuer}/token + {issuer}/userinfo shape need an issuer chosen so the suffixes still resolve. Group claims for role mapping must be surfaced in the userinfo response (the flow reads claims from userinfo), so enable a userinfo groups claim rather than relying solely on ID-token group claims.

Azure AD (Entra ID)

Register a single-tenant app with a Web redirect URI = the OIDC callback. Mint a client secret and capture its Value (clientSecret), the Application (client) ID (clientId), and the Directory (tenant) ID (goes in issuer). Grant delegated openid/email/profile; add GroupMember.Read.All and an emitted groups claim if mapping on Entra groups, then grant admin consent.

Azure AD OIDC config

{
  "oidc": {
    "enabled": true,
    "provider": "azure_ad",
    "issuer": "https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0",
    "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "clientSecret": "your-client-secret-value",
    "callbackUrl": "https://your-app.example.com/api/auth/idp/oidc/callback",
    "scope": ["openid", "email", "profile"],
    "attributeMapping": {
      "email": "email",
      "name": "name"
    }
  }
}

Okta

Create an OIDC → Web Application integration with sign-in redirect URI = the OIDC callback. Take Client ID / Client Secret from the General tab and set issuer to your authorization-server base, e.g. https://your-org.okta.com/oauth2/default. Add the groups scope and configure the authorization server's groups claim to be returned from userinfo for role mapping.

Okta OIDC config

{
  "oidc": {
    "enabled": true,
    "provider": "okta",
    "issuer": "https://your-org.okta.com/oauth2/default",
    "clientId": "0oaxxxxxxxxxxxxxxxx",
    "clientSecret": "your-client-secret",
    "callbackUrl": "https://your-app.example.com/api/auth/idp/oidc/callback",
    "scope": ["openid", "email", "profile", "groups"],
    "attributeMapping": {
      "email": "email",
      "name": "name"
    }
  }
}

Google Workspace

Create a Web-application OAuth client ID with authorized redirect URI = the OIDC callback; use the issued Client ID / Client Secret. Google does not expose directory groups through standard OIDC scopes, so group-based role mapping is generally unavailable for Google, expect Workspace users to resolve to the default User role and assign elevated roles in-app.

Google Workspace OIDC config

{
  "oidc": {
    "enabled": true,
    "provider": "google",
    "issuer": "https://accounts.google.com/o/oauth2/v2/auth",
    "clientId": "xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
    "clientSecret": "GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "callbackUrl": "https://your-app.example.com/api/auth/idp/oidc/callback",
    "scope": ["openid", "email", "profile"],
    "attributeMapping": {
      "email": "email",
      "name": "name"
    }
  }
}

Security Considerations

OIDC Troubleshooting

ErrorCauseFix
OIDC not configured (503)OIDC is not enabledSet oidc.enabled to true and fill in all required fields
Redirect to /login?error=invalid_callbackMissing code or state parameter in callback URLCheck the redirect URI is exactly correct in the IdP. The IdP may be misconfigured or the callback URL path is wrong.
Redirect to /login?error=invalid_stateThe state parameter does not match (expired or CSRF attempt)If the login took longer than 10 minutes, try again. If persistent, check for proxy/load balancer issues that might route the callback to a different server instance.
Redirect to /login?error=oidc_failedToken exchange or userinfo request failedCheck server logs for details. Common causes: wrong client secret, expired client secret, incorrect issuer URL, network connectivity to the IdP.
No email in user profileMissing email scope or IdP not returning email claimEnsure email is in the scope array and the IdP is configured to include the email claim