HTTP servlet extension · built against Server SDK 10.1 and 11.1
AuthZEN Adapter
Serves the OpenID AuthZEN Authorization API 1.0 from inside PingAuthorize and turns each request into a native decision or query. Any AuthZEN client - an API gateway, an application, an AI agent - can rely on the policies you already govern, with no proxy between the API and the decision.
The shape of it
Supported AuthZEN operations
An AuthZEN evaluation {subject, action, resource, context} becomes a governance-engine decision request. Each tuple element travels as a JSON string under a configurable attribute prefix, so a Trust Framework request resolver can read it; the engine's verdict maps straight back to the AuthZEN boolean, and any policy reason rides along in context.
The subject id is accepted under id (spec) or identity (as earlier Ping enforcement points send it) - identity wins when both are present. The adapter reads {"authorised":…}, {"authorized":…} or {"decision":"PERMIT"|"DENY"} from the engine, so it works against either dialect.
Flat attributes, Shared Signals events and the SPIFFE caller check are off unless configured; advice mapping applies whenever policy returns the advice statement codes.
| Addition | Turned on by | What it does |
|---|---|---|
| Flat tuple attributes | flat-tuple=true | Also sends the tuple's scalars as dot-free attributes, for policies that bind request attributes directly. |
| Advice mapping | always on, policy-driven | Folds step-up-required and identity-proofing-required statements into the response context. |
| SSF Transmitter | ssf-shared-secret | Queues a Security Event Token for each evaluation, and serves stream management for one push receiver. |
| SPIFFE caller | accepted-spiffe-id | Takes the caller from the mTLS peer's verified certificate and forwards it to policy as <prefix>.caller. |
API surface
Eight paths, one extension
| Method | Path | Purpose | Gate |
|---|---|---|---|
| POST | /access/v1/evaluation | Single access evaluation - returns a decision object, not an array. | caller |
| POST | /access/v1/evaluations | Batch evaluation. Honours options.evaluations_semantic: execute_all (default), deny_on_first_deny, permit_on_first_permit. Each entry overlays the request-level defaults; an empty batch evaluates the top-level tuple once. | caller |
| POST | /access/v1/search/subject | Subject search via the governance-engine query endpoint. | caller |
| POST | /access/v1/search/resource | Resource search via policy: one evaluation whose PERMIT carries an entitled_accounts statement, else the empty set. | caller |
| POST | /access/v1/search/action | Action search via the query endpoint. | caller |
| GET | /.well-known/authzen-configuration | Metadata document with absolute endpoint URLs and eight vendor-namespaced capability URNs. Honours X-Forwarded-Proto/Host/Port behind a TLS-terminating edge. | public |
| GET | /.well-known/ssf-configuration | SSF Transmitter metadata. 404 unless SSF is enabled. | public |
| POSTGETDELETE | /ssf/stream | SSF stream management - create, read, delete. 404 unless SSF is enabled. The read returns the receiver's authorization_header: protect it with api-key or a SPIFFE caller check, because with neither set the adapter admits any caller. | caller |
Metadata document
GET /.well-known/authzen-configuration { "policy_decision_point": "https://pdp.example", "access_evaluation_endpoint": "https://pdp.example/access/v1/evaluation", "access_evaluations_endpoint": "https://pdp.example/access/v1/evaluations", "search_subject_endpoint": "https://pdp.example/access/v1/search/subject", "search_resource_endpoint": "https://pdp.example/access/v1/search/resource", "search_action_endpoint": "https://pdp.example/access/v1/search/action", "capabilities": [ "urn:idpartners:authzen:capability:access-evaluation", "urn:idpartners:authzen:capability:access-evaluations", "urn:idpartners:authzen:capability:search-subject", "urn:idpartners:authzen:capability:search-resource", "urn:idpartners:authzen:capability:search-action", "urn:idpartners:authzen:capability:evaluations-semantic:execute-all", "urn:idpartners:authzen:capability:evaluations-semantic:deny-on-first-deny", "urn:idpartners:authzen:capability:evaluations-semantic:permit-on-first-permit" ] }
Errors, one shape
Every failure is {"error", "error_description"}. A missing field or bad JSON is 400 invalid_request; a refused caller is 401 unauthorized; an unknown or disabled path is 404 not_found; an engine that cannot be reached or answers without a decision is 502 pdp_error - and the engine's own text goes to the server error log, not to the client.
AuthZEN 1.0 defines capabilities as an optional array of PDP-specific URNs. No standard URNs are registered yet, so the adapter publishes vendor-namespaced ones that truthfully describe its surface, including the three batch semantics.
Translation bench · live
AuthZEN in, PingAuthorize decision out
The bench mirrors the adapter's mapping rules in your browser: edit the request, flip the arguments, and watch what PingAuthorize's decision engine would be asked. Then pick how the engine answers and see the AuthZEN response the PEP receives. Highlighted keys are what the adapter adds beyond the spec's tuple.
flat-tuple is for.Reading the decision
Advice a PEP can branch on
Two policy statement codes are mapped into the AuthZEN response context as custom advice fields, so a PEP can trigger step-up authentication or identity proofing rather than act on a bare DENY. These fields are ID Partners conventions, not part of AuthZEN, and the PEP decides how to issue any challenge.
Statements are scanned in order and the last advice statement's message wins. A missing or malformed payload still sets the bare flag; it just contributes no detail. With no advice at all, the reason comes from the engine's reason field, else the first statement's payload.
| Statement code | Response context |
|---|---|
| step-up-required | step_up_required: truestep_up_scope ← payload scopereason ← payload message |
| identity-proofing-required | identity_proofing_required: trueidentity_proofing_doctype ← payload doctypereason ← payload message |
| anything else | reason ← engine reason, else first statement payload |
Batch semantics
Where a batch stops
The Access Evaluations endpoint evaluates entries in order and short-circuits per options.evaluations_semantic. The response omits a top-level decision and returns the evaluations array for the entries it reached. Below, the same three-entry batch - PERMIT, DENY, PERMIT - under each semantic.
| evaluations_semantic | Stops at | Entry 1 | Entry 2 | Entry 3 | Results returned |
|---|---|---|---|---|---|
| execute_all (default) | never | true | false | true | 3 |
| deny_on_first_deny | the first false | true | false | not evaluated | 2 |
| permit_on_first_permit | the first true | true | not evaluated | not evaluated | 1 |
Each entry overlays the request-level subject, action, resource and context - the entry wins where present. A batch envelope with no entries evaluates its top-level tuple once. Every entry that is evaluated publishes its own SSF event.
Search
Two routes to an answer set
Subject and action search: the query endpoint
These call the governance-engine query endpoint (query-url) and map the echoed values into AuthZEN results. The query names subject and resource with your attribute-prefix but the action bare, for Trust Frameworks that give action its own value source - validate the mapping against your query resolvers. An echoed value is parsed as JSON when it is JSON and kept as text when it is not, so an engine that answers with a bare action name is understood rather than rejected.
POST query-url · subject search { "query": [ { "attribute": "Gateway.subject", "values": ["{\"type\":\"user\"}"] }, { "attribute": "action" }, { "attribute": "Gateway.resource", "values": ["{\"type\":\"account\",\"id\":\"acct-1\"}"] } ] }
Resource search: the policy
The query endpoint cannot enumerate a free resource id - there is no finite value source to draw from - so the answer comes from policy instead. One evaluation runs with the resource carrying only its type; a PERMIT with an entitled_accounts statement supplies the permitted ids.
statement code: entitled_accounts · payload { "accounts": ["CHK-1001", "SAV-2002"] }
A DENY, a missing statement, a wrong code, a non-JSON payload or a payload without an array all yield the empty set. The search fails closed rather than guessing, and your policy must attach the statement for it to return anything.
Conformance notes. Evaluation requires subject.type, subject.id, action.name, resource.type and resource.id, and refuses a request that lacks one - or gives one that is not a string - before the engine is asked. Subject and action search go to the engine's query endpoint with the members that are known and the request context. Resource search does the same when resource-search=query is set; by default it sends only the resource type and reads the ids from a policy statement. There is no pagination: results come back in one response with no page, and they identify an entity without describing it. In a batch, an entity an entry gives replaces the request-level default whole; batch-defaults=fill relaxes that for a missing type, id or action name.
Conformance testing · OpenID Foundation
Run through the OpenID Foundation's conformance suite
The adapter is tested with the OpenID Foundation conformance suite, the suite behind OpenID Connect and FAPI certification. In September 2026 all six of its AuthZEN policy decision point plans were run, on suite release 5.3.1, against PingAuthorize 11.1 holding the certification scenario's fixture policy, built as code, with the metadata document found by discovery: 147 of 147 tests passed. The run is held in ID Partners' account on the suite; ask us for the plan logs.
Two things to know before quoting that number. The suite's AuthZEN plans are alpha tests, not yet part of the OpenID certification programme, so this is evidence of conformance rather than a certificate, and ID Partners does not describe the adapter as certified. And one module still tests a rule from the draft of the certification scenario - that a batch entry inherits a missing type from the request-level default - which the published scenario and AuthZEN 1.0 reversed. The adapter follows the published rule; the batch-defaults=fill setting lets it satisfy the suite as it stands, and comes off when the suite catches up.
| Plan | Suite plan id | Result |
|---|---|---|
| Access evaluation | SqMxLMEydMHzZ | passed |
| Access evaluations, batch | UPYVDDIVYMlE2 | passed |
| Access evaluations, comprehensive | R1qiA8KCOn97j | passed |
| Subject search | k6qP6j6Hcv2Ry | passed |
| Resource search | tfedjdGtk3w7t | passed |
| Action search | fc5HiJso5fzBW | passed |
Suite release 5.3.1, September 2026, with pdp_auth_type=none and pdp_server_metadata=discovery: the run that exercises the metadata checks. The AuthZEN Authorization API 1.0 is an OpenID Foundation specification.
Who may call · live
Three ways in, tried in order
api-key says that someone holding the shared key called. On a connection handler that requires a client certificate, the handler has already established which workload called before the adapter runs - and the adapter reads the peer's SPIFFE ID off that verified chain.
- The peer's SPIFFE ID is listed in
accepted-spiffe-id. Admitted, no bearer needed, and forwarded to policy as<prefix>.caller(pluscallerSpiffeIdwithflat-tuple). - Otherwise, unless
spiffe-requiredis true, a validAuthorization: Bearer <api-key>. Admitted with no caller attribute - its absence is how policy tells a key holder from a listed workload. - Otherwise, when no
api-keyis configured either, anyone.
The two /.well-known/ documents skip the gate by design. The caller rides on evaluations, batches and resource search; the query path carries none. spiffe-required with no listed ID, or an ID that is not a well-formed SPIFFE ID, is refused when the adapter is configured, rather than silently locking everyone out.
Shared Signals · opt in
Evaluation events for a Shared Signals receiver
Set ssf-shared-secret and the adapter acts as a Shared Signals transmitter for the evaluations it answers - single and batch. Searches, and calls made directly to PingAuthorize's own decision APIs, produce no events. The adapter queues evaluation events for background delivery. Delivery is best effort. Unset, the SSF paths answer 404 and nothing is published.
# register the Receiver curl -s https://pdp.example/ssf/stream \ -H "Authorization: Bearer <api-key>" \ -H 'Content-Type: application/json' \ -d '{ "aud": "receiver-app", "events_requested": ["https://schemas.idpartners.com.au/ssf/authzen-decision"], "delivery": { "method": "urn:ietf:rfc:8935", "endpoint_url": "https://receiver.example/events", "authorization_header": "Bearer <receiver-secret>" } }' # 201 {"stream_id":"…","iss":"…","aud":"receiver-app","events_supported":[…], # "events_requested":[…],"events_delivered":[…],"delivery":{…}}
GET /ssf/stream?stream_id=… reads it back; DELETE unregisters it, idempotently. events_delivered is the intersection of what was asked for and what the Transmitter supports - an unrecognised event type is simply not delivered, not an error.
POST https://receiver.example/events Content-Type: application/secevent+jwt # header { "alg": "HS256", "typ": "secevent+jwt" } # claims - no sub or exp { "iss": "https://pdp.example", "jti": "0d9c6b1e-4f7a-4a52-9a0e-3c1b7d2e8f41", "iat": 1789180200, "aud": "receiver-app", "events": { "https://schemas.idpartners.com.au/ssf/authzen-decision": { "subject": "agent-42", "action": "transfer", "resource": "acct-1", "decision": "DENY", "step_up_required": true, "step_up_scope": "payments:high-value", "reason": "Transfers over 10,000 AUD need a stronger sign-in", "attrs": { "amount": 25000, "currency": "AUD", "to_account": "SAV-2002" } } } }
Advice fields from the decision context are merged in at the top level. attrs carries a fixed whitelist of request-context keys - amount, currency, to_account, user_scope, channel - and is omitted when none are present.
How delivery behaves, and current limits
- Never in the decision's way. Events are delivered on a background thread with 5-second timeouts, so a slow or unreachable Receiver cannot add latency to, or fail, the decision that triggered them.
- Best effort. Up to 256 events queue for delivery; beyond that, new events are dropped and counted. A failed delivery is not retried.
- Push to one Receiver. RFC 8935 push delivery to a single registered stream, managed with create, read and delete on
/ssf/stream. The registration lives in memory, so a Receiver registers again after a server restart. - Shared-secret signing. Tokens are signed HS256 with
ssf-shared-secret, shared with the receiver out of band. No signing keys are published. - Discovery metadata is partial.
/.well-known/ssf-configurationreturnsissuer,delivery_methods_supportedandconfiguration_endpoint. It omitsjwks_uri, which SSF 1.0 requires when the transmitter signs tokens, andspec_version, so receivers will assume1_0-ID1. Check your receiver accepts this before relying on it.
Configuration
Sixteen extension arguments
Each is an extension-argument:name=value on the HTTP servlet extension.
| Argument | Default | What it does |
|---|---|---|
| Reaching the governance engine | ||
| pdp-url required | https://localhost:8443/governance-engine | The JSON PDP decision endpoint. |
| query-url | …/governance-engine/query | Query endpoint used by subject and action search. |
| pdp-secret-header | CLIENT-TOKEN | Header carrying the shared secret. |
| pdp-secret | unset | The shared secret value. |
| trust-any-server-cert | true | Trust any TLS certificate from the engine. Suits a loopback engine; set false when the engine is reached over a network. |
| timeout-millis | 12000 | Connect and read timeout for engine calls. |
| Shaping the decision request | ||
| domain-prefix | empty | The decision request's domain. |
| pdp-service | empty | The decision request's service. |
| pdp-action | empty | The decision request's action. |
| attribute-prefix | empty | Trust Framework attribute prefix, e.g. Gateway. Empty names the attributes subject, action, resource, context. |
| flat-tuple | false | Also send agentId, actionName, resourceType, resourceId (and callerSpiffeId) as dot-free attributes. |
| Who may call | ||
| api-key | unset | Require Authorization: Bearer <api-key> on every endpoint except the two /.well-known/ documents. |
| accepted-spiffe-id | unset · repeatable | SPIFFE IDs admitted from the mTLS peer's verified certificate, without the key, and forwarded as <prefix>.caller. |
| spiffe-required | false | Refuse anything but a listed SPIFFE ID, dropping the key fallback. Needs at least one accepted-spiffe-id. |
| Shared Signals | ||
| ssf-issuer | https://localhost:1443 | The iss every Security Event Token is signed as. |
| ssf-shared-secret | unset | HS256 signing secret. Unset disables the whole SSF surface. |
Install and compatibility
Two commands into a running server
The adapter ships as a manage-extension bundle for your PingAuthorize major version. Install it, then apply the registration batch inside it - which also adds the adapter to the HTTPS connection handler.
# 1. install the bundle <paz>/bin/manage-extension --install authzen-adapter-<version>.zip # 2. register and serve it - set the arguments in the batch first <paz>/bin/dsconfig --no-prompt --batch-file config/register.dsconfig # serve it from another handler too, e.g. the SPIFFE mTLS handler <paz>/bin/dsconfig set-connection-handler-prop \ --handler-name "SPIFFE mTLS Connection Handler" \ --add "http-servlet-extension:AuthZEN Adapter"
| PingAuthorize | Servlet API | Built against Server SDK |
|---|---|---|
| Version 10 | javax.servlet 4.0 | 10.1.0.0 |
| Version 11 | jakarta.servlet 6.1 | 11.1.0.0 |
- One code base, both majors. PingAuthorize 11 moved its Server SDK from
javax.servlettojakarta.servletand refuses a bundle built for the other. Both bundles are built from the same source. Talk to ID Partners to confirm support for your exact PingAuthorize release. - Nothing to conflict with. The bundle carries only the extension jar; JSON, servlet and SDK classes come from the running server.
- Coverage-gated. The build fails unless the methods that admit a caller, shape a decision request, read an answer, stop a batch or build an event have full line and branch test coverage.