Skip to main content
Version: 1.0

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:

MethodWhen to useHow it works
Admin APIFirst-time setup, test clients, clients your team manages directlyPOST /admin/api/clients with client details. You control the credentials and configuration.
Software-statement DCRFederated ecosystems, partner onboardingPOST /register with a software_statement JWT. OPA policy decides whether to allow it and what the client gets.
Plain DCRSelf-service registration, MCP clientsPOST /register without a software statement. Allowed when public_client_policy.enabled: true in policy data.
CIMDZero-config MCP clientsURL-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

PathTriggerClient typeTrust source
CIMDURL-format client_id (starts with https://) in /authorizePublicPolicy data (public_client_policy.cimd)
Plain DCRPOST /register without software_statementPublic or confidentialPolicy data (public_client_policy.dcr)
Software-statement DCRPOST /register with software_statementPer statementSoftware statement + OPA
Admin APIPOST /admin/api/clientsAnyDirect 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:

ClaimWhat it controls
issWho issued the statement. Must be in data.oauth.trust.trusted_issuers for DCR.
subSoftware identifier. Used as a lookup key in policy data (data.oauth.software_id).
grant_typesWhich OAuth grants this client can use: client_credentials, authorization_code, refresh_token, token-exchange.
granted_resourcesWhat this client can request access to: an array of { type, scopes, resource_indicator? }.
resourcesWhat this client serves as a Resource Server: an array of { type, scopes, resource_indicator }.
Any custom claimAvailable 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 needs read on health records.
  • resources — what this client hosts as a Resource Server. An EHR API that serves health records with read and write.

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