# OAUTH2/OIDC

ODD Platform supports different OIDC/OAuth2 providers. Currently there are:

* [AWS Cognito](#aws-cognito)
* [Github](#github)
* [Google](#google)
* [Azure AD](#azure-a-d)
* [Okta](#other-oidc-providers)
* [Keycloak](#other-oidc-providers)
* [Custom OIDC provider](#other-oidc-providers)

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 OAUTH2
* `auth.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 provider
* `auth.oauth2.client.{client-id}.client-secret`. Client secret obtained from provider
* `auth.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

{% hint style="warning" %}
For all OIDC providers **openid** scope must be included!
{% endhint %}

* `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.

{% hint style="info" %}
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.
{% endhint %}

* `auth.oauth2.client.{client-id}.username-attribute`. Defines which token claim should be picked as username in ODD Platform
* `auth.oauth2.client.{client-id}.admin-attribute`. Defines which token claim is responsible for admin principal
* `auth.oauth2.client.{client-id}.admin-principals`. List of users, who will have ADMIN role on login (for detailed explanation please check the [Roles](/configuration-and-deployment/enable-security/authorization/roles.md) section).
* `auth.oauth2.client.{client-id}.pkce`. Optional Boolean (default unset, i.e. disabled). Enables [Proof Key for Code Exchange (RFC 7636)](https://datatracker.ietf.org/doc/html/rfc7636) for the authorization code flow. When `pkce: true` **and** `client-secret` is empty, the platform registers the client as a **public** OAuth2 client (`client_authentication_method=none`) and PKCE protects the code exchange. When `client-secret` is set, the platform always uses confidential-client authentication and the `pkce` flag has no effect on the registration. Most commonly required by Keycloak — see the [Keycloak with PKCE](#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](/configuration-and-deployment/enable-security/admin-promotion.md).

| Provider        | Admin detection in code                                                                                                                         | Match semantic                                              | Silent caveats                                                                                                                                                                          |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **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 substring** (groups) | `admin-principals` **bypasses** `organization-name`; `admin-groups` matches as substring; GitHub Enterprise Server is **not supported** — `api.github.com` is hard-coded in the handler |
| **Google**      | `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**        | Pass-through Custom OIDC handler                                                                                                                | n/a                                                         | **No admin path** — the Okta provider has no admin-detection logic; ADMIN promotion requires manual Owner-Role binding via Management UI                                                |
| **Keycloak**    | Pass-through Custom OIDC handler                                                                                                                | n/a                                                         | **No admin path** — same as Okta                                                                                                                                                        |
| **Custom OIDC** | Provider-specific admin detection is absent; the handler relies on the generic OIDC defaults (no `admin-groups` semantic)                       | —                                                           | Author per-provider promotion logic at the Owner-Role binding tier                                                                                                                      |

**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 from `cognito:groups` token 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](https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html) for more details.

{% hint style="info" %}
`auth.oauth2.client.{client-id}.username-attribute` is `cognito:username` by default
{% endhint %}

{% tabs %}
{% tab title="YAML" %}

```yaml
auth:
    type: OAUTH2
    oauth2:
        client:
            cognito:
                provider: cognito
                client-id: {client_id}
                client-secret: {client_secret}
                scope: openid
                redirect-uri: {host}/login/oauth2/code/cognito
                client-name: Cognito
                issuer-uri: {issuer_uri}
                logout-uri: {logout_uri}
                admin-groups: admin
                admin-attribute: cognito:username
                admin-principals: john,david
```

{% endtab %}

{% tab title="Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_OAUTH2_CLIENT_COGNITO_PROVIDER=cognito
AUTH_OAUTH2_CLIENT_COGNITO_CLIENT_ID={client_id}
AUTH_OAUTH2_CLIENT_COGNITO_CLIENT_SECRET={client_secret}
AUTH_OAUTH2_CLIENT_COGNITO_SCOPE=openid
AUTH_OAUTH2_CLIENT_COGNITO_REDIRECT_URI={host}/login/oauth2/code/cognito
AUTH_OAUTH2_CLIENT_COGNITO_CLIENT_NAME=Cognito
AUTH_OAUTH2_CLIENT_COGNITO_ISSUER_URI={issuer_uri}
AUTH_OAUTH2_CLIENT_COGNITO_LOGOUT_URI={logout_uri}
AUTH_OAUTH2_CLIENT_COGNITO_ADMIN_GROUPS=admin
AUTH_OAUTH2_CLIENT_COGNITO_ADMIN_ATTRIBUTE=cognito:username
AUTH_OAUTH2_CLIENT_COGNITO_ADMIN_PRINCIPALS=john,david
```

{% endtab %}
{% endtabs %}

#### 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](/configuration-and-deployment/enable-security/authorization/roles.md) 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 organization
* `auth.oauth2.client.{client-id}.admin-groups`. Grants admin privilegies for users who are members of these teams, which are inside above organization

{% hint style="warning" %}
In order to retrieve organization information from github, **user:read** and **read:org** scopes must be included
{% endhint %}

{% hint style="danger" %}
**`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.
{% endhint %}

{% hint style="warning" %}
**`admin-groups` is a case-insensitive substring match against team names.** `admin-groups: [admins]` matches `team-admins`, `admins-readonly`, `data-admins` — every member of every such team is promoted to ADMIN. Use long, full-team-name tokens (for example `odd-platform-admins-team`) so no substring of an unrelated team accidentally matches. A platform-side fix to use exact equality is tracked upstream.
{% endhint %}

{% hint style="warning" %}
**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.
{% endhint %}

{% hint style="warning" %}
**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.
{% endhint %}

{% tabs %}
{% tab title="YAML" %}

```yaml
auth:
    type: OAUTH2
    oauth2:
        client:
            github:
                provider: github
                client-id: {client_id}
                client-secret: {client_secret}
                scope: user:read,read:org
                redirect-uri: {host}/login/oauth2/code/github
                client-name: Github
                authorization-uri: https://github.com/login/oauth/authorize
                token-uri: https://github.com/login/oauth/access_token
                user-info-uri: https://api.github.com/user
                user-name-attribute: login
                organization-name: my-cool-org
                admin-groups: admin
                admin-attribute: login
                admin-principals: john,david
```

{% endtab %}

{% tab title="Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_OAUTH2_CLIENT_GITHUB_PROVIDER=github
AUTH_OAUTH2_CLIENT_GITHUB_CLIENT_ID={client_id}
AUTH_OAUTH2_CLIENT_GITHUB_CLIENT_SECRET={client_secret}
AUTH_OAUTH2_CLIENT_GITHUB_SCOPE=user:read,read:org
AUTH_OAUTH2_CLIENT_GITHUB_REDIRECT_URI={host}/login/oauth2/code/github
AUTH_OAUTH2_CLIENT_GITHUB_CLIENT_NAME=Github
AUTH_OAUTH2_CLIENT_GITHUB_AUTHORIZATION_URI=https://github.com/login/oauth/authorize
AUTH_OAUTH2_CLIENT_GITHUB_TOKEN_URI=https://github.com/login/oauth/access_token
AUTH_OAUTH2_CLIENT_GITHUB_USER_INFO_URI=https://api.github.com/user
AUTH_OAUTH2_CLIENT_GITHUB_USER_NAME_ATTRIBUTE=login
AUTH_OAUTH2_CLIENT_GITHUB_ORGANIZATION_NAME=my-cool-org
AUTH_OAUTH2_CLIENT_GITHUB_ADMIN_GROUPS=admin
AUTH_OAUTH2_CLIENT_GITHUB_ADMIN_ATTRIBUTE=login
AUTH_OAUTH2_CLIENT_GITHUB_ADMIN_PRINCIPALS=john,david
```

{% endtab %}
{% endtabs %}

#### 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.

{% hint style="danger" %}
**`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.
{% endhint %}

{% tabs %}
{% tab title="YAML" %}

```yaml
auth:
    type: OAUTH2
    oauth2:
        client:
            google:
                provider: google
                client-id: {client_id}
                client-secret: {client_secret}
                scope: openid,profile,email
                redirect-uri: {host}/login/oauth2/code/google
                client-name: Google
                issuer-uri: https://accounts.google.com
                user-name-attribute: name
                admin-attribute: email
                admin-principals: john@odd.com,david@odd.com
                allowed-domain: odd.com
```

{% endtab %}

{% tab title="Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_OAUTH2_CLIENT_GOOGLE_PROVIDER=google
AUTH_OAUTH2_CLIENT_GOOGLE_CLIENT_ID={client_id}
AUTH_OAUTH2_CLIENT_GOOGLE_CLIENT_SECRET={client_secret}
AUTH_OAUTH2_CLIENT_GOOGLE_SCOPE=openid,profile,email
AUTH_OAUTH2_CLIENT_GOOGLE_REDIRECT_URI={host}/login/oauth2/code/google
AUTH_OAUTH2_CLIENT_GOOGLE_CLIENT_NAME=Google
AUTH_OAUTH2_CLIENT_GOOGLE_ISSUER_URI=https://accounts.google.com
AUTH_OAUTH2_CLIENT_GOOGLE_USER_NAME_ATTRIBUTE=name
AUTH_OAUTH2_CLIENT_GOOGLE_ADMIN_ATTRIBUTE=email
AUTH_OAUTH2_CLIENT_GOOGLE_ADMIN_PRINCIPALS=john@odd.com,david@odd.com
AUTH_OAUTH2_CLIENT_GOOGLE_ALLOWED_DOMAIN=odd.com
```

{% endtab %}
{% endtabs %}

#### 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:

1. Go to **Azure Active Directory** → **App registrations** → **New registration**.
2. Choose the supported account types (single-tenant vs. multi-tenant) that match the deployment.
3. Add a **Web** redirect URI: `{host}/login/oauth2/code/azure`.
4. Under **Certificates & secrets**, generate a client secret and store its value — it cannot be viewed again later.
5. Under **API permissions**, add the following Microsoft Graph **delegated** permissions and grant admin consent: `openid`, `offline_access`, `User.Read`. Add `email` and `profile` as well if the `profile`/`email` scopes are requested below.
6. (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 the `roles` claim of the ID token, which is what ODD Platform reads by default.
   * **Azure AD security groups** — under **Token configuration** → **Add groups claim**, include the `groups` claim in the ID token. You must also set `groups-claim: groups` in the ODD configuration (see the note under `admin-groups` below), because the Azure handler reads from `roles` unless told otherwise.
7. 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).

{% tabs %}
{% tab title="YAML" %}

```yaml
auth:
    type: OAUTH2
    oauth2:
        client:
            azure:
                provider: azure
                client-id: {azure_client_id}
                azure-tenant-id: {azure_tenant_id}
                client-secret: {azure_client_secret}
                client-name: Azure AD
                scope:
                  - openid
                  - offline_access
                  - profile
                  - email
                  - https://graph.microsoft.com/User.Read
                redirect-uri: {host}/login/oauth2/code/azure
                issuer-uri: https://login.microsoftonline.com/{azure_tenant_id}/v2.0
                jwk-set-uri: https://login.microsoftonline.com/{azure_tenant_id}/discovery/v2.0/keys
                user-info-uri: https://graph.microsoft.com/oidc/userinfo
                logout-uri: https://login.microsoftonline.com/{azure_tenant_id}/oauth2/v2.0/logout
                user-name-attribute: email
                admin-attribute: email
                admin-principals:
                  - admin1@yourdomain.com
                  - admin2@yourdomain.com
                admin-groups:
                  - Admins
                  - Managers
```

{% endtab %}

{% tab title="Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_OAUTH2_CLIENT_AZURE_PROVIDER=azure
AUTH_OAUTH2_CLIENT_AZURE_CLIENT_ID={azure_client_id}
AUTH_OAUTH2_CLIENT_AZURE_AZURE_TENANT_ID={azure_tenant_id}
AUTH_OAUTH2_CLIENT_AZURE_CLIENT_SECRET={azure_client_secret}
AUTH_OAUTH2_CLIENT_AZURE_CLIENT_NAME=Azure AD
AUTH_OAUTH2_CLIENT_AZURE_SCOPE=openid,offline_access,profile,email,https://graph.microsoft.com/User.Read
AUTH_OAUTH2_CLIENT_AZURE_REDIRECT_URI={host}/login/oauth2/code/azure
AUTH_OAUTH2_CLIENT_AZURE_ISSUER_URI=https://login.microsoftonline.com/{azure_tenant_id}/v2.0
AUTH_OAUTH2_CLIENT_AZURE_JWK_SET_URI=https://login.microsoftonline.com/{azure_tenant_id}/discovery/v2.0/keys
AUTH_OAUTH2_CLIENT_AZURE_USER_INFO_URI=https://graph.microsoft.com/oidc/userinfo
AUTH_OAUTH2_CLIENT_AZURE_LOGOUT_URI=https://login.microsoftonline.com/{azure_tenant_id}/oauth2/v2.0/logout
AUTH_OAUTH2_CLIENT_AZURE_USER_NAME_ATTRIBUTE=email
AUTH_OAUTH2_CLIENT_AZURE_ADMIN_ATTRIBUTE=email
AUTH_OAUTH2_CLIENT_AZURE_ADMIN_PRINCIPALS=admin1@yourdomain.com,admin2@yourdomain.com
AUTH_OAUTH2_CLIENT_AZURE_ADMIN_GROUPS=Admins,Managers
```

{% endtab %}
{% endtabs %}

**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.

{% tabs %}
{% tab title="YAML" %}

```yaml
auth:
    type: OAUTH2
    oauth2:
        client:
            azure:
                provider: azure
                client-id: {azure_client_id}
                azure-tenant-id: {azure_tenant_id}
                client-secret: {azure_client_secret}
                client-name: Azure AD
                scope:
                  - openid
                  - offline_access
                  - profile
                  - email
                  - https://graph.microsoft.com/User.Read
                redirect-uri: {host}/login/oauth2/code/azure
                authorization-uri: https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize
                token-uri: https://login.microsoftonline.com/organizations/oauth2/v2.0/token
                jwk-set-uri: https://login.microsoftonline.com/organizations/discovery/v2.0/keys
                user-info-uri: https://graph.microsoft.com/oidc/userinfo
                logout-uri: https://login.microsoftonline.com/organizations/oauth2/v2.0/logout
                user-name-attribute: email
                admin-attribute: email
                admin-principals:
                  - admin1@yourdomain.com
                  - admin2@yourdomain.com
```

{% endtab %}

{% tab title="Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_OAUTH2_CLIENT_AZURE_PROVIDER=azure
AUTH_OAUTH2_CLIENT_AZURE_CLIENT_ID={azure_client_id}
AUTH_OAUTH2_CLIENT_AZURE_AZURE_TENANT_ID={azure_tenant_id}
AUTH_OAUTH2_CLIENT_AZURE_CLIENT_SECRET={azure_client_secret}
AUTH_OAUTH2_CLIENT_AZURE_CLIENT_NAME=Azure AD
AUTH_OAUTH2_CLIENT_AZURE_SCOPE=openid,offline_access,profile,email,https://graph.microsoft.com/User.Read
AUTH_OAUTH2_CLIENT_AZURE_REDIRECT_URI={host}/login/oauth2/code/azure
AUTH_OAUTH2_CLIENT_AZURE_AUTHORIZATION_URI=https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize
AUTH_OAUTH2_CLIENT_AZURE_TOKEN_URI=https://login.microsoftonline.com/organizations/oauth2/v2.0/token
AUTH_OAUTH2_CLIENT_AZURE_JWK_SET_URI=https://login.microsoftonline.com/organizations/discovery/v2.0/keys
AUTH_OAUTH2_CLIENT_AZURE_USER_INFO_URI=https://graph.microsoft.com/oidc/userinfo
AUTH_OAUTH2_CLIENT_AZURE_LOGOUT_URI=https://login.microsoftonline.com/organizations/oauth2/v2.0/logout
AUTH_OAUTH2_CLIENT_AZURE_USER_NAME_ATTRIBUTE=email
AUTH_OAUTH2_CLIENT_AZURE_ADMIN_ATTRIBUTE=email
AUTH_OAUTH2_CLIENT_AZURE_ADMIN_PRINCIPALS=admin1@yourdomain.com,admin2@yourdomain.com
```

{% endtab %}
{% endtabs %}

#### Notes:

* Ensure the `openid` scope is always included, as it is mandatory for OIDC.
* The `azure-tenant-id` should correspond to the Azure AD tenant that owns the app registration.
* The `jwk-set-uri` is mandatory for Azure to function correctly with ODD Platform when the discovery endpoint is unreachable.
* `logout-uri` is the Azure AD OpenID Connect logout endpoint — `https://login.microsoftonline.com/{azure_tenant_id}/oauth2/v2.0/logout` for single-tenant and `https://login.microsoftonline.com/organizations/oauth2/v2.0/logout` (or the `common` variant 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.

{% hint style="warning" %}
`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.
{% endhint %}

* `admin-principals` is the list of user identifiers (matched against the `admin-attribute` claim, `email` in the examples above) that will be granted the ADMIN role on login.
* `admin-groups` grants 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 the `roles` claim**, which Azure AD populates from **App roles** assigned to the user. The values in `admin-groups` must match the app role `value` fields, not Azure AD display names.
  * To grant ADMIN based on Azure AD **security group** membership instead, set `groups-claim: groups` in the Azure configuration and add the `groups` claim to the ID token in Azure (**Token configuration** → **Add groups claim**). The `admin-groups` list then matches against values emitted in the `groups` claim (group object IDs by default — switch the Azure claim output to `Group Name` in 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 `email` attribute, 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`, and `logout-uri` correspond to your tenant ID.
* If logout returns a 500 error or never completes, verify that `logout-uri` is set and matches your single-tenant / multi-tenant choice. An unset `logout-uri` triggers a `NullPointerException` in the Azure logout handler; the browser sees a 500 while the user remains signed in on both sides.
* If `admin-groups` has no effect, check which claim is being read:
  * **App Roles (default)** — decode the ID token at `jwt.ms` or equivalent; confirm the `roles` claim is present and contains the app-role `value` fields that your `admin-groups` list references. If the `roles` claim 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: groups` is set in the ODD configuration (without this, `admin-groups` is matched against `roles`, not `groups`), and that the `groups` claim 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.

{% hint style="warning" %}
**Okta and Keycloak (and any other "Custom OIDC" provider) have no admin-detection path.** These providers fall through the generic `CustomOIDCUserHandler` which does not consult `admin-groups`, `admin-principals`, or any provider-specific claim for ADMIN promotion. Every authenticated user is granted the `USER` role at login; promote individual operators to ADMIN by binding them to an Owner that holds an ADMIN-equivalent [Role](/configuration-and-deployment/enable-security/authorization/roles.md) bundle through the Management UI. The provider list at the top of this page includes Okta and Keycloak because OIDC discovery and login work — admin-detection parity with Cognito / GitHub / Azure / Google does not exist today.
{% endhint %}

{% tabs %}
{% tab title="OKTA YAML" %}

```yaml
auth:
    type: OAUTH2
    oauth2:
        client:
            okta:
                provider: okta
                client-id: {client_id}
                client-secret: {client_secret}
                scope: openid,profile,email
                redirect-uri: {host}/login/oauth2/code/okta
                client-name: Okta
                issuer-uri: {okta_issuer_uri}
                user-name-attribute: email
                admin-attribute: email
                admin-principals: john@odd.com,david@odd.com
```

{% endtab %}

{% tab title="OKTA Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_OAUTH2_CLIENT_OKTA_PROVIDER=okta
AUTH_OAUTH2_CLIENT_OKTA_CLIENT_ID={client_id}
AUTH_OAUTH2_CLIENT_OKTA_CLIENT_SECRET={client_secret}
AUTH_OAUTH2_CLIENT_OKTA_SCOPE=openid,profile,email
AUTH_OAUTH2_CLIENT_OKTA_REDIRECT_URI={host}/login/oauth2/code/okta
AUTH_OAUTH2_CLIENT_OKTA_CLIENT_NAME=Okta
AUTH_OAUTH2_CLIENT_OKTA_ISSUER_URI={issuer_uri}
AUTH_OAUTH2_CLIENT_OKTA_USER_NAME_ATTRIBUTE=email
AUTH_OAUTH2_CLIENT_OKTA_ADMIN_ATTRIBUTE=email
AUTH_OAUTH2_CLIENT_OKTA_ADMIN_PRINCIPALS=john@odd.com,david@odd.com
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Keycloak YAML" %}

```yaml
auth:
    type: OAUTH2
    oauth2:
        client:
            keycloak:
                provider: keycloak
                client-id: {client_id}
                client-secret: {client_secret}
                scope: openid,profile,email
                redirect-uri: {host}/login/oauth2/code/keycloak
                client-name: Keycloak
                issuer-uri: {keycloak_issuer_uri}
                user-name-attribute: preferred_username
                admin-attribute: preferred_username
                admin-principals: john,david
```

{% endtab %}

{% tab title="Keycloak Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_OAUTH2_CLIENT_KEYCLOAK_PROVIDER=keycloak
AUTH_OAUTH2_CLIENT_KEYCLOAK_CLIENT_ID={client_id}
AUTH_OAUTH2_CLIENT_KEYCLOAK_CLIENT_SECRET={client_secret}
AUTH_OAUTH2_CLIENT_KEYCLOAK_SCOPE=openid,profile,email
AUTH_OAUTH2_CLIENT_KEYCLOAK_REDIRECT_URI={host}/login/oauth2/code/keycloak
AUTH_OAUTH2_CLIENT_KEYCLOAK_CLIENT_NAME=Keycloak
AUTH_OAUTH2_CLIENT_KEYCLOAK_ISSUER_URI={issuer_uri}
AUTH_OAUTH2_CLIENT_KEYCLOAK_USER_NAME_ATTRIBUTE=preferred_username
AUTH_OAUTH2_CLIENT_KEYCLOAK_ADMIN_ATTRIBUTE=preferred_username
AUTH_OAUTH2_CLIENT_KEYCLOAK_ADMIN_PRINCIPALS=john,david
```

{% endtab %}
{% endtabs %}

#### Keycloak with PKCE

Keycloak realms can be configured to require [Proof Key for Code Exchange (PKCE, RFC 7636)](https://datatracker.ietf.org/doc/html/rfc7636) 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.

{% tabs %}
{% tab title="YAML" %}

```yaml
auth:
    type: OAUTH2
    oauth2:
        client:
            keycloak:
                provider: keycloak
                client-id: {client_id}
                pkce: true
                scope: openid,profile,email
                redirect-uri: {host}/login/oauth2/code/keycloak
                client-name: Keycloak
                issuer-uri: {keycloak_issuer_uri}
                user-name-attribute: preferred_username
                admin-attribute: preferred_username
                admin-principals: john,david
```

{% endtab %}

{% tab title="Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_OAUTH2_CLIENT_KEYCLOAK_PROVIDER=keycloak
AUTH_OAUTH2_CLIENT_KEYCLOAK_CLIENT_ID={client_id}
AUTH_OAUTH2_CLIENT_KEYCLOAK_PKCE=true
AUTH_OAUTH2_CLIENT_KEYCLOAK_SCOPE=openid,profile,email
AUTH_OAUTH2_CLIENT_KEYCLOAK_REDIRECT_URI={host}/login/oauth2/code/keycloak
AUTH_OAUTH2_CLIENT_KEYCLOAK_CLIENT_NAME=Keycloak
AUTH_OAUTH2_CLIENT_KEYCLOAK_ISSUER_URI={issuer_uri}
AUTH_OAUTH2_CLIENT_KEYCLOAK_USER_NAME_ATTRIBUTE=preferred_username
AUTH_OAUTH2_CLIENT_KEYCLOAK_ADMIN_ATTRIBUTE=preferred_username
AUTH_OAUTH2_CLIENT_KEYCLOAK_ADMIN_PRINCIPALS=john,david
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**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.
{% endhint %}

{% hint style="info" %}
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.
{% endhint %}

## 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.

| Provider                          | Local session invalidated | IdP access token revoked                                                                                                                                 | If not revoked, residual validity                                                            |
| --------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| **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                                          | —                                                                                            |
| **Google**                        | yes                       | yes — the handler `POST`s 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.

{% hint style="warning" %}
**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.
{% endhint %}

{% hint style="warning" %}
**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.
{% endhint %}

## 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/configuration-and-deployment/enable-security/authentication/oauth2-oidc.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
