Skip to main content
Version: 1.0

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.

Response200 OK, application/json:

FieldDescription
issuerAuthorization 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_supportedArray of supported RAR type URIs (derived from policy data)
client_id_metadata_document_supportedtrue — 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.

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

ParameterRequiredDescription
response_typeYesMust be code
client_idYesRegistered client identifier
redirect_uriYesMust exactly match a registered redirect URI
scopeNoSpace-separated scopes (e.g., openid profile)
stateNoOpaque value for CSRF protection
nonceNoReplay protection for ID tokens
code_challengeNo*PKCE challenge (Base64url-encoded SHA-256 of verifier); required for public clients
code_challenge_methodNoS256 (recommended) or plain
resourceNoResource indicator (RFC 8707) — mutually exclusive with authorization_details
resource_typesNoComma-separated resource type URIs
authorization_detailsNoJSON array of RAR objects (RFC 9396) — mutually exclusive with resource/scope
acr_valuesNoRequested authentication context class reference
promptNonone, login, or consent
login_hintNoHint for the upstream IdP
X-Tenant-IdNo (header)Tenant scoping

Success redirect302 Found to redirect_uri?code=<code>&state=<state>

Error redirect302 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:

  1. The AS fetches the JSON document at the client_id URL.
  2. It validates that the document's client_id field matches the URL exactly.
  3. It validates that all redirect_uris in the request are present in the document's redirect_uris.
  4. A public client record is created on-the-fly with registration_type: cimd.
  5. The client receives synthetic granted_resources from data.oauth.public_client_policy.cimd.granted_resources in 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:

ParameterRequiredDescription
grant_typeYesSee grant types below
client_idNo*Required when not using Basic auth
client_secretNo*Required for confidential clients not using client_assertion
client_assertion_typeNourn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertionNoJWT signed with client's private key

Success response200 OK, application/json:

FieldDescription
access_tokenOpaque bearer token
token_typeBearer
expires_inLifetime in seconds
scopeGranted scope (may differ from requested)
refresh_tokenPresent when refresh token was issued
id_tokenRS256-signed JWT, present for OIDC flows
authorization_detailsRAR details array (RFC 9396), when applicable
issued_token_typeToken exchange responses only (RFC 8693)

Error response400/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.

ParameterRequiredDescription
scopeNoRequested scopes
resourceNoResource indicator (RFC 8707)
resource_typesNoComma-separated resource type URIs
authorization_detailsNoJSON 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.

ParameterRequiredDescription
codeYesAuthorization code from /authorize redirect
redirect_uriYesMust match the URI used in the authorization request
code_verifierNo*Required when PKCE was used
resourceNoResource indicator (RFC 8707)
resource_typesNoComma-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.

ParameterRequiredDescription
refresh_tokenYesRefresh token from a previous token response
scopeNoRequested scopes (subset of original)
resourceNoResource 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.

ParameterRequiredDescription
subject_tokenYesToken to exchange
subject_token_typeYesType URI (see supported types below)
requested_token_typeNoDesired output token type
audienceNoIntended audience for the new token
scopeNoRequested scopes
resourceNoResource indicator (RFC 8707)
resource_typesNoComma-separated resource type URIs
authorization_detailsNoJSON array of RAR objects (RFC 9396)

Supported subject_token_type values:

Type URIDescription
urn:ietf:params:oauth:token-type:access_tokenExchange 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_tokenExchange 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:

ErrorDescription
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_issuerThe 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 has introspection or uma_protection scope
  • Authorization: Basic <base64(client_id:secret)> — client must be entitled to introspect
  • client_assertion form parameters — private_key_jwt

Request parameters:

ParameterRequiredDescription
tokenYesThe token to introspect
token_type_hintNoaccess_token or refresh_token
contextNoJSON object with RS-supplied context (e.g., resource, resource_types, scopes) merged into OPA input
X-Tenant-IdNo (header)Tenant scoping

Response200 OK, application/json:

If the token is invalid or expired, active is false and all other fields are absent.

