The PingAccess rule
AuthZEN PDP is an Add-on SDK rule: the PingAccess counterpart of the Kong plugin, sending the PDP the same AuthZEN request and rendering the same challenges, so a client cannot tell from a deny which gateway said no. Token and actor-claim extraction, RFC 9470 challenges, REST mapping, RFC 9728 and AuthZEN discovery and policy layers are native Java; DPoP verification and every decision on an MCP route are delegated to coaz-pep, as Kong does. It is configured in the PingAccess console or through the admin API, spelled the same as every other PEP here.
Not to be confused with PingAccess's built-in PingAuthorize Policy Decision Access Control and PingAuthorize Access Control rules. Those speak PingAuthorize's own APIs to PingAuthorize alone. This one speaks AuthZEN 1.0 to any conformant PDP, discovers which PDP decides for a resource, and returns resolvable challenges.
Upgrading to 0.4.0
0.4.0 refuses things 0.3 let through. A rule saved under 0.3 may fail to configure under 0.4.0, and every request through it is then a 500 deny until it is fixed, so check each rule before deploying the jar.
style: mcpneedscoaz_url, and every request on an MCP route goes to coaz-pep, not onlytools/call. Nothing passes on a token alone.coaz_defaultsis on by default: every MCP method is governed by the COAZ binding's default table.- An MCP body the rule cannot read one way only is refused with a JSON-RPC error: a batch, invalid UTF-8, a BOM, trailing data, a case-variant member, a compressed or partial body.
- An access token PingAccess did not validate is a 401: protect the application, or tick
allow_insecurefor development. X-User-Tokenneeds a JWKS and an audience. Without a JWKS it is ignored, andrequire_user_loginwithout one is refused. The token must carryexp,suband the audience.X-User-Tokencounts only as the principal's own login: itssubis the access token's subject, and it is neither the access token itself nor a delegated token. A route where staff approve for a customer setsuser_token_subject: pdp, and the PDP judges whose login it is.- The PDP gets coaz-pep's user context:
user_scope,token_aud,user_acr,user_sub,user_iss, andauthorization_detailswith the consented amount and creditor. - Missing security settings are refused, not warned about: an API key or forwarded token over http,
coaz_urlwithout its key, discovery withoutpdp_allowlist,pdp_discovery_insecure,pdp_ssl_verifyoff.allow_insecurelets a development rule start with them, and logs each one. - Every URL must be one the rule can call: absolute http or https with a host.
http://authzen_pdp:8080is refused - an underscore is not legal in a host name. - A fail-open layer opens on an outage only. A 4xx, a redirect or an answer that is not a decision is a refusal, and closed.
- A resource whose metadata cannot be read is not the static PDP's. Only a resource that publishes nothing (a 404, or a document naming no PDP) falls back to
authzen_url; an outage, a refusal or a document that does not validate serves the last good copy for up to one more TTL, then fails the resource layer by its mode. - A payment needs a readable body with a
from_accountand a numeric amount, or it is a 400 before any PDP is asked.
Build and install
The Add-on SDK is on no public Maven repository. Take it from a PingAccess install or, without one, from the public Docker image, which needs no licence to pull.
cd gateways/pingaccess/authzen-pdp
id=$(docker create pingidentity/pingaccess:9.1.0-latest)
mkdir -p .pa/lib && docker cp "$id:/opt/server/lib/pingaccess-sdk-9.1.0.1.jar" .pa/lib/ && docker rm "$id"
mvn verify # tests, the coverage ratchet, the jar
# or, against an install:
mvn -Dpa.server.root=/opt/pingaccess -Dpa.sdk.version=9.1.0.1 verify
The jar is target/authzen-pdp-pingaccess-<version>.jar; PingAccess supplies Jackson, jose4j and slf4j, so nothing is bundled. The bytecode is Java 17, so it needs a Java 17 or later runtime; it has been tested on PingAccess 9.1.0.1 (Java 21) and nothing earlier. Copy it to <PA_HOME>/deploy/ and restart; the DevOps image's local server profile is /opt/in/instance/deploy/. The rule then appears as AuthZEN PDP (class com.idpartners.pa.authzen.AuthZenRule) for Site and Agent destinations.
The rule in the console
The console draws one form from the rule's descriptor: each field with a widget, a label, a default, a help note, and whether it is required or behind Show Advanced Settings. The same descriptor is what the admin API serves at GET /pa-admin-api/v3/rules/descriptors/AuthZenPdpRule. Captured from the demo's PingAccess (docker compose --profile pingaccess) as configured by its start-up hook.





