Standards status
Each row below is one requirement of one standard. It gives the status, what the code does, the source file it rests on, and the integration test or the dated run that shows it. A row with no code path, or with neither a test nor a run, fails the docs check.
The status means only what the row says. Implemented means the code does what the row says, the docs check finds the code and the tests the row cites, and those tests passed in our run on 2026-10-13. A test marked may skip can skip when its setup is missing, such as a sign-in through the mock identity provider, and a skip is not a pass. Partial means part of the requirement works and the row says which part does not. Not implemented means the code does not do it. Refused by design means the server refuses it on purpose. No row says the product conforms to a whole standard.
Paths under Code start at src/main/java/com/identos/pbac/ unless they name another directory. Tests under Evidence are in integration-tests/ and run against an instance with ./pbac test <instance> -- <file> -k <name>.
MCP authorization
The rows under MCP authorization follow revision 2026-07-28 of the MCP specification. In that revision, dynamic client registration is deprecated and kept for clients that cannot use a client ID metadata document. The PolicyArc MCP server is the managed gateway at /gateway/mcp. The authorization server issues its tokens.
These rows cover the requirements that fall on an authorization server or an MCP server. Requirements on MCP clients, such as sending the resource parameter, are the client's to meet. Rows under the other standards cover the MCP requirements that point to them, such as refresh token rotation, exact redirect URIs and code_challenge_methods_supported.
Five server requirements of the revision have no row. All authorization server endpoints must be served over HTTPS, which the deployment's TLS setup decides. Servers must account for scope hierarchies. An MCP proxy that uses a static client ID must get the user's consent for each dynamically registered client before it forwards to a third-party authorization server. No test or run here covers those three. Clients and servers must store tokens securely, as OAuth 2.1 section 7.1 describes. The server keeps each access and refresh token in its database as issued, not as a hash, and we saw an issued access token in the access_token table on 2026-10-13. An authorization server must check that a client ID metadata document is valid JSON with the required fields. The server checks only client_id and redirect_uris.
Not listed
A standard appears here only when the repo has a test or a run for it. JWT access tokens (RFC 9068) are not listed, because access tokens are opaque. Pushed authorization requests (RFC 9126) are not listed, because the server has no such endpoint.
OAuth 2.1 (draft-ietf-oauth-v2-1-13)
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Implicit grant (response_type=token) | Refused by design | Not offered. /authorize answers 400 invalid_request with "response_type must be 'code'". Discovery lists only code. | oauth/service/AuthorizeService.java |
|
| Resource owner password grant | Refused by design | Not offered. /token answers unsupported_grant_type for grant_type=password. | oauth/controller/TokenController.java |
|
| Exact redirect URI matching | Implemented | A redirect_uri must match a registered value. A prefix of a registered URI is refused. A loopback URI matches on any port. | oauth/util/RedirectUriValidator.java |
|
| Authorization codes are single use | Implemented | A second redemption of a code returns invalid_grant, with "Invalid or expired authorization code" when it comes after the first, and "Authorization code already used" when two redemptions race. | oauth/grant/AuthorizationCodeGrantHandler.javaoauth/grant/AuthorizationCodeGrantHandler.java |
|
| client_credentials only for confidential clients | Implemented | A public client that asks for client_credentials is refused. | oauth/grant/ClientCredentialsGrantHandler.java |
|
| Refresh token rotation for public clients (section 4.3.1) | Partial | Rotates for public clients without DPoP. The old token is deleted, so a replay is refused but does not revoke the new token. A public client whose tokens are DPoP-bound is not rotated. | oauth/service/TokenService.javaoauth/service/TokenService.javaoauth/grant/RefreshTokenGrantHandler.java |
|
RFC 7636 PKCE
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| S256 code challenge | Implemented | S256 is the only method discovery advertises. A wrong code_verifier is refused. | oauth/controller/WellKnownController.java |
|
| plain code challenge method | Refused by design | /authorize answers 400 invalid_request, "PKCE code_challenge_method 'plain' is not allowed; use S256". /token refuses a code that carries plain. | oauth/service/AuthorizeService.javaoauth/grant/AuthorizationCodeGrantHandler.java |
|
| Code challenge required for public clients | Implemented | A public client with no code_challenge is sent back to its redirect URI with error=invalid_request. The pkce control can widen this to every client or a named list. | oauth/service/AuthorizeService.javasrc/main/resources/controls/pkce.control.v1.json |
|
OpenID Connect Core 1.0
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| ID token claims iss, sub, aud, exp, iat | Implemented | Issued from the authorization code flow when openid is granted, signed RS256, with the client in aud. | oauth/service/IdTokenService.java |
|
| nonce and auth_time in the ID token | Implemented | A nonce from the request is copied into the ID token. max_age adds auth_time. | oauth/service/IdTokenService.java |
|
| UserInfo endpoint | Implemented | GET /userinfo with a bearer token returns sub, which matches the ID token's sub. An invalid token gets 401. The controller also maps POST. test_userinfo_uses_get_or_post calls GET only, so no test here calls POST. | oauth/controller/UserInfoController.javaoauth/controller/UserInfoController.java |
|
| Pairwise subject identifiers | Not implemented | Discovery lists only the public subject type. The test asserts only that subject_types_supported is a list. The run saw ["public"]. | oauth/controller/WellKnownController.java |
|
OpenID Connect Discovery 1.0 and RFC 8414
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Authorization server metadata | Implemented | Served at /.well-known/openid-configuration and /.well-known/oauth-authorization-server, with code_challenge_methods_supported and registration_endpoint. | oauth/controller/WellKnownController.java |
|
RFC 7591 Dynamic Client Registration
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Registration without a software statement | Implemented | POST /register accepts public and confidential clients. The public_client_policy control limits the grant types, and turning it off refuses plain registration. | oauth/controller/RegisterController.java |
|
| Software statement signature | Implemented | With PBAC_SOFTWARE_STATEMENT_VERIFY at its default of true, a statement whose payload was changed after signing, or one with no signature, is refused with invalid_software_statement. | src/main/resources/application.yml |
|
RFC 7523 JWT client authentication
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| private_key_jwt client authentication | Implemented | A client registered with private_key_jwt authenticates at /token and /introspect with a signed client_assertion. | oauth/service/ClientAssertionValidator.java |
|
RFC 7009 Token Revocation
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Token revocation | Partial | POST /revoke makes the caller's own token inactive. On 0.65.0, revoking another client's token returns 200 and the token stays active. RFC 7009 section 2.1 says "If this validation fails, the request is refused and the client is informed of the error by the authorization server as described below." | oauth/controller/RevocationController.java |
|
RFC 7662 Token Introspection
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Token introspection | Implemented | POST /introspect needs a caller with a bearer token that carries the right scope, or Basic auth. An unknown token returns active false. The caller can add resource context, and policy is evaluated again with it. | oauth/controller/IntrospectController.java |
|
RFC 8693 Token Exchange
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Token exchange with access and ID tokens | Implemented | Accepts an access token or an ID token from a trusted issuer as the subject token. The response has no refresh token. The subject token stays valid. | oauth/grant/TokenExchangeGrantHandler.java |
|
| Actor tokens and the act claim | Implemented | An actor_token adds an act claim whose sub is the actor token's subject, or its client when it has none, shown at introspection. A chained exchange nests act claims. Without an actor_token there is no act claim. | oauth/controller/TokenController.javaoauth/grant/TokenExchangeGrantHandler.java |
|
| Exchanged token never wider than the subject token | Partial | On 0.65.0 the token rules compare a request with the subject token only for a resource the exchange did not derive itself (token.rego skips a resource marked derived). When the subject token names a resource, the exchange inherits it as an explicit resource, and that resource is compared. Otherwise the server derives the resource from the requested scope and marks it derived. In our run a client_credentials token for mcp:tools, requested with or without resource_types=urn:mcp:server, was exchanged for mcp:resources and for introspection, and a token for introspection requested with resource_types=urn:as:introspect was exchanged for mcp:tools. A token for introspection requested without resource_types names the resource urn:as:introspect, and its exchange for mcp:tools was refused with access_denied, reason token_exchange_actions_exceeded. The integration test asserts only a 400 with an error. The rego test asserts the reason. | opa/policies/oauth/token.regoopa/policies/oauth/token.regooauth/grant/TokenExchangeGrantHandler.javasrc/main/resources/controls/token_derivation.control.v1.json |
|
RFC 8707 Resource Indicators
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| resource parameter (section 2) | Implemented | Accepted at /authorize and /token. A resource with a fragment is refused with invalid_request. An unknown resource is refused. | oauth/controller/TokenController.java |
|
RFC 9396 Rich Authorization Requests
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| authorization_details | Implemented | Accepted at /authorize and /token, returned in the token response, and advertised in authorization_details_types_supported. A type the policy data does not list is refused. The introspection test checks the field's shape only when it is present, so it does not show that introspection returns it. | oauth/OAuthError.java |
|
RFC 9449 DPoP
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| DPoP-bound access tokens | Implemented | A valid proof at /token yields token_type DPoP and cnf.jkt at introspection. A replayed proof jti is refused. | oauth/service/DPopProofValidator.java |
|
| DPoP proof at the resource (section 4.3, check 12, and 7) | Implemented | The managed gateway refuses a DPoP-bound token sent without a proof or with a proof from another key. | gateway/service/GatewayFlowService.java |
|
RFC 9728 Protected Resource Metadata
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Protected resource metadata | Implemented | GET /.well-known/oauth-protected-resource names the resource <issuer>/gateway/mcp, the authorization server and scopes_supported. A gateway request with no token gets 401 and a WWW-Authenticate header with resource_metadata. | gateway/controller/GatewayDiscoveryController.javagateway/service/GatewayChallengeRenderer.java |
|
RFC 9207 Authorization Server Issuer Identification
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| iss parameter in authorization responses | Not implemented | The success redirect carries code and state only, and error redirects carry no iss. Discovery has no authorization_response_iss_parameter_supported. | oauth/service/OAuth2CallbackService.java |
|
OAuth Client ID Metadata Document (draft-ietf-oauth-client-id-metadata-document-00)
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| URL client IDs | Implemented | An HTTPS client_id with a path is fetched at /authorize. The document's client_id must equal the URL and the redirect_uri must be listed in it. Discovery sets client_id_metadata_document_supported. | oauth/service/ClientIdMetadataService.java |
|
| Fetch guard | Implemented | An HTTPS client_id is fetched through the AS's guarded fetcher. A client_id that points at a link-local or private address is refused at once, without a connection. | oauth/service/ClientIdMetadataService.java |
|
MCP authorization, revision 2026-07-28
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| MCP server publishes protected resource metadata (MUST) | Implemented | The gateway at /gateway/mcp is the MCP server. See the RFC 9728 row. | gateway/controller/GatewayDiscoveryController.java |
|
| Client ID metadata documents (SHOULD) and dynamic registration (MAY, deprecated in this revision) | Implemented | Both obtain a token. Neither test calls the gateway. | oauth/controller/WellKnownController.java |
|
| Accept only tokens issued for this resource (MUST) | Partial | A token anchored by resource indicator to one managed resource is refused on a sibling. A type-wide token works on every managed resource of its type. A token of another type is accepted when its scope names match the scopes the call needs. In our 0.65.0 run a token for urn:example:x with scope orders:read read an order through a resource of type urn:example:orders. A token granted only openid cannot call an MCP tool. | gateway/service/GatewayFlowService.java |
|
| No token passthrough to upstream APIs (MUST NOT) | Implemented | The client's Authorization header is not forwarded to the upstream. The upstream gets the credential its managed resource configures. | gateway/service/GatewayProxyService.java |
|
| Invalid or expired token gets HTTP 401 (MUST) | Implemented | A gateway call with a token the server does not know gets 401 with WWW-Authenticate: Bearer, resource_metadata and error="invalid_token". | gateway/service/GatewayFlowService.java |
|
| Redirect URIs are localhost or HTTPS (MUST) | Implemented | Registration refuses an http redirect URI on a public host with invalid_redirect_uri. At /authorize, a redirect_uri on a public host name that resolves to 127.0.0.1 is refused with 400, not treated as loopback. | oauth/service/RegisterService.javaoauth/service/AuthorizeService.java |
|
| Precautions against redirecting to untrusted URIs (MUST, OAuth 2.1 section 7.12.2) | Partial | A redirect_uri must match a registered one exactly, so the server never redirects to an unregistered URI. A request with a registered URI that fails a check, such as a public client with no PKCE challenge, is redirected back with an error before anyone signs in. The server does not judge whether a registered URI can be trusted. | oauth/util/RedirectUriValidator.javaoauth/service/AuthorizeService.java |
|
| Show the redirect URI host name during authorization (MUST, client ID metadata document security) | Not implemented | The authorization server has no page of its own during /authorize. It sends the browser straight to the identity provider, so it shows no redirect URI host and no localhost warning. | oauth/service/AuthorizeService.java |
|
| HTTP 403 for insufficient scope or permissions (MUST) | Partial | A REST call through the gateway that policy refuses gets HTTP 403. An MCP tools/call that policy refuses gets HTTP 200 with a JSON-RPC result that has isError true and the text 403. | gateway/controller/GatewayMcpController.javagateway/service/GatewayFlowService.java |
|
| insufficient_scope challenge on a scope denial (SHOULD) | Not implemented | A REST call that policy refuses gets 403 with no WWW-Authenticate header. An MCP tool call that policy refuses gets an in-band error result. The 401 challenge carries resource_metadata and no scope parameter. | gateway/service/GatewayFlowService.java |
|
UMA 2.0 Grant and Federated Authorization
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Resource registration and permission tickets | Partial | Resource registration and POST /permission work with a protection API token. /token does not redeem a permission ticket, so no RPT is issued. | oauth/controller/UmaPermissionController.javaoauth/controller/TokenController.java |
|
OpenID Connect CIBA Core 1.0
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Backchannel authentication, poll mode | Implemented | POST /bc-authorize starts a request that a policy-named approver decides. The client polls /token with the CIBA grant. A second redemption returns invalid_grant. | oauth/controller/BackchannelAuthorizeController.java |
|
OpenID AuthZEN Authorization API
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| Access evaluation | Implemented | POST /access/v1/evaluation returns the policy decision. The caller needs a bearer token with the authzen scope, or the Basic credentials of a client entitled to authzen. | pdp/authzen/controller/AccessEvaluationController.java |
|
SPIFFE JWT-SVID
| Requirement | Status | What the code does | Code | Evidence |
|---|---|---|---|---|
| JWT-SVID client authentication | Implemented | A workload presents a JWT-SVID at /token. A bad signature, an expired SVID or an untrusted trust domain is refused. | oauth/service/SpiffeAuthenticator.java |
|
Where the rows come from
The rows live in services/pbac-documentation/src/data/standards.json. python3 scripts/docs_claims_check.py reads every row and fails when a code file no longer contains the text the row cites, a test the row names is no longer defined, or a cited test can call pytest.skip and is not marked may skip. It does not run the tests. To run them, pass the files and names from the Evidence column to ./pbac test.
Send feedback about this page