OAUTH2/OIDC
Configure ODD Platform to authenticate users via OAuth2/OIDC against AWS Cognito, GitHub, Google, Azure AD, Okta, Keycloak, or any custom OIDC-compliant identity provider.
ODD Platform supports different OIDC/OAuth2 providers. Currently there are:
It is possible to have multiple providers at the same time (e.g. you want to allow to authenticate users from Github and Google, or from multiple Cognito user pools). Configuration properties name for each provider must fit the pattern auth.oauth2.client.{client_id}.{client_parameter}, where client_id is provider identifier.
There are some common parameters which are used across all providers:
auth.type. Must be set to OAUTH2auth.oauth2.client.{client-id}.provider. Provider code, which helps application to understand which provider is used.auth.oauth2.client.{client-id}.client-id. Client ID obtained from providerauth.oauth2.client.{client-id}.client-secret. Client secret obtained from providerauth.oauth2.client.{client-id}.client-name. Custom name, which will be shown on UI in case of multiple providers enabled. (optional)auth.oauth2.client.{client-id}.redirect-uri. Redirect URL. Must be defined as{domain}/login/oauth2/code/{client-id}auth.oauth2.client.{client-id}.scope. Authorization scopes which are allowed for application
For all OIDC providers openid scope must be included!
auth.oauth2.client.{client-id}.issuer-uri. URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 Authorization Server Metadata endpoint defined by RFC 8414.
Given that the issuer uri is composed of a host and a path, ODD Platform tries to fetch information, calling following URLs:
host/.well-known/openid-configuration/path
issuer/.well-known/openid-configuration
host/.well-known/oauth-authorization-server/path
If you don't have issuer uri or if you want to override some values, there are special properties, which should be defined:
auth.oauth2.client.{client-id}.authorization-uri.Authorization URI for the provider.auth.oauth2.client.{client-id}.token-uri.Token URI for the provider.auth.oauth2.client.{client-id}.user-info-uri.User info URI for the provider.auth.oauth2.client.{client-id}.jwk-set-uri.JWK set URI for the provider.
If issuer uri can provide this info above parameters might be skipped.
auth.oauth2.client.{client-id}.username-attribute. Defines which token claim should be picked as username in ODD Platformauth.oauth2.client.{client-id}.admin-attribute. Defines which token claim is responsible for admin principalauth.oauth2.client.{client-id}.admin-principals. List of users, who will have ADMIN role on login (for detailed explanation please check the Roles section).auth.oauth2.client.{client-id}.pkce. Optional Boolean (default unset, i.e. disabled). Enables Proof Key for Code Exchange (RFC 7636) for the authorization code flow. Whenpkce: trueandclient-secretis empty, the platform registers the client as a public OAuth2 client (client_authentication_method=none) and PKCE protects the code exchange. Whenclient-secretis set, the platform always uses confidential-client authentication and thepkceflag has no effect on the registration. Most commonly required by Keycloak — see the Keycloak with PKCE example below.
Admin-detection per-provider matrix
The configuration POJO that backs auth.oauth2.client.{id} is uniform across every provider, but each provider's runtime handler reads a different subset of the configured fields. The matrix below summarises how admin-principals and admin-groups actually behave for every supported provider — the silent-no-op rows in particular catch operators by surprise because the configuration loads without error. For the same comparison across all auth modes (not just OAuth2), see Admin promotion across providers.
AWS Cognito
admin-principals (email / configured username-attribute) and admin-groups (against the cognito:groups claim)
exact match on both
—
GitHub
admin-principals (login) before the organization-name check; admin-groups after, against team names returned by /user/teams
exact (principals); case-insensitive full match (groups)
admin-principals bypasses organization-name; GitHub Enterprise Server is not supported — api.github.com is hard-coded in the handler
admin-principals (email by default; overridable via admin-attribute)
exact match
admin-groups is a silent no-op — the field binds without error but the handler never reads it; use admin-principals or admin-attribute
Azure AD
admin-principals (against admin-attribute) and admin-groups (against the roles claim by default; switch to groups via groups-claim)
exact match
—
Okta
admin-principals always (against the admin-attribute / username-attribute claim); admin-groups only when groups-claim is set
case-insensitive full match
admin-groups is inert unless groups-claim is configured — the generic OIDC handler has no default groups claim. admin-principals works with no extra config
Keycloak
same as Okta — admin-principals always; admin-groups only when groups-claim is set
case-insensitive full match
same as Okta — admin-groups needs groups-claim
Custom OIDC
admin-principals always; admin-groups only when groups-claim is set
case-insensitive full match
no default groups claim — set groups-claim to use admin-groups; admin-principals needs no extra config
Reading the matrix: if your provider row says "silent caveats", the configured admin-groups / admin-principals are not enforced the way the property names suggest. The provider-specific sections below repeat each caveat at the point of configuration; this matrix is the single comparison view.
AWS Cognito
AWS Cognito provider can be configured using common oauth properties and couple of provider specific properties:
auth.oauth2.client.{client-id}.admin-groups. List of admin groups. Groups are retrieved fromcognito:groupstoken claim.auth.oauth2.client.{client-id}.logout-uri. Application will be redirected to this URI after user logout for removing session on cognito side. Please check AWS Docs for more details.
auth.oauth2.client.{client-id}.username-attribute is cognito:username by default
Github
You can use Github as your OAUTH provider. ODD platform can retrieve info about user organizations and teams and use it for granting ADMIN permissions (for detailed explanation please check the Roles section). There are some github specific properties, which can be set:
auth.oauth2.client.{client-id}.organization-name. Restricts login only for users from this particular organizationauth.oauth2.client.{client-id}.admin-groups. Grants admin privilegies for users who are members of these teams, which are inside above organization
In order to retrieve organization information from github, user:read and read:org scopes must be included
admin-principals bypasses organization-name. The handler checks admin-principals before the organization-name gate. A login matched in admin-principals is granted ADMIN regardless of organization membership — so an attacker who can register the matching GitHub login on github.com obtains a platform-ADMIN backdoor if a typo or stale entry sits in the list. Audit admin-principals for unowned or guessable usernames before relying on organization-name for boundary enforcement.
admin-groups is a case-insensitive full team-name match — not a substring match. admin-groups: [admins] promotes members of a team named exactly admins (case ignored, so Admins and ADMINS match too) and does not match team-admins, admins-readonly, or data-admins. Enter each admin team's name exactly as it appears in GitHub. This is consistent with every other provider — across all modes, admin-groups / admin-principals matching is full-value, case-insensitive equality (no substring, no prefix); see the admin-promotion comparison.
GitHub Enterprise Server (GHES) is not supported. The GitHub handler hard-codes https://api.github.com for its /user/orgs and /user/teams calls — there is no configuration knob to point at a GHES instance. Deployments using https://github.example.com/api/v3 see DNS/cert failures on the post-login enrichment. A configurable base URL is tracked upstream; until then, use a different OAuth provider for GHES-only deployments.
A GitHub username rename orphans the user's owner association. ODD identifies a GitHub user by the mutable login — the claim selected by user-name-attribute (login in the example below) — not the stable numeric account id, and the owner mapping is keyed on that login. When a user renames their GitHub account they sign in as a new identity: their previous owner association is silently orphaned — their My Objects empties and their ownership strands under the old name — with no automatic re-link. Plan a manual update of the user-owner mapping around any GitHub login rename. Note also that GitHub releases the old login after 90 days, so it can later be claimed by someone else.
Google
ODD Platform allows to authenticate users via Google. You can restrict users to login under your organization domain. This is controlled by auth.oauth2.client.{client-id}.allowed-domain property.
admin-groups is a silent no-op for the Google provider. The Google handler does not read the admin-groups list — the field binds without error but every member of a configured admin-group remains a regular USER. Promote individual operators to ADMIN via admin-principals (email by default) or via a custom claim configured through admin-attribute. Track upstream for a boot-time warning when admin-groups is set with the Google provider.
Azure AD
ODD Platform supports integration with Azure Active Directory (Azure AD) using OAuth2/OpenID Connect (OIDC). Azure AD applications can be registered in one of two modes:
Single-tenant — only users from one specific Azure AD tenant can sign in. Use this for organisation-internal deployments.
Multi-tenant — users from any Azure AD tenant can sign in. Use this when the platform is hosted as a service for several organisations.
The two modes share the same properties; they differ only in how the issuer-uri, authorization-uri, token-uri, and logout-uri are constructed. Examples for both forms are given below.
Prerequisites: Azure AD app registration
Before configuring ODD Platform, register an application in Azure AD:
Go to Azure Active Directory → App registrations → New registration.
Choose the supported account types (single-tenant vs. multi-tenant) that match the deployment.
Add a Web redirect URI:
{host}/login/oauth2/code/azure.Under Certificates & secrets, generate a client secret and store its value — it cannot be viewed again later.
Under API permissions, add the following Microsoft Graph delegated permissions and grant admin consent:
openid,offline_access,User.Read. Addemailandprofileas well if theprofile/emailscopes are requested below.(Optional) To use
admin-groups, choose one of the following:Azure AD App Roles (default, recommended) — under App roles, create the roles you want to grant ADMIN (for example
Admins,Managers), then assign users or groups to those roles. Azure will emit the role values in therolesclaim of the ID token, which is what ODD Platform reads by default.Azure AD security groups — under Token configuration → Add groups claim, include the
groupsclaim in the ID token. You must also setgroups-claim: groupsin the ODD configuration (see the note underadmin-groupsbelow), because the Azure handler reads fromrolesunless told otherwise.
Note the Application (client) ID and Directory (tenant) ID — both are needed by the configuration below.
Single-tenant configuration
Use the tenant-specific issuer-uri; {azure_tenant_id} is your Directory (tenant) ID. Spring Security will discover authorization-uri, token-uri, jwk-set-uri, and user-info-uri from the issuer's OpenID Connect discovery document, so they do not need to be set explicitly. jwk-set-uri is still shown in the example because it can be required when the discovery endpoint is unreachable (for example, from air-gapped networks).
Multi-tenant configuration
For a multi-tenant application, the OpenID Connect discovery document is not served under a tenant-specific URL, so issuer-uri cannot be used. Instead, override authorization-uri, token-uri, jwk-set-uri, and logout-uri to point at the organizations endpoint (use common if the app should also accept personal Microsoft accounts). azure-tenant-id must still be set to the tenant that owns the app registration.
Notes:
Ensure the
openidscope is always included, as it is mandatory for OIDC.The
azure-tenant-idshould correspond to the Azure AD tenant that owns the app registration.The
jwk-set-uriis mandatory for Azure to function correctly with ODD Platform when the discovery endpoint is unreachable.logout-uriis the Azure AD OpenID Connect logout endpoint —https://login.microsoftonline.com/{azure_tenant_id}/oauth2/v2.0/logoutfor single-tenant andhttps://login.microsoftonline.com/organizations/oauth2/v2.0/logout(or thecommonvariant if the app also accepts personal Microsoft accounts) for multi-tenant. On logout, ODD Platform redirects the browser here so Azure AD can end its own session and then return the user to the Platform.
logout-uri must be set for Azure SSO. The Azure-specific logout handler calls URI.create(provider.getLogoutUri()); leaving logout-uri unset raises a NullPointerException and the logout flow returns a 500 response. Always include logout-uri when configuring the azure provider.
admin-principalsis the list of user identifiers (matched against theadmin-attributeclaim,emailin the examples above) that will be granted the ADMIN role on login.admin-groupsgrants the ADMIN role to every user whose token contains one of the listed values. By default, ODD Platform's Azure handler reads these values from therolesclaim, which Azure AD populates from App roles assigned to the user. The values inadmin-groupsmust match the app rolevaluefields, not Azure AD display names.To grant ADMIN based on Azure AD security group membership instead, set
groups-claim: groupsin the Azure configuration and add thegroupsclaim to the ID token in Azure (Token configuration → Add groups claim). Theadmin-groupslist then matches against values emitted in thegroupsclaim (group object IDs by default — switch the Azure claim output toGroup Namein the Token configuration dialog if you want to match on group display names).
If an external user's login doesn't provide the email attribute by default, ensure that the user exists as an external guest in Azure AD associated with an email.
Troubleshooting Tips:
If you encounter errors regarding the missing
emailattribute, ensure the user exists in Azure AD as a properly configured external guest user with an email attribute.For single-tenant deployments, always verify that
issuer-uri,jwk-set-uri,user-info-uri, andlogout-uricorrespond to your tenant ID.If logout returns a 500 error or never completes, verify that
logout-uriis set and matches your single-tenant / multi-tenant choice. An unsetlogout-uritriggers aNullPointerExceptionin the Azure logout handler; the browser sees a 500 while the user remains signed in on both sides.If
admin-groupshas no effect, check which claim is being read:App Roles (default) — decode the ID token at
jwt.msor equivalent; confirm therolesclaim is present and contains the app-rolevaluefields that youradmin-groupslist references. If therolesclaim is absent, verify that you have assigned users to app roles under Enterprise applications → your app → Users and groups.Security groups — confirm that
groups-claim: groupsis set in the ODD configuration (without this,admin-groupsis matched againstroles, notgroups), and that thegroupsclaim is emitted by the Azure token configuration.
Other OIDC providers
ODD Platform doesn't have any specific parameters for other providers, so they can be easily configured using default parameters. You can check examples below for OKTA and Keycloak OIDC providers.
Okta, Keycloak, and any other "Custom OIDC" provider DO promote to ADMIN via admin-principals. These providers fall through to the generic CustomOIDCUserHandler, which evaluates admin-principals on every login: a user whose admin-attribute (or username-attribute) claim matches a configured admin-principals entry — case-insensitive, full match — is promoted to ADMIN with no extra configuration. admin-groups is also honoured, but only if you set groups-claim explicitly, because the generic handler has no default groups claim. With neither admin-principals nor (groups-claim + admin-groups) configured, every authenticated user is granted USER and promotion is a manual Owner-Role binding (an ADMIN-equivalent Role bundle) through the Management UI. The Okta and Keycloak YAML examples below set admin-principals, consistent with this.
Keycloak with PKCE
Keycloak realms can be configured to require Proof Key for Code Exchange (PKCE, RFC 7636) on the authorization code flow — most commonly for public clients (browser-based or SPA-style flows that cannot keep a client secret).
To enable PKCE, set pkce: true on the Keycloak provider configuration and leave client-secret empty. The platform then registers the client as public (client_authentication_method=none) and Spring Security performs the code exchange with PKCE.
Confidential clients (with client-secret set) and pkce: true. When both pkce: true and client-secret are configured, the platform falls back to confidential-client authentication using the client secret and the pkce flag has no effect on the OAuth2 client registration — PKCE parameters are not added to the authorization request. If your Keycloak realm enforces PKCE on a confidential client, configure the client as public (omit client-secret) and rely on pkce: true alone, or disable PKCE enforcement on the realm side.
The pkce property is declared on ODD's generic OAuth2 provider configuration, so it is technically available for any provider — not just Keycloak. In practice, Keycloak is the realm most commonly configured to require PKCE.
Logout token-revocation matrix
When an operator clicks Sign out in the ODD UI, the platform invalidates the local server-side session and — depending on the provider — optionally asks the identity provider (IdP) to revoke the OAuth2 access token. Whether the IdP-issued token is actually revoked differs per provider, and the residual-token-validity window can be substantial. Operators on shared or public workstations need to know which providers leave a usable token behind after a "successful" logout.
AWS Cognito
yes
no — the handler redirects to Cognito's /logout for session removal but does not call /oauth2/revoke
access token ~1 hour; refresh token up to 90 days, depending on the User Pool configuration
GitHub
yes
yes — the handler calls DELETE /applications/{client_id}/grant to revoke the OAuth grant on the user's behalf
—
yes
yes — the handler POSTs to oauth2.googleapis.com/revoke for the access token
—
Azure AD
yes
no — Azure AD v2.0 does not expose RFC 7009 token revocation (protocol-level limitation); the handler can only redirect to the OIDC end-session endpoint
access token ~1 hour; refresh token per tenant policy
ODD_IAM
yes
no — session invalidation only by design (the IAM provider does not implement revocation)
—
Okta / Keycloak / Custom OIDC
yes
no — there is no dedicated logout handler; the platform invalidates only the local session
per IdP policy on the access token; refresh tokens persist until they expire
Operator caution. For any row in the IdP access token revoked: no column, a token captured by an attacker before logout — for example, from browser-side credential exfiltration on a shared terminal — remains usable against IdP-protected resources for the remainder of the token's natural validity. The ODD platform's session invalidation does not affect this; the only mitigation in those modes is to wait for the token to expire or to sign out at the IdP directly (via the Cognito hosted UI, the Azure portal session controls, etc.).
A platform-side fix for the Cognito gap — calling /oauth2/revoke from the Cognito logout handler — is tracked upstream.
Post-logout redirect derivation
All five OAuth2 logout handlers (Cognito, Github, Google, Azure, ODD_IAM) build the post_logout_redirect_uri (or, for the providers that use a different name, the equivalent return-URL parameter) from the inbound request's URI. The platform reads scheme, host, port, path, and query from the inbound HTTP request and reconstructs the base URL with / as the path — there is no allowlist of trusted hosts, no platform.base-url-style configuration property, and no scheme enforcement.
In a typical deployment where the inbound Host header is the browser-presented value and the platform's reverse proxy passes it through unchanged, the post-logout redirect points back to the same origin and there is nothing to worry about. The concern surfaces when the reverse proxy trusts user-controlled Host or X-Forwarded-Host headers, or when the IdP's post-logout-redirect allowlist is wider than the platform's host.
If your deployment terminates TLS at a reverse proxy that trusts and forwards user-controlled Host or X-Forwarded-Host headers, the post-logout redirect chain can be hijacked. An attacker sending X-Forwarded-Host: attacker.example.com causes the platform to construct a return-URL targeting the attacker domain; if the IdP's post_logout_redirect_uri allowlist is wildcarded (e.g. *.example.com), the IdP accepts it and the user lands on attacker.example.com after their authenticated logout completes. Mitigations: configure the reverse proxy to strip or rewrite the inbound Host / X-Forwarded-Host headers before forwarding to the platform; configure the IdP's logout allowlist with the most-specific host(s), not wildcards.
This compounds with the revocation matrix above. For Cognito / Azure / ODD_IAM (where IdP access tokens are not revoked on platform logout), an open-redirect that lands the user on an attacker-controlled domain still benefits from any captured tokens remaining valid against IdP-protected resources. The two issues form one security cluster: revocation gap + redirect derivation + reverse-proxy header trust. A platform-side odd.platform-base-url allowlist that validates the inbound Host against a configured value is tracked upstream.
UI feedback on logout outcome
The SPA's Sign out action sends the user to /logout via a hard navigation; the platform handles the rest. The SPA does not distinguish between "logged out + IdP token revoked" (Google / GitHub) and "logged out + IdP token still valid" (Cognito / Azure / ODD_IAM) in any UI-visible signal. Operators on shared or public terminals who depend on revocation should not infer it from the absence of an error; sign out from the IdP directly (Cognito hosted UI, Azure portal, etc.) in addition to ODD logout when the workstation is not yours. Per-provider feedback messages in the SPA are tracked upstream.
Last updated