Every field
Names are as saved under configuration; the console shows a label with the name in brackets. advanced marks a field behind Show Advanced Settings.
The PDP
| Field | Widget · default | Meaning |
|---|---|---|
authzen_url required | text | The static PDP: decides for a resource that publishes nothing, and is always permitted. |
authzen_api_key | concealed | Bearer key for authzen_url, sent over https only. A discovered PDP never receives it. Stored encrypted; the API returns {"encryptedValue": "OBF:JWE:…"}. |
pdp_timeout_ms advanced | text · 10000 | Deadline on each PDP call, the whole exchange included. |
pdp_ssl_verify advanced | checkbox · true | Certificate verification on every outbound call. Off trusts any chain but still checks the host name, and needs allow_insecure: dev only. |
The route
| Field | Widget · default | Meaning |
|---|---|---|
pep_label | text · pingaccess-pep | Names this PEP in challenges and the X-PDP-PEP header. |
style | select · rest | rest (resource server) or mcp (MCP edge: every request goes to coaz-pep, and coaz_url is required). |
require_token | checkbox · true | Deny without a readable access token. A token PingAccess did not validate is a 401 either way, unless allow_insecure. |
require_dpop | checkbox · false | Delegate the RFC 9449 sender-constraint check to coaz-pep; needs coaz_url, and is refused at configuration time without it. |
require_user_login | checkbox · false | Deny without a verified X-User-Token, with a login challenge. Needs user_token_jwks_url. |
stepup_scope | text | Scope named in a step-up challenge when the PDP's advice names none. |
resource | text | The protected resource's identifier (RFC 8707), the key discovery starts from. |
forward_access_token | checkbox · false | Send the raw token to the PDP as context.access_token; refused over plain http. |
coaz-pep
| Field | Widget · default | Meaning |
|---|---|---|
coaz_url | text | coaz-pep's HTTP check API. Required for style: mcp and for require_dpop. |
coaz_api_key | concealed | Its CHECK_API_TOKEN; required with coaz_url, and sent over https only. |
mcp_upstream_url | text | Where tools/list lives; coaz-pep reads each tool's mapping from it. |
federation_entity_url | text | Relay the resource's two well-known documents from coaz-pep at this URL. |
coaz_defaults advanced | checkbox · true | Govern every MCP method by the COAZ-MCP binding's default table; false keeps the old pass-through for methods other than tools/call. |
coaz_timeout_ms advanced | text · 15000 | Deadline on each coaz-pep call, the whole exchange included. |
Discovery
| Field | Widget · default | Meaning |
|---|---|---|
pdp_discovery | select · off | off, authzen or resource. No federation mode; the same reason as Kong, and the same remedy: behind coaz-pep. |
pdp_metadata_ttl advanced | text · 300 | Cache TTL, seconds, for resource and PDP metadata. A fetch that fails, or a document that does not validate, serves the last good copy for up to one more TTL; after that, or with none, the layer is unavailable and its failure mode decides. |
pdp_allowlist | list | Permitted discovered-PDP prefixes; required when pdp_discovery is on. authzen_url is always permitted. |
resource_metadata_allowlist | list · any | Permitted resource prefixes for metadata fetches. |
pdp_discovery_insecure advanced | checkbox · false | Allow http for discovered URLs; needs allow_insecure (dev only). |
Layers
| Field | Widget · default | Meaning |
|---|---|---|
pdp_layers | list · ["resource"] | Ordered PDPs, every one of which must permit; fail-open / fail-closed per entry. An empty list means resource. |
fail_mode | select · closed | What a layer does when its PDP is down (no connection, a timeout, a 5xx, a 429), unless it says for itself. A refusal - a 4xx, a redirect, an answer that is not a decision - never opens. |
The user token
| Field | Widget · default | Meaning |
|---|---|---|
user_token_jwks_url | text | Verify X-User-Token against this JWKS, over https (jose4j is on PingAccess's classpath); exp and sub are required. Unset, the token is ignored, or decoded under allow_insecure. |
user_token_issuer advanced | text | Expected iss when verifying. |
user_token_audience | text | Expected aud; required with the JWKS. |
user_token_subject advanced | select · principal | Whose login counts. principal: only a token whose sub is the access token's subject. pdp: another person's verified login too - a staff member approving for a customer - and the PDP, told user_sub and user_iss, decides. Either way, not the access token itself and not a delegated (act) token. |
Transport and compatibility
| Field | Widget · default | Meaning |
|---|---|---|
legacy_subject_identity advanced | checkbox · true | Also send the non-standard subject.identity beside subject.id. |
allow_insecure advanced | checkbox · false | Development only: start with what the rule otherwise refuses (unvalidated access tokens, an unverified X-User-Token, keys over http, no pdp_allowlist, TLS verification off), each relaxation logged at configure time. |
Saving validates twice: field constraints first, reported on the field under a Save Failed banner (the PDP URL must not be blank, the drop-downs must hold one of their options, the numbers must be numbers), then the rule's own configure. That refuses a URL the rule could not call (not absolute http or https, no host, user info), style: mcp or require_dpop without coaz_url, a JSON null for a drop-down, and - unless allow_insecure is ticked - every missing or weakened security setting, listed in one banner. The descriptor sets agentCachingDisabled: true: an agent never caches this rule's verdict, because it depends on the token, the body and the PDP's current view.
The admin API
The same configuration as JSON, keyed by the names above. This is the repository's reference sample; demo/pingaccess/hooks/81-after-start-process.sh is the whole sequence, site and application included, as a script.
curl -sk -u administrator:$PA_PASSWORD -H 'X-Xsrf-Header: PingAccess' -H 'Content-Type: application/json' \
https://pa-admin:9000/pa-admin-api/v3/rules -d '{
"name": "authzen-pdp",
"className": "com.idpartners.pa.authzen.AuthZenRule",
"supportedDestinations": ["Site", "Agent"],
"configuration": {
"authzen_url": "https://pdp.bank.example",
"authzen_api_key": "...",
"pep_label": "PEP#2 (Bank API edge)",
"style": "rest",
"require_token": true,
"pdp_discovery": "resource",
"resource": "https://api.bank.example",
"resource_metadata_allowlist": ["https://api.bank.example"],
"pdp_allowlist": ["https://pdp.bank.example"],
"pdp_layers": ["https://estate-pdp.example fail-open", "resource"],
"user_token_jwks_url": "https://as.bank.example/jwks",
"user_token_audience": "https://api.bank.example"
}
}'
A saved rule reads back as GET /pa-admin-api/v3/rules/{id} with the form's values under configuration; concealed fields come back encrypted.
Attaching it to an application
Then POST /applications with the rule in the API policy. The demo's hook creates the site, the virtual host and the application in that order and attaches the rule; this is the shape of the application.
{
"name": "bank",
"contextRoot": "/bank",
"destination": "Site",
"siteId": 1,
"virtualHostIds": [1],
"accessValidatorId": 0, # unprotected: the demo's tokens are unsigned, so its rule sets allow_insecure
"enabled": true,
"policy": { "API": [ { "type": "Rule", "id": 1 } ] }
}
In the console: Applications → Applications, edit the application, the API Policy tab, drag authzen-pdp from Available Rules onto the policy. Applications are created disabled unless enabled is set. Unprotected applications still run rules, but the rule refuses a token PingAccess did not validate unless it is configured with allow_insecure, which is how the demo uses unsigned tokens. A production application has an access token validator, and then PingAccess validates the bearer before the rule runs.
What PingAccess does, and what the rule does
- The access token. When the application is protected, PingAccess validates the bearer token before any rule runs: signature or introspection, expiry, audience. The rule reads what PingAccess established from the exchange's identity, and those attributes win over the token's own payload, which is also read when the token is a JWT, so
act,cnfandacrare available whether PingAccess exposes them or not. Unprotected, PingAccess establishes no identity, and a request carrying a token is a 401 unless the rule hasallow_insecure. - DPoP. PingAccess enforces DPoP natively when it validates the token (the application's
dpopSettings); that is the place for it.require_dpopis for the unprotected case and for parity with Kong: the proof goes to coaz-pep's/v1/dpop/verify. - The user token. Unlike Kong, the rule verifies
X-User-Tokenitself, since jose4j is on the classpath: signature,exp,suband the configured audience. Withoutuser_token_jwks_urlthe token is ignored. A verified token counts only as the principal's own login, unlessuser_token_subjectispdp; whenever one counts, the PDP is told whose it is (user_sub,user_iss), as coaz-pep tells it. - MCP routes. Every request goes to coaz-pep's
/v1/mcp/check. A body that is not exactly one JSON-RPC message the rule read in full is refused first: 400 with-32700or-32600, 413 for a partial body, 415 for a compressed one. - Headers. A permit removes every
X-Auth-*header the client sent before setting the ones the PEP asserts. Values from a PDP's answer are cleaned for a header, and escaped insideWWW-Authenticate. - The deny on the wire, the forwarded context, layers and fail mode are the Kong plugin's, knob for knob. A layer fails open on an outage only, never on a refusal. The long form of each is on the Kong page; the reasoning is in
docs/pingaccess.md.