> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/federation/oauth-and-oidc.md).

# OAuth 2.0 and OpenID Connect

Register an OAuth 2.0 client so OpenIAM becomes its authorization server and OpenID Connect provider, then fence it so entitlement actually applies.

## What this is, and when you need it

An OAuth client in OpenIAM is an **authentication provider** row of type `OAUTH_CLIENT`, backed by a resource and a set of provider attributes. Registering one turns OpenIAM into the authorization server and OpenID Connect provider for that application: OpenIAM issues the code, the access token and — when the request carries the `openid` scope — the ID token.

From 2026.10.2 one kind of client registers itself: a Model Context Protocol (MCP) client such as Claude, which identifies itself by a Client ID Metadata Document. You do not create a row for it; you decide which origins may do so. See [Let an MCP client register itself](#let-an-mcp-client-register-itself).

This is not the same task as making OpenIAM an OAuth/OIDC **consumer** (logging users in through Google, Apple, Criipto or Duo). Those are separate provider types; see [Social sign-in](/administration-guide/security/social-authentication.md) and [Authentication providers and MFA](/administration-guide/security/authentication-providers-and-mfa.md).

## At a glance

|                          |                                                                                                                                                                                                           |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | One registered client whose discovery document answers, that completes an authorization-code-with-PKCE exchange, issues a token with the lifetime you chose, and refuses a user who is not entitled to it |
| **Where**                | **Access Control → Authentication Providers → New** (`/webconsole/newAuthProvider`), type **OAUTH\_CLIENT**                                                                                               |
| **You need first**       | The application's exact redirect URI including scheme and path, a decision on the grant flow (one client row supports exactly one), and the managed system whose identities this client authenticates     |
| **Takes**                | Two hours for a first client, most of it the token exchange and the entitlement test                                                                                                                      |
| **Reversible**           | Yes — deleting the provider row removes the client and orphans nothing else, but any token already issued stays valid until its stored expiry passes. Revocation is per token                             |

## Before you begin

Two shipped behaviours will bite you in this order, and both are stated precisely further down. **Token Expiration is in minutes and is multiplied by 60 again if you leave it blank**, which produces a 60-hour access token — see [The token lifetime trap](#the-token-lifetime-trap). And **OAuth client entitlement enforcement is off out of the box**, so any authenticated user can obtain a token from any registered client until you address two separate settings — see [Fence the client](#fence-the-client-oauth-entitlement-enforcement-is-off-out-of-the-box).

One prerequisite is easy to get wrong. For the resource-owner-password flow OpenIAM looks the username up **only** within the client's own managed system; an identity that exists on another managed system is reported as a bad password, not as a missing user. Pick the managed system deliberately.

Redirect-URI matching is asymmetric between the two endpoints, so record the URI exactly once and use that string everywhere: the authorize endpoint matches case-insensitively against the registered list after stripping everything from the first `?` onward, and the token endpoint compares byte for byte.

From 2026.10.2 a client on the PKCE flow that has no client secret must use the `S256` challenge method, and every parameter on the redirect back to the client is URL-encoded; both are stated in [OAuth endpoints, grants, scopes and signing](/administration-guide/federation/oauth-and-oidc/oauth-and-oidc-reference.md).

## Register an OAuth client

This is a confidential client using authorization code with PKCE — the flow to start from for a web application, and the only sensible answer for a single-page or mobile client.

{% stepper %}
{% step %}

### Create the provider row

**Access Control → Authentication Providers → New**, provider type **OAUTH\_CLIENT**. Give it a name and select the managed system whose identities this client authenticates.
{% endstep %}

{% step %}

### Fill the client attributes

The left column is the label on the form; the middle column is the `ATTRIBUTE_ID` you would filter on if you ever query the stored attributes.

| Field label                | Attribute ID                  | Value for this example                  |
| -------------------------- | ----------------------------- | --------------------------------------- |
| Client ID                  | `OAuthClientID`               | `<CLIENT_ID>`                           |
| Client Secret              | `OAuthClientSecret`           | `<CLIENT_SECRET>`                       |
| Redirect URL               | `OAuthRedirectUrl`            | `https://app.example.com/oidc/callback` |
| Authorization Grant Flow   | `OAuthAuthorizationGrantFlow` | `AUTHORIZATION_CODE_PKCE`               |
| OAuth Client (auth type)   | `OAuthClientAuthType`         | `BASIC_HTTP`                            |
| Token Expiration (minutes) | `OAuthTokenExpiration`        | `60`                                    |
| Use Refresh Token          | `OAuthUseRefreshToken`        | checked                                 |
| Signing Algorithm          | `JwtAlgorithm`                | `RS256`                                 |
| JWT Issuer                 | `JwtIssue`                    | `example-app`                           |
| Default Scopes             | `OAuthClientScopes`           | `openid`, `profile`, `email`            |
| Skip Scope Approval        | `OAuthClientSkipAllowDialog`  | checked, for a first-party app          |

**Do not leave Token Expiration blank.** `60` gives a one-hour token; blank gives 60 hours. Leaving **Default Scopes** empty makes the `scope` parameter mandatory on every authorization request.

Pick `RS256` or `RS512` rather than an HMAC algorithm for anything a third party validates: an HS-signed client publishes no key at the JWKS endpoint, because its only key is the shared secret, so only a party that already holds that secret can verify its tokens. Every value the drop-downs offer, and what each one changes, is in [OAuth endpoints, grants, scopes and signing](/administration-guide/federation/oauth-and-oidc/oauth-and-oidc-reference.md).
{% endstep %}

{% step %}

### Turn **Send ID Token as Access Token** off if this client calls UserInfo

With it on, the application is handed the ID token where it expects the access token, and everything that later resolves that token by its value fails. The UserInfo endpoint answers `invalid_request` with *"Provided token is not found"*. See [UserInfo only accepts the opaque access token](#userinfo-only-accepts-the-opaque-access-token). Use the setting only for a relying party that consumes the JWT directly and never calls a token-resolving endpoint.
{% endstep %}

{% step %}

### Save, wait the sweep, then confirm discovery

The provider cache refreshes on `org.openiam.oauth.sweep.time.ms`, shipped `72000` — roughly 72 seconds. Open the discovery URL in a browser **after** that, not immediately: until the sweep runs, the discovery URL for your new **JWT Issuer** returns the `No OAuth Provider with Issuer` failure and looks like a misconfiguration.

{% code overflow="wrap" %}

```
https://<OPENIAM_HOST>/idp/oauth2/<JWT_ISSUER>/.well-known/openid-configuration
```

{% endcode %}

`<JWT_ISSUER>` is the value of the client's **JWT Issuer** attribute, not the client ID. That URL is the first of the values you hand the application team. On 2026.10.1 and later the relying party can configure itself from the document, including its `jwks_uri`. On earlier releases that value is malformed — give them `https://<OPENIAM_HOST>/idp/oauth2/<JWT_ISSUER>/.well-known/jwks`, which is the path that actually answers.
{% endstep %}

{% step %}

### Sign in through the application once, end to end

Hand the application team the endpoint URLs, the client ID and secret, and the grant flow, and have them complete one real sign-in. Watch the audit log while they do: `AUTHORIZE` then `GET_TOKEN` is the pair that says the exchange completed.

Two constraints are yours to state, because both present as unexplained failures at their end and neither is visible on any screen. PKCE is mandatory on this flow, so a client configured for plain authorization code will be refused. And the redirect URI the application sends when it exchanges the code must be **byte-identical** to the one it sent to start the sign-in: OpenIAM compares those two exactly, including case, while it matches the registered list case-insensitively — so a URI accepted at the start of the flow can still be rejected at the end of it.

Authorization codes live five minutes, are single use, and the lifetime is not configurable.
{% endstep %}

{% step %}

### Fence the client

Out of the box, entitlement decides nothing. Do the four changes in [Fence the client](#fence-the-client-oauth-entitlement-enforcement-is-off-out-of-the-box) now, while you are still on a test client, and finish by confirming that an unentitled user is refused with `insufficient_rights`. Remember that the device-code grant returns before that check and is never denied by it.
{% endstep %}
{% endstepper %}

## Let an MCP client register itself

An MCP client such as Claude does not ask you for a client id and secret. It reads the IdP's tenant metadata document, sees that Client ID Metadata Documents are accepted, and sends the `https` URL of its own metadata document as `client_id`. The IdP fetches that document, under the guard described in [Client ID Metadata Documents](/administration-guide/federation/oauth-and-oidc/oauth-and-oidc-reference.md#client-id-metadata-documents), and creates the client row itself on the user's first sign-in. Your part is to decide who may do that. Deploying the OpenIAM MCP server itself, publishing it through the gateway and connecting Claude are in [Connect AI assistants through the MCP server](/administration-guide/federation/mcp-server.md).

{% stepper %}
{% step %}

### Confirm the tenant metadata document answers

Open it in a browser. It needs no client and no sweep.

{% code overflow="wrap" %}

```
https://<OPENIAM_HOST>/idp/oauth2/.well-known/openid-configuration
```

{% endcode %}

Check `issuer` reads `https://<OPENIAM_HOST>/idp/oauth2`, that `code_challenge_methods_supported` contains `S256`, that `token_endpoint_auth_methods_supported` contains `none`, and that `client_id_metadata_document_supported` is `true`. If the issuer shows an internal host name, set `org.openiam.oauth.issuer` in `idp.ui.properties` to the public value and restart the IdP.
{% endstep %}

{% step %}

### Decide which origins may register

`org.openiam.oauth.cimd.allowed.origins` in `idp.ui.properties` ships as `https://claude.ai`, which covers Claude's hosted apps and Claude Code. Add an origin per vendor, comma-separated, as scheme and host; set the property to an empty value to refuse every self-registration. Restart the IdP after a change.
{% endstep %}

{% step %}

### Make sure the MCP scopes exist and are entitled

A provisioned client is granted the scope names in `org.openiam.oauth.cimd.default.scopes`, the four `openiam:mcp:*:read` scopes the 2026.10.2 migration seeds. A user receives a scope only when entitled to its resource through a role, so check **Access Control → Resources** for the four scope resources and their role grants before the first user tries. A scope name the deployment does not have is skipped and logged, and the client then obtains no token until it exists.
{% endstep %}

{% step %}

### Have a user connect once

The user adds the OpenIAM MCP server to Claude and signs in. The consent page names the client, shows the host its metadata document came from, lists the MCP scopes, and warns when every callback address is local, which is normal for Claude Code. After **Allow**, the client appears under **Access Control → Authentication Providers** as `<client name> (<host>)`, type **OAUTH\_CLIENT**, grant flow **Proof Key for Code Exchange (PKCE)**, no client secret. The audit log shows `AUTHORIZE` and `GET_TOKEN` as for any client, and `CREATE_OAUTH_PROVIDER` for the row itself. **Deny** sends the user back to the client with `error=access_denied`; nothing is stored and the client may ask again.
{% endstep %}

{% step %}

### Disable it when you need to

**Delete the provider row** to stop that client; every token it holds is revoked the normal way. The next sign-in from the same `client_id` provisions a fresh row, so to keep it out for good also remove its origin from `org.openiam.oauth.cimd.allowed.origins`. Removing the origin alone stops new registrations and leaves rows already provisioned in place.
{% endstep %}
{% endstepper %}

## Verification

| What to check                        | How                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The tenant metadata document answers | `https://<OPENIAM_HOST>/idp/oauth2/.well-known/openid-configuration` returns 200 with the issuer, `S256`, `none` and `client_id_metadata_document_supported: true` (2026.10.2 and later). It does not depend on any client row                                                                                                                                                         |
| The client is live in the cache      | The discovery URL returns 200. A failure carrying `No OAuth Provider with Issuer: <x>` means the **JWT Issuer** value does not match what you requested, or the 72-second sweep has not run yet                                                                                                                                                                                        |
| The signing key is usable            | Open the JWKS URL, `https://<OPENIAM_HOST>/idp/oauth2/<JWT_ISSUER>/.well-known/jwks`. For an RSA client it publishes one public key marked for signature use whose algorithm equals the one you set on **Signing Algorithm**, and nothing private. For an HMAC client it returns an empty key set by design; verify those tokens with the shared secret instead                        |
| The lifetime is what you set         | The lifetime the application is told about equals your configured minutes × 60 seconds. `216000` — sixty hours — means **Token Expiration** is blank                                                                                                                                                                                                                                   |
| Entitlement is actually enforced     | Have a sign-in attempted as a user who is not entitled to the client's resource. It must fail with `insufficient_rights` and the localized `AUTH_USER_NOT_PROVISIONED` message. If it succeeds, one of the two bypasses is still in place                                                                                                                                              |
| Revocation took effect               | The revoked token stops being accepted, and `REVOKE_OAUTH_TOKEN` is audited. An ID token is blacklisted for the remainder of its original lifetime rather than deleted, so the token string still exists — only the outcome of presenting it changes                                                                                                                                   |
| Audit records exist                  | `AUTHORIZE`, `GET_TOKEN`, `REVOKE_OAUTH_TOKEN` and `AUTHORIZE_PUBLIC_APPLICATION`. Provider changes record `CREATE_OAUTH_PROVIDER` / `MODIFY_OAUTH_PROVIDER`. Every one of these events carries the authentication provider it resolved to, which is how you tell two clients apart. See [Audit actions and retention](/administration-guide/reference/audit-actions-and-retention.md) |

## When it goes wrong

| Symptom                                                                                                    | Cause                                                                                                                                                                                                                                                                                                               | Fix                                                                                                                                                                                                                                                                                                             |
| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unsupported_response_type` / `Client doesn't support authorization_code flow`                             | `OAuthAuthorizationGrantFlow` is not `AUTHORIZATION_CODE` or `AUTHORIZATION_CODE_PKCE`                                                                                                                                                                                                                              | Change **Authorization Grant Flow** on the client row, then wait out the 72-second sweep                                                                                                                                                                                                                        |
| `invalid_request` / `Redirect URL does not mach to initial one.`                                           | The redirect URI the application sent when it exchanged the code differs from the one it started with by even one character                                                                                                                                                                                         | OpenIAM compares those two exactly; the application must send one identical string, including case                                                                                                                                                                                                              |
| `invalid_request` / `The Redirect URI for Client ID {0} is not registered`                                 | The URI the application started the sign-in with, minus any query string, is not in **Redirect URL** on the client row                                                                                                                                                                                              | Add the exact URI; matching against the registered list is case-insensitive but the path must match                                                                                                                                                                                                             |
| `invalid_grant` / `Code parameter is expired`                                                              | Authorization codes live five minutes, and the lifetime is not configurable                                                                                                                                                                                                                                         | The application has to shorten the round trip; the code is single-use as well as short-lived                                                                                                                                                                                                                    |
| `invalid_request` / `Code verifier is wrong…`                                                              | The PKCE verifier the application generated does not meet RFC 7636 §4.1                                                                                                                                                                                                                                             | Nothing on the client row affects this. The application's own OAuth library owns it                                                                                                                                                                                                                             |
| `invalid_request` / `This client must use the S256 code_challenge_method`                                  | The client has no secret on the PKCE flow and sent `code_challenge_method=plain`, or none (2026.10.2 and later)                                                                                                                                                                                                     | Have the application use `S256`, or set `org.openiam.oauth.pkce.plain.allowed=true` to keep the old behaviour                                                                                                                                                                                                   |
| `invalid_grant` / `This client must use the S256 code_challenge_method` at the token endpoint              | The code was issued with a `plain` challenge to a client that must use `S256`, typically one issued before the property changed                                                                                                                                                                                     | The client starts a new sign-in with `S256`                                                                                                                                                                                                                                                                     |
| `invalid_client` / `The client … could not be registered from its metadata document: …`                    | The `client_id` is an https URL whose origin is not in `org.openiam.oauth.cimd.allowed.origins`, whose document could not be fetched or failed validation, or whose provisioning the backend refused; the rest of the message names the rule                                                                        | Add the origin, or fix the document the vendor publishes. The IdP log carries the same reason with the URL. A refused provisioning that mentions `Client Secret` means the backend predates 2026.10.2                                                                                                           |
| `invalid_target` on the redirect, or from the token endpoint                                               | The request's `resource` names none of the resources configured under `org.openiam.oauth.resources` (2026.10.2 and later)                                                                                                                                                                                           | Set `org.openiam.oauth.resource.mcp.url` to the URL the client sends, or have the client send the one the IdP derives, `https://<OPENIAM_HOST>/mcp`                                                                                                                                                             |
| `invalid_scope` on the redirect although the scopes exist                                                  | The request named a `resource` and none of the requested scopes is in that resource's `org.openiam.oauth.resource.<name>.scopes`                                                                                                                                                                                    | Align the two lists                                                                                                                                                                                                                                                                                             |
| `invalid_grant` / `Refresh token was not issued to this client`                                            | The application presented a refresh token that another client obtained (2026.10.2 and later)                                                                                                                                                                                                                        | Each client refreshes with its own token; this is the check that stops one client redeeming another's                                                                                                                                                                                                           |
| `invalid_grant` / `Refresh token is unknown, expired or already redeemed`                                  | The refresh token was used before, or never existed                                                                                                                                                                                                                                                                 | Since 2026.10.1 a refresh token is single use; the application must keep the token the last refresh returned                                                                                                                                                                                                    |
| `/idp/oauth2/token/refresh` answers 400 `invalid_request`                                                  | `org.openiam.oauth.token.refresh.legacy.enabled` is `false`                                                                                                                                                                                                                                                         | Have the application refresh through `POST /idp/oauth2/token` with `grant_type=refresh_token`, or set the property back to `true`                                                                                                                                                                               |
| Claude asks for a client id and secret instead of signing in                                               | The tenant metadata document does not advertise `client_id_metadata_document_supported: true` together with `none`, usually because `org.openiam.oauth.cimd.allowed.origins` is empty                                                                                                                               | Set the origin and restart the IdP; check the document in a browser                                                                                                                                                                                                                                             |
| `unauthorized_client` / `Client ID / Client secret don't match`                                            | The secret does not match, or the application presents it the other way round from what the **OAuth Client** field says                                                                                                                                                                                             | Align the two. **Basic Authentication** and **Request body** are the only two choices, and the client must use the one on the row                                                                                                                                                                               |
| A PKCE client completes a sign-in with no secret at all                                                    | Under `AUTHORIZATION_CODE_PKCE` OpenIAM skips secret validation entirely when the client supplies none — deliberate, per RFC 7636 public clients                                                                                                                                                                    | Do not treat **Client Secret** as a control on a PKCE client. Have the application team confirm it for you by omitting it                                                                                                                                                                                       |
| `insufficient_rights`                                                                                      | Entitlement enforcement is on and the user is not entitled to the client's resource                                                                                                                                                                                                                                 | Entitle the user — [Fence the client](#fence-the-client-oauth-entitlement-enforcement-is-off-out-of-the-box)                                                                                                                                                                                                    |
| Any authenticated user can get a token                                                                     | `org.openiam.authorization.oauth.disabled` ships `true`, and every `OAUTH_CLIENT` resource was set public                                                                                                                                                                                                           | Both have to be addressed. Same section                                                                                                                                                                                                                                                                         |
| `invalid_request` / `Client does not support refresh authorization token operation`                        | `OAuthUseRefreshToken` is off on the client row                                                                                                                                                                                                                                                                     | Enable **Use Refresh Token**                                                                                                                                                                                                                                                                                    |
| `invalid_request` / `Client doesn't support refresh_token flow, because it implements Implicit Grand flow` | **Authorization Grant Flow** is `IMPLICIT`                                                                                                                                                                                                                                                                          | Implicit cannot refresh; move to `AUTHORIZATION_CODE_PKCE`                                                                                                                                                                                                                                                      |
| The JWKS endpoint fails with `does not have a valid signing algorithm`                                     | `JwtAlgorithm` is unset, or is neither HMAC nor RSA                                                                                                                                                                                                                                                                 | Set **Signing Algorithm** to one of `HS256`, `HS512`, `RS256`, `RS512`                                                                                                                                                                                                                                          |
| Relying party cannot fetch keys from `jwks_uri`                                                            | On releases before 2026.10.1 the advertised `jwks_uri` is prefixed with the raw issuer name                                                                                                                                                                                                                         | Upgrade, or configure the relying party with the literal JWKS URL — [Every endpoint](/administration-guide/federation/oauth-and-oidc/oauth-and-oidc-reference.md#every-endpoint)                                                                                                                                |
| `/token/info` answers `401` with `unauthorized_client`                                                     | `org.openiam.oauth.token.info.public` is `false` and the caller did not authenticate as a registered OAuth client                                                                                                                                                                                                   | Have the caller send its client credentials the way it does at `/token`, in a Basic header or in the body, or set the property back to `true`                                                                                                                                                                   |
| No ID token when the application refreshes                                                                 | Before 2026.10.2 OpenIAM read the requested scopes comma-separated on a refresh and space-separated on the initial sign-in, so a client that reused one string got no ID token back                                                                                                                                 | Upgrade: from 2026.10.2 both separators are accepted on a refresh. On earlier releases tell the application team to send the scopes comma-separated on refresh                                                                                                                                                  |
| The application cannot reach `/idp/oauth2/refresh`                                                         | `org.openiam.oauth.refresh.url` holds that path but nothing serves it                                                                                                                                                                                                                                               | Hand over `/idp/oauth2/token/refresh` instead — that is the path that answers                                                                                                                                                                                                                                   |
| Token still accepted after logout                                                                          | An access token is deleted on revocation, but an ID token is only blacklisted, and the two are revoked by different means                                                                                                                                                                                           | Have the application revoke both. The blacklist lives in the shared cache, so it does not survive that cache being emptied — [What termination does, and does not, invalidate](/administration-guide/security/session-management/session-management-reference.md#what-termination-does-and-does-not-invalidate) |
| An OAuth-protected path answers 404 while the log says `oauth2 error insufficient_scope. code: 403`        | The 404 is misdirection. The URI pattern that matched belongs to a resource that is not among the token's authorized scopes, so OpenIAM rejects the request before it reaches the application. The same `insufficient_scope` is raised when no URI pattern matched at all, which is why the two symptoms look alike | Add the path as a URI pattern on the scope resource the client is entitled to. The rejection is `insufficient_scope`, not "not found" — read the log line rather than the status code                                                                                                                           |
| `insufficient_scope` / `Unknown URI pattern` on an OAuth-protected path                                    | No URI pattern matched the request at all, so there is no resource to compare against the token's scopes                                                                                                                                                                                                            | Add the pattern to the content provider — [Content providers and URI patterns](/administration-guide/access/content-providers-and-uri-patterns.md)                                                                                                                                                              |
| `id_token` and `access_token` are byte-identical, and UserInfo returns `Provided token is not found`       | **Send ID Token as Access Token** is on                                                                                                                                                                                                                                                                             | [UserInfo only accepts the opaque access token](#userinfo-only-accepts-the-opaque-access-token)                                                                                                                                                                                                                 |
| A `client_credentials` token has an unexpected subject                                                     | That grant issues a token whose subject is the literal user ID `"0001"` rather than a real identity                                                                                                                                                                                                                 | Expected, and not configurable. Do not build an authorization decision at the application on that subject                                                                                                                                                                                                       |
| Change saved, behaviour unchanged                                                                          | The OAuth provider sweep is `org.openiam.oauth.sweep.time.ms` = `72000` ms, not the 60-second one                                                                                                                                                                                                                   | Wait 72 seconds                                                                                                                                                                                                                                                                                                 |

## Roll it back

Deleting the provider row removes the client and orphans nothing else, but any token already issued stays valid until its stored expiry passes — revocation is per token, so revoke the ones you know about.

If you enabled entitlement enforcement and locked an application out, the fastest reversal is to set `org.openiam.authorization.oauth.disabled` back to `true`; that restores the shipped behaviour for **every** client at once, which is why it is a reversal and not a fix. Follow [Change configuration safely](/administration-guide/start/change-safely.md) before editing a client that is carrying production traffic.

## Fence the client: OAuth entitlement enforcement is off out of the box

Two things happened in 4.1.8.0, and both point the same way. Every existing `OAUTH_CLIENT` resource was set public, and `org.openiam.authorization.oauth.disabled` was introduced with the shipped value `true`, category `System`, labelled *Disable OAuth Client Authorization (backward compatibility — previously all OAuth clients were available to all users)*. No later release changes that value.

Both the authorize and the token endpoint apply the same three-part test before issuing anything, and a client passes — is allowed through — if **any** of the three is satisfied:

1. `org.openiam.authorization.oauth.disabled` is `true`; or
2. the client's resource is public; or
3. the user is entitled to the client's resource, through any group or role.

Only when the property is `false` **and** the resource is not public does entitlement decide anything. A user who fails all three gets `insufficient_rights`.

{% hint style="danger" %}
**Out of the box, any authenticated user can obtain a token from any registered OAuth client.** The property ships `true`, which settles the test before entitlement is ever consulted; and even if you change the property, every `OAUTH_CLIENT` resource that existed at 4.1.8.0 is public, which settles it again. Assigning users to the client's resource has no effect until **both** are addressed.

Confirm the state of your own deployment before you assume it is fenced: read **Disable OAuth Client Authorization** in **Administration → System Configuration → System**, then open the client's resource and check the public flag. Both have to be off before an entitlement means anything.
{% endhint %}

Four changes turn enforcement on:

1. **Clear the global bypass.** **Administration → System Configuration → System**, set **Disable OAuth Client Authorization** to `false`.
2. **Make the client's resource non-public.** On **Access Control → Authentication Providers → → Resource**, clear the public flag. A public resource is exempt regardless of the property.
3. **Entitle the users, groups or roles that may use the client.** OpenIAM evaluates this with the same entitlement check it uses everywhere else, so normal group and role inheritance applies.
4. **Confirm the denial path.** Request a token as a user who is not entitled and expect `insufficient_rights`.

Two gaps survive all four. The **device-code grant is not subject to this check** and can never fail with `insufficient_rights`, so it is a hole in any fence you build this way. And every scope resource ships public — re-set public as recently as 4.2.1.0 — so **per-scope entitlement is not enforced in the shipped state either**; check the public flag on the scope resource before you design around it.

## The token lifetime trap

The field is labelled **Token Expiration (minutes)** — that label was corrected in 4.1.1.0, and older deployments may still show the earlier wording. The Webconsole validates it as a number between 1 and 999999, and whatever is in the field is multiplied by 60 to get seconds. When the field is **empty**, OpenIAM substitutes a fallback that is already expressed in seconds — `3600` — and then multiplies that by 60 as well.

{% hint style="warning" %}
**Leave Token Expiration blank and you ship a 60-hour access token.** The empty-field fallback of `3600` seconds is multiplied by 60 into **216,000 seconds — 60 hours, two and a half days**. Always set the field: `60` gives a one-hour token, `15` gives fifteen minutes.

The check takes one token exchange. `expires_in` in the token response is the number OpenIAM actually applied, so it should equal your value × 60. If it reads `216000`, the field is blank.
{% endhint %}

The same conversion is applied on refresh and on the device-code grant, so a blank field is long-lived everywhere, not only on the first token.

## UserInfo only accepts the opaque access token

The UserInfo endpoint looks the token it is presented with up by its literal value, and a miss comes back as `invalid_request` with *"Provided token is not found"*. The value OpenIAM keyed the token on is always the **opaque** access token — the random string it generated — and never the JWT.

That matters because one client setting makes OpenIAM hand the application a *different* string from the one it stored. When **Send ID Token as Access Token** is on, the application is given the ID token where it expects the access token. The opaque token is still generated and still stored — it is simply not what the application got. So the two values the application receives are byte-identical, and everything that later resolves the token by its value fails.

The application team can confirm the setting from a single sign-in without any access to your server: if the ID token and the access token are the same string, it is on. The setting also forces an ID token to be minted regardless of scope — OpenIAM issues one when the `openid` scope was requested **or** when this flag is on.

{% hint style="warning" %}
**The attribute the Webconsole actually stores is `SEND_ID_TOKEN_AS_ACCESS_TOKEN`.** A second attribute ID spelled `IsSendIdTokenAsAccessToken` exists, but nothing reads or writes it: the Webconsole and the client auto-created alongside a content provider both use `SEND_ID_TOKEN_AS_ACCESS_TOKEN`. If you are querying the stored attributes directly to find out whether the flag is set, that is the ID to filter on; a query for `IsSendIdTokenAsAccessToken` will always come back empty. From the console you cannot tell the two apart at all — the checkbox is the same checkbox.
{% endhint %}

## Related

* [OAuth endpoints, grants, scopes and signing](/administration-guide/federation/oauth-and-oidc/oauth-and-oidc-reference.md) — every endpoint, the tenant metadata document, Client ID Metadata Documents, the `resource` parameter, the six grant flows against the wire `grant_type` values, the client authentication and assertion types, the four signing algorithms, the shipped scopes and the properties added in 2026.10.2
* [Connect AI assistants through the MCP server](/administration-guide/federation/mcp-server.md) — the OpenIAM MCP server, the scopes it checks, and how Claude.ai and Claude Code connect to it
* [Authentication providers and MFA](/administration-guide/security/authentication-providers-and-mfa.md) — the other provider types
* [What an OpenIAM session is, and what ends one](/administration-guide/security/session-management/session-management-reference.md) — how an OAuth token relates to a browser session, and the one absolute session cap OpenIAM has

*Applies to OpenIAM 2026.10.2. Last reviewed 17 October 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

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

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/federation/oauth-and-oidc.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
