OAuth and OIDC endpoints
Base URL: https://pbac.example.com (configured via PBAC_OIDC_ISSUER).
All token and authorization responses include Cache-Control: no-store and Pragma: no-cache.
Multi-tenant deployments may pass X-Tenant-Id: <tenant> on any request to scope the operation to a tenant.
Discovery
GET /.well-known/openid-configuration
Returns the OIDC discovery document. No authentication required.
Response — 200 OK, application/json:
| Field | Description |
|---|---|
issuer | Authorization server issuer URL |
authorization_endpoint | /authorize |
token_endpoint | /token |
registration_endpoint | /register |
jwks_uri | /.well-known/jwks.json |
userinfo_endpoint | /userinfo |
introspection_endpoint | /introspect |
resource_registration_endpoint | /resource_registration/resource |
permission_endpoint | /permission |
response_types_supported | ["code"] |
id_token_signing_alg_values_supported | ["RS256"] |
authorization_details_types_supported | Array of supported RAR type URIs (derived from policy data) |
client_id_metadata_document_supported | true — CIMD (draft-ietf-oauth-client-id-metadata-document) is supported |
curl https://pbac.example.com/.well-known/openid-configuration
GET /.well-known/jwks.json
Returns the public JWK Set used to verify ID tokens (RS256). No authentication required.
Response — 200 OK, application/json:
{
"keys": [
{ "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "...", "n": "...", "e": "AQAB" }
]
}
curl https://pbac.example.com/.well-known/jwks.json
Authorization
GET /authorize
Initiates the OAuth 2.1 / OIDC authorization code flow (RFC 6749 §4.1, OAuth 2.1). The AS redirects the user agent to the configured upstream IdP, then redirects back to the client's redirect_uri with a code (or an error).
PKCE is mandatory for public clients; strongly recommended for confidential clients.
response_type=token (implicit grant) and grant_type=password (ROPC) are not supported.
Query parameters:
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code |
client_id | Yes | Registered client identifier |
redirect_uri | Yes | Must exactly match a registered redirect URI |
scope | No | Space-separated scopes (e.g., openid profile) |
state | No | Opaque value for CSRF protection |
nonce | No | Replay protection for ID tokens |
code_challenge | No* | PKCE challenge (Base64url-encoded SHA-256 of verifier); required for public clients |
code_challenge_method | No | S256 (recommended) or plain |
resource | No | Resource indicator (RFC 8707) — mutually exclusive with authorization_details |
resource_types | No | Comma-separated resource type URIs |
authorization_details | No | JSON array of RAR objects (RFC 9396) — mutually exclusive with resource/scope |
acr_values | No | Requested authentication context class reference |
prompt | No | none, login, or consent |
login_hint | No | Hint for the upstream IdP |
X-Tenant-Id | No (header) | Tenant scoping |
Success redirect — 302 Found to redirect_uri?code=<code>&state=<state>
Error redirect — 302 Found to redirect_uri?error=<code>&error_description=<desc>&state=<state>
Client ID Metadata Document (CIMD)
When client_id is a URL (starts with https://), the AS interprets it as a Client ID Metadata Document per draft-ietf-oauth-client-id-metadata-document. This is the mechanism used by the MCP Authorization Specification for zero-config client registration.
Behaviour:
- The AS fetches the JSON document at the
client_idURL. - It validates that the document's
client_idfield matches the URL exactly. - It validates that all
redirect_urisin the request are present in the document'sredirect_uris. - A public client record is created on-the-fly with
registration_type: cimd. - The client receives synthetic
granted_resourcesfromdata.oauth.public_client_policy.cimd.granted_resourcesin policy data.
URL constraints: The URL must have a path component, must not contain fragment (#) or dot segments (. / ..). http://localhost and http://127.0.0.1 are accepted in development when pbac.registration.cimd.allow-insecure-localhost: true is set.
# Browser-initiated; shown as an example URL
curl -G https://pbac.example.com/authorize \
--data-urlencode "response_type=code" \
--data-urlencode "client_id=my-app" \
--data-urlencode "redirect_uri=https://app.example.com/callback" \
--data-urlencode "scope=openid profile" \
--data-urlencode "state=random-state" \
--data-urlencode "code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" \
--data-urlencode "code_challenge_method=S256"
Token
POST /token
Issues access tokens. Content-Type: application/x-www-form-urlencoded.
Client authentication: Authorization: Basic <base64(client_id:secret)>, form parameters client_id/client_secret, or client_assertion (private_key_jwt).
Common parameters:
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | See grant types below |
client_id | No* | Required when not using Basic auth |
client_secret | No* | Required for confidential clients not using client_assertion |
client_assertion_type | No | urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
client_assertion | No | JWT signed with client's private key |
Success response — 200 OK, application/json:
| Field | Description |
|---|---|
access_token | Opaque bearer token |
token_type | Bearer |
expires_in | Lifetime in seconds |
scope | Granted scope (may differ from requested) |
refresh_token | Present when refresh token was issued |
id_token | RS256-signed JWT, present for OIDC flows |
authorization_details | RAR details array (RFC 9396), when applicable |
issued_token_type | Token exchange responses only (RFC 8693) |
Error response — 400/401, application/json:
{ "error": "invalid_client", "error_description": "..." }
grant_type=client_credentials
For confidential clients only (OAuth 2.1 §4.2.2). Issues an access token without a user context.
| Parameter | Required | Description |
|---|---|---|
scope | No | Requested scopes |
resource | No | Resource indicator (RFC 8707) |
resource_types | No | Comma-separated resource type URIs |
authorization_details | No | JSON array of RAR objects (RFC 9396) |
curl -s -X POST https://pbac.example.com/token \
-u "my-service:s3cr3t" \
-d "grant_type=client_credentials" \
-d "resource=https://api.example.com/fhir" \
-d "scope=read"
grant_type=authorization_code
Exchanges an authorization code for tokens. PKCE verifier must be supplied if a challenge was included in the authorization request.
| Parameter | Required | Description |
|---|---|---|
code | Yes | Authorization code from /authorize redirect |
redirect_uri | Yes | Must match the URI used in the authorization request |
code_verifier | No* | Required when PKCE was used |
resource | No | Resource indicator (RFC 8707) |
resource_types | No | Comma-separated resource type URIs |
curl -s -X POST https://pbac.example.com/token \
-u "my-app:s3cr3t" \
-d "grant_type=authorization_code" \
-d "code=SplxlOBeZQQYbYS6WxSbIA" \
-d "redirect_uri=https://app.example.com/callback" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
grant_type=refresh_token
Exchanges a refresh token for a new access token.
| Parameter | Required | Description |
|---|---|---|
refresh_token | Yes | Refresh token from a previous token response |
scope | No | Requested scopes (subset of original) |
resource | No | Resource indicator (RFC 8707) |
curl -s -X POST https://pbac.example.com/token \
-u "my-app:s3cr3t" \
-d "grant_type=refresh_token" \
-d "refresh_token=tGzv3JOkF0XG5Qx2TlKWIA"
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
RFC 8693 Token Exchange. Exchanges an existing token for a new access token with a different audience or scope.
| Parameter | Required | Description |
|---|---|---|
subject_token | Yes | Token to exchange |
subject_token_type | Yes | Type URI (see supported types below) |
requested_token_type | No | Desired output token type |
audience | No | Intended audience for the new token |
scope | No | Requested scopes |
resource | No | Resource indicator (RFC 8707) |
resource_types | No | Comma-separated resource type URIs |
authorization_details | No | JSON array of RAR objects (RFC 9396) |
Supported subject_token_type values:
| Type URI | Description |
|---|---|
urn:ietf:params:oauth:token-type:access_token | Exchange an access token issued by this AS. The AS looks up the token and constrains the new token's scopes to a subset of the original. |
urn:ietf:params:oauth:token-type:id_token | Exchange an id_token from a trusted external issuer. The AS verifies the JWT signature against the issuer's JWKS and extracts subject claims. The issuer's iss must be in trusted_issuers. |
Example — access_token exchange (delegation):
curl -s -X POST https://pbac.example.com/token \
-u "my-service:s3cr3t" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token=<access_token>" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "scope=read"
Example — id_token exchange (cross-domain identity):
curl -s -X POST https://pbac.example.com/token \
-u "my-service:s3cr3t" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token=<id_token_jwt>" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:id_token"
Error responses for id_token exchange:
| Error | Description |
|---|---|
invalid_grant — "Subject token missing iss claim" | The id_token JWT has no iss claim |
invalid_grant — "Subject token missing sub claim" | The id_token JWT has no sub claim |
invalid_grant — "Subject token expired" | The id_token's exp is in the past |
invalid_grant — "Subject token signature verification failed" | JWT signature does not match the issuer's JWKS |
invalid_grant — "Unable to verify subject token issuer" | The issuer's JWKS endpoint is unreachable |
OPA deny — untrusted_token_issuer | The id_token's iss is not in trusted_issuers |
Introspection
POST /introspect
RFC 7662 Token Introspection. Re-evaluates the token against OPA policy and returns its current active state and claims. Callers must be authorized resource servers.
Content-Type: application/x-www-form-urlencoded.
Authentication (one of):
Authorization: Bearer <token>— where the bearer token hasintrospectionoruma_protectionscopeAuthorization: Basic <base64(client_id:secret)>— client must be entitled to introspectclient_assertionform parameters — private_key_jwt
Request parameters:
| Parameter | Required | Description |
|---|---|---|
token | Yes | The token to introspect |
token_type_hint | No | access_token or refresh_token |
context | No | JSON object with RS-supplied context (e.g., resource, resource_types, scopes) merged into OPA input |
X-Tenant-Id | No (header) | Tenant scoping |
Response — 200 OK, application/json:
If the token is invalid or expired, active is false and all other fields are absent.
| Field | Description |
|---|---|
active | true if the token is valid and policy allows access |
scope | Granted scopes |
client_id | Client that obtained the token |
sub | Subject identifier |
username | Human-readable username |
token_type | Bearer |
exp | Expiration (Unix timestamp) |
iat | Issued-at (Unix timestamp) |
iss | Issuer |
resource | Resource indicator (RFC 8707) |
resource_types | Array of granted resource type URIs |
authorization_details | RAR details array (RFC 9396) |
obligations | Policy-defined obligations map |
extensions | Additional PDP output claims |
JIT single-use tokens (flagged jit_single_use in PDP output) are deleted after the first successful introspection.
# Bearer auth
curl -s -X POST https://pbac.example.com/introspect \
-H "Authorization: Bearer <rs-access-token>" \
-d "token=<token-to-introspect>"
# With RS-supplied context
curl -s -X POST https://pbac.example.com/introspect \
-H "Authorization: Bearer <rs-access-token>" \
-d "token=<token-to-introspect>" \
--data-urlencode 'context={"resource":"https://api.example.com/fhir","resource_types":["https://example.com/Patient"]}'
Registration
POST /register
RFC 7591 Dynamic Client Registration. Issues a client_id (and optionally a client_secret) for the submitted client metadata.
Two registration modes are supported:
- Software-statement DCR — request includes
software_statement(a signed JWT). OPA evaluates the statement to determine what grants and resources the client is entitled to. - Plain DCR — request omits
software_statement. Allowed whenpublic_client_policy.enabled: truein policy data. Both public clients (token_endpoint_auth_method: none) and confidential clients are supported. Requested grant types are validated againstpublic_client_policy.dcr.allowed_grant_types.
Content-Type: application/json. No authentication required (policy-gated).
Request body:
| Field | Required | Description |
|---|---|---|
redirect_uris | Yes | Array of allowed redirect URIs |
token_endpoint_auth_method | No | none, client_secret_post, client_secret_basic, or private_key_jwt (default: client_secret_basic) |
grant_types | No | Array of grant types the client intends to use |
response_types | No | Array of response types |
scope | No | Requested default scope |
client_name | No | Human-readable name |
client_uri | No | Home page URI |
logo_uri | No | Logo URI |
contacts | No | Array of contact email addresses |
tos_uri | No | Terms of service URI |
policy_uri | No | Privacy policy URI |
jwks_uri | No | HTTPS URI of the client's JWK Set (must be HTTPS) |
jwks | No | Inline JWK Set (alternative to jwks_uri) |
software_id | No | UUID identifying the software package |
software_version | No | Version string |
software_statement | No | Signed JWT asserting client metadata (RFC 7591 §2.3) |
tenant_id | No | Tenant scoping |
Response — 201 Created, application/json:
| Field | Description |
|---|---|
client_id | Assigned client identifier |
client_secret | Client secret (confidential clients only) |
client_id_issued_at | Unix timestamp of registration |
client_secret_expires_at | 0 (non-expiring) |
redirect_uris | Registered redirect URIs |
grant_types | Registered grant types |
token_endpoint_auth_method | Registered auth method |
# With software statement (federated trust)
curl -s -X POST https://pbac.example.com/register \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": ["https://app.example.com/callback"],
"client_name": "My Application",
"token_endpoint_auth_method": "client_secret_basic",
"software_statement": "eyJ..."
}'
# Plain DCR — public client (requires public_client_policy.enabled: true in policy data)
curl -s -X POST https://pbac.example.com/register \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": ["https://app.example.com/callback"],
"client_name": "My Public App",
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"]
}'
UserInfo
GET /userinfo (also POST)
OIDC UserInfo endpoint. Returns claims for the authenticated user. Requires a valid access token with openid scope.
Authentication: Authorization: Bearer <access-token>
Response — 200 OK, application/json:
{
"sub": "user-123",
"name": "Jane Smith",
"email": "jane@example.com",
"preferred_username": "jsmith",
"email_verified": true
}
Error — 401 Unauthorized with WWW-Authenticate: Bearer error="invalid_token".
curl -s https://pbac.example.com/userinfo \
-H "Authorization: Bearer <access-token>"
UMA 2.0
POST /permission
UMA 2.0 Permission endpoint (UMA §4). A resource server posts a permission request and receives a permission ticket, which the client then uses to obtain an RPT via /token.
Authentication: Authorization: Bearer <PAT> — a Protection API Token with uma_protection scope.
Content-Type: application/json.
Request body:
| Field | Required | Description |
|---|---|---|
resource_id | Yes | UMA resource identifier |
resource_scopes | Yes | Array of requested scope strings |
Response — 201 Created, application/json:
{ "ticket": "<permission-ticket>" }
curl -s -X POST https://pbac.example.com/permission \
-H "Authorization: Bearer <pat>" \
-H "Content-Type: application/json" \
-d '{"resource_id": "res-abc123", "resource_scopes": ["read"]}'
POST /resource_registration/resource
UMA 2.0 Resource Registration API (UMA §3.2). Creates a resource record owned by the authenticated subject.
Authentication: Authorization: Bearer <PAT> with uma_protection scope.
Content-Type: application/json.
Request body:
| Field | Required | Description |
|---|---|---|
name | No | Human-readable resource name |
type | No | Resource type URI |
resource_scopes | No | Array of applicable scope strings |
Response — 201 Created, application/json:
{ "_id": "<resource-id>" }
Location header points to the new resource.
curl -s -X POST https://pbac.example.com/resource_registration/resource \
-H "Authorization: Bearer <pat>" \
-H "Content-Type: application/json" \
-d '{"name": "Patient Record 42", "type": "https://example.com/Patient", "resource_scopes": ["read", "write"]}'
GET /resource_registration/resource
Lists resource IDs owned by the authenticated subject.
Response — 200 OK: JSON array of resource ID strings.
GET /resource_registration/resource/{resourceId}
Returns the resource record.
Response — 200 OK: { "_id", "name", "type", "resource_scopes" }
PUT /resource_registration/resource/{resourceId}
Updates a resource record. Request body same as POST. Response — 200 OK.
DELETE /resource_registration/resource/{resourceId}
Deletes a resource record. Response — 204 No Content or 404 Not Found.
Delegation
POST /delegation
Creates a delegation grant — allows the authenticated subject to delegate access to a resource to another subject or client. Requires a Bearer token with delegation_management scope.
Content-Type: application/json.
Request body:
| Field | Required | Description |
|---|---|---|
grantee_type | No | subject or client |
grantee_subject_id | No | Subject ID of the grantee (when grantee_type=subject) |
grantee_client_id | No | Internal client PK of the grantee (when grantee_type=client) |
resource_id | Yes | UMA resource ID to delegate |
scopes | No | Array of scope strings to delegate |
expires_at | No | ISO 8601 expiry timestamp |
Response — 201 Created:
{ "delegation_id": "<id>" }
GET /delegation
Lists all delegations owned by the authenticated subject. Response — 200 OK: JSON array of delegation objects.
GET /delegation/{delegationId}
Returns a single delegation. Response — 200 OK or 404 Not Found.
PUT /delegation/{delegationId}
Updates a delegation (currently supports updating expires_at). Response — 200 OK or 404 Not Found.
DELETE /delegation/{delegationId}
Revokes a delegation. Response — 204 No Content or 404 Not Found.
AuthZEN
POST /access/v1/evaluation
AuthZEN Access Evaluation endpoint. Evaluates whether a subject may perform an action on a resource, delegating to OPA.
Authentication (one of):
Authorization: Bearer <token>— token must haveauthzenscopeAuthorization: Basic <base64(client_id:secret)>— client must be entitled toauthzen
Content-Type: application/json.
Request body:
| Field | Required | Description |
|---|---|---|
subject | Yes | Object with type (string), id (string), properties (object) |
action | Yes | Object with name (string), properties (object) |
resource | Yes | Object with type (string), id (string), properties (object) |
context | No | Arbitrary context map passed to OPA |
Response — 200 OK, application/json:
| Field | Description |
|---|---|
decision | true if access is granted, false otherwise |
context | Optional map with additional PDP context (obligations, etc.) |
curl -s -X POST https://pbac.example.com/access/v1/evaluation \
-H "Authorization: Bearer <authzen-token>" \
-H "Content-Type: application/json" \
-d '{
"subject": { "type": "user", "id": "alice@example.com" },
"action": { "name": "read" },
"resource": { "type": "https://example.com/Patient", "id": "patient-42" }
}'
Next steps
- Admin API Reference — Manage IdPs, clients, resource servers, policy rules, and audit logs
- Getting Tokens — Practical guide to requesting tokens with each grant type
- Standards Compliance — Detailed compliance status for each specification