FieldDescription
activetrue if the token is valid and policy allows access
scopeGranted scopes
client_idClient that obtained the token
subSubject identifier
usernameHuman-readable username
token_typeBearer
expExpiration (Unix timestamp)
iatIssued-at (Unix timestamp)
issIssuer
resourceResource indicator (RFC 8707)
resource_typesArray of granted resource type URIs
authorization_detailsRAR details array (RFC 9396)
obligationsPolicy-defined obligations map
extensionsAdditional 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 when public_client_policy.enabled: true in policy data. Both public clients (token_endpoint_auth_method: none) and confidential clients are supported. Requested grant types are validated against public_client_policy.dcr.allowed_grant_types.

Content-Type: application/json. No authentication required (policy-gated).

Request body:

FieldRequiredDescription
redirect_urisYesArray of allowed redirect URIs
token_endpoint_auth_methodNonone, client_secret_post, client_secret_basic, or private_key_jwt (default: client_secret_basic)
grant_typesNoArray of grant types the client intends to use
response_typesNoArray of response types
scopeNoRequested default scope
client_nameNoHuman-readable name
client_uriNoHome page URI
logo_uriNoLogo URI
contactsNoArray of contact email addresses
tos_uriNoTerms of service URI
policy_uriNoPrivacy policy URI
jwks_uriNoHTTPS URI of the client's JWK Set (must be HTTPS)
jwksNoInline JWK Set (alternative to jwks_uri)
software_idNoUUID identifying the software package
software_versionNoVersion string
software_statementNoSigned JWT asserting client metadata (RFC 7591 §2.3)
tenant_idNoTenant scoping

Response201 Created, application/json:

FieldDescription
client_idAssigned client identifier
client_secretClient secret (confidential clients only)
client_id_issued_atUnix timestamp of registration
client_secret_expires_at0 (non-expiring)
redirect_urisRegistered redirect URIs
grant_typesRegistered grant types
token_endpoint_auth_methodRegistered 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>

Response200 OK, application/json:

{
"sub": "user-123",
"name": "Jane Smith",
"email": "jane@example.com",
"preferred_username": "jsmith",
"email_verified": true
}

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

FieldRequiredDescription
resource_idYesUMA resource identifier
resource_scopesYesArray of requested scope strings

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

FieldRequiredDescription
nameNoHuman-readable resource name
typeNoResource type URI
resource_scopesNoArray of applicable scope strings

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

Response200 OK: JSON array of resource ID strings.

GET /resource_registration/resource/{resourceId}

Returns the resource record.

Response200 OK: { "_id", "name", "type", "resource_scopes" }

PUT /resource_registration/resource/{resourceId}

Updates a resource record. Request body same as POST. Response200 OK.

DELETE /resource_registration/resource/{resourceId}

Deletes a resource record. Response204 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:

FieldRequiredDescription
grantee_typeNosubject or client
grantee_subject_idNoSubject ID of the grantee (when grantee_type=subject)
grantee_client_idNoInternal client PK of the grantee (when grantee_type=client)
resource_idYesUMA resource ID to delegate
scopesNoArray of scope strings to delegate
expires_atNoISO 8601 expiry timestamp

Response201 Created:

{ "delegation_id": "<id>" }

GET /delegation

Lists all delegations owned by the authenticated subject. Response200 OK: JSON array of delegation objects.

GET /delegation/{delegationId}

Returns a single delegation. Response200 OK or 404 Not Found.

PUT /delegation/{delegationId}

Updates a delegation (currently supports updating expires_at). Response200 OK or 404 Not Found.

DELETE /delegation/{delegationId}

Revokes a delegation. Response204 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 have authzen scope
  • Authorization: Basic <base64(client_id:secret)> — client must be entitled to authzen

Content-Type: application/json.

Request body:

FieldRequiredDescription
subjectYesObject with type (string), id (string), properties (object)
actionYesObject with name (string), properties (object)
resourceYesObject with type (string), id (string), properties (object)
contextNoArbitrary context map passed to OPA

Response200 OK, application/json:

FieldDescription
decisiontrue if access is granted, false otherwise
contextOptional 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