Register and configure clients
A client is any application that requests tokens from the AS — a web app, a mobile app, an API service, an AI agent. What a client can do is defined by its software statement — the grant types, resources, and scopes it carries — and shaped by policy at every request.
Registering a client
There are several ways to create a client:
| Method | When to use | How it works |
|---|---|---|
| Admin API | First-time setup, test clients, clients your team manages directly | POST /admin/api/clients with client details. You control the credentials and configuration. |
| Software-statement DCR | Federated ecosystems, partner onboarding | POST /register with a software_statement JWT. OPA policy decides whether to allow it and what the client gets. |
| Plain DCR | Self-service registration, MCP clients | POST /register without a software statement. Allowed when public_client_policy.enabled: true in policy data. |
| CIMD | Zero-config MCP clients | URL-format client_id in /authorize. AS fetches and validates metadata, creates a transient client on-the-fly. |
All methods produce the same thing: a client record with a client_id, credentials, and stored metadata.
For evaluation, start with the Admin API — it's simpler and requires no software statement. Move to DCR or CIMD when you're ready to model a federated or self-service registration flow.
Registration paths in detail
| Path | Trigger | Client type | Trust source |
|---|---|---|---|
| CIMD | URL-format client_id (starts with https://) in /authorize | Public | Policy data (public_client_policy.cimd) |
| Plain DCR | POST /register without software_statement | Public or confidential | Policy data (public_client_policy.dcr) |
| Software-statement DCR | POST /register with software_statement | Per statement | Software statement + OPA |
| Admin API | POST /admin/api/clients | Any | Direct configuration |
CIMD is the zero-config path for MCP clients. The AS fetches the client's metadata document from the URL, validates client_id and redirect_uris match, and creates a transient public client. The registration_type field on the client record is set to cimd. Synthetic granted_resources come from data.oauth.public_client_policy.cimd.granted_resources in policy data.
Plain DCR requires public_client_policy.enabled: true in policy data. Requested grant types are validated against public_client_policy.dcr.allowed_grant_types. The registration_type is set to plain_dcr. Like CIMD clients, plain DCR clients without a software statement receive granted_resources from policy data rather than from a software statement.
Per-client overrides are possible for both CIMD and plain DCR clients by setting public_client_policy.clients.<client_id> in policy data, which can override granted_resources, TTLs, or other policy-controlled attributes for specific clients.
Software statements
A software statement is a JWT that describes what a client is and what it should be able to do. It carries the client's capabilities as claims:
| Claim | What it controls |
|---|---|
iss | Who issued the statement. Must be in data.oauth.trust.trusted_issuers for DCR. |
sub | Software identifier. Used as a lookup key in policy data (data.oauth.software_id). |
grant_types | Which OAuth grants this client can use: client_credentials, authorization_code, refresh_token, token-exchange. |
granted_resources | What this client can request access to: an array of { type, scopes, resource_indicator? }. |
resources | What this client serves as a Resource Server: an array of { type, scopes, resource_indicator }. |
| Any custom claim | Available to OPA policy as input.client.software_statement.<claim>. Use for trust levels, agent metadata, app roles — anything policy should evaluate. |
The AS stores the full software statement with the client record. At every token, introspect, and authorize request, OPA receives it as input.context.client.software_statement. This means policy always has the complete picture of what the client claims to be.
This is what makes PBAC flexible without being complicated: the software statement is a bag of claims, and policy decides what to do with them. You don't configure client permissions in the AS — you express them in the statement and let policy interpret them.
granted_resources vs resources
These two fields look similar but serve different purposes:
granted_resources— what this client can consume. A patient portal that needsreadon health records.resources— what this client hosts as a Resource Server. An EHR API that serves health records withreadandwrite.
Most clients only have granted_resources. A client with resources is declaring itself as a Resource Server — during DCR, OPA policy can return is_resource_server: true to auto-create the RS record.
In practice, most evaluation scenarios only need granted_resources. You'll add resources when you're modeling a real Resource Server that other clients will access.
For Admin API clients
When you create a client via the Admin API, you can include a softwareStatement JSON map in the request body. This is stored the same way as a DCR software statement — OPA sees it identically. You can also issue a signed JWT via POST /admin/api/clients/{clientId}/software-statement.
Identity providers
An Identity Provider (IdP) is an external OIDC provider — Okta, Azure AD, Keycloak, or any OIDC-compliant service. The AS federates with IdPs for user-delegated flows (authorization code grant).
When a user needs to authenticate, OPA selects which IdP to redirect to. This is configured in policy data:
{
"user": {
"idp_selection": {
"default_idp": "corporate-idp",
"resource_to_idp": {
"https://api.health.example.com": "health-idp"
},
"acr_to_idp": {
"mfa": "mfa-idp"
}
}
}
}
Priority: acr_to_idp > resource_to_idp > resource_type_to_idp > default_idp.
IdPs are registered via the Admin API with their OIDC endpoints and the AS's client credentials at that IdP. The Admin UI includes a "Discover from Issuer" button that auto-populates endpoints from /.well-known/openid-configuration.
Resource servers and resource types
A Resource Server is an API that accepts tokens issued by this AS. It is identified by a resource indicator URI (per RFC 8707) — for example, https://api.example.com/fhir.
A Resource Type is a category of data that a Resource Server hosts — for example, urn:example:Patient with scopes read, write, delete. Resource Types define the valid scopes for that category. A single RS can serve multiple types, and the same type can span multiple RSes.
When a client requests a token with resource=https://api.example.com/fhir and resource_types=urn:example:Patient, the AS resolves the resource indicator to a Resource Server and validates the requested scopes against the type's scope registry.
Resource Servers can be created manually via the Admin API, or auto-created during DCR when OPA returns is_resource_server: true.
DPoP-bound access tokens
Clients can request sender-constrained access tokens using DPoP (RFC 9449). A DPoP-bound token carries the public key thumbprint of the client and can only be used alongside a valid DPoP proof — a stolen token without the private key is useless.
To signal DPoP capability during DCR, include dpop_bound_access_tokens: true in the registration request or software statement:
{
"grant_types": ["client_credentials", "authorization_code"],
"dpop_bound_access_tokens": true
}
When this field is present and true, the AS expects a DPoP header on every token request from that client. Token requests that omit the proof header are rejected.
Public vs confidential clients: For confidential clients, only access tokens are key-bound. For public clients (SPAs, mobile apps), refresh tokens are also bound to the DPoP key at issuance — the same key must be used when redeeming the refresh token. This closes the gap that exists for public clients who cannot hold a long-lived client secret.
Trust
Trust in PBAC is policy-driven and evaluated at every decision.
Software statement issuers must be listed in data.oauth.trust.trusted_issuers for DCR registration to succeed. The AS can optionally verify the JWT signature (PBAC_SOFTWARE_STATEMENT_VERIFY=true).
Trust decisions happen in policy. OPA has full access to the software statement claims and can use any claim — trust_level, agent_provider, custom fields — to make per-request decisions. For example, policy can:
- Grant shorter TTLs to clients with
trust_level: "low" - Issue JIT single-use tokens to agent clients
- Deny write scopes to clients from unknown providers
- Require additional verification for unrecognized issuers
Next steps
- Policy — How OPA evaluates all of this into allow/deny decisions
- Enforcement — How Resource Servers validate tokens and enforce obligations