AuthZEN
permdock/authzen serves the OpenID AuthZEN Authorization API 1.0 (evaluation, evaluations, search, discovery) from a PermDock policy so the decision endpoint is a standard PDP.
permdock/authzen exposes a PermDock policy as an AuthZEN Policy Decision Point. One permdockHandler serves the evaluation, batched evaluations, search and discovery endpoints. The React decision endpoint (permdockHandler in permdock/next, the endpoint option of PermDockProvider) and the pdp provider use the same request and response schemas, so PermDock speaks one wire format whether it is the PDP or the PEP.
Purpose
The OpenID AuthZEN Authorization API 1.0 (final January 2026) standardises how a PEP asks a PDP "may this subject perform this action on this resource in this context". It defines /access/v1/evaluation, batched /access/v1/evaluations (boxcar), /access/v1/search/subject, /search/resource, /search/action, and a .well-known/authzen-configuration metadata document, plus a certification programme with Basic, Batch, Search and Discovery levels. Keycloak and the NLgov profile implement it. PermDock adopts it instead of a bespoke format. In-repo tests cover all four shapes, and testAuthZen from permdock/testing runs the official interop Todo vectors (conformance).
API
import { createPermDock } from "permdock/authzen";
export const { permdockHandler } = createPermDock(policy, {
subject: fromBearer, // (request) => principal | null; MUST be real authentication
resources: {
post: {
load: (id) => loadPost(id),
list: ({ where }) => db.posts.where(where),
},
},
});
// Fetch-first: mount under /access/v1 and /.well-known
app.all("/access/v1/*", (c) => permdockHandler(c.req.raw));
app.get("/.well-known/authzen-configuration", (c) =>
permdockHandler(c.req.raw),
);| Endpoint | PermDock call | Certification level |
|---|---|---|
POST /access/v1/evaluation | decide(permission, resource) | Basic |
POST /access/v1/evaluations | simulate([[permission, resource], ...]) | Batch |
POST /access/v1/search/action | catalog of permitted actions on a resource for the subject ("what can I do") | Search |
POST /access/v1/search/resource | filter / where over a resource type, materialised via resources.<type>.list; results are { type, id } entities | Search |
POST /access/v1/search/subject | subjects permitted for an action on a resource (requires a subject enumerator) | Search |
GET /.well-known/authzen-configuration | PDP metadata: endpoint URLs, supported features | Discovery |
permdockHandleris a Fetch handler (RequesttoResponse), so it mounts on Hono, Next.js route handlers, Node and any server kernel adapter.- The same handler is what PermDock Cloud runs as a hosted Authorization Decision Service: publish the policy, and Kong, Envoy, Tyk, Zuplo or a service in another language calls the hosted
/access/v1/evaluationwith a Vercel OIDC or client-credentials token and gets the samecontext(outcome, denial reasons, approvaltoken) that the embedded handler produces. Running the handler yourself and using the Cloud are interchangeable; the decision semantics are one code path (Cloud adapter, PermDock Cloud). resourcestells the handler how to load an instance by id (forwhereconditions on instance actions) and how to enumerate for resource search.subjectauthenticates the calling PEP or end user; see "decision endpoint auth" below.trustedPep(pep)is the allow-list of authenticated PEPs that may evaluate on behalf of another subject. It is off by default; a missing predicate,false, a non-function value or a throw means the request-body subject, actor and delegation are ignored.
Request lifecycle
- The handler authenticates the request via
subject. Unauthenticated requests get401; there is no anonymous evaluation unless the policy declares anonymous grants and the deployment opts in. - The AuthZEN request is validated:
subject,action,resource, optionalcontext, each withtype,idandproperties. - Mapping to PermDock:
| AuthZEN field | PermDock |
|---|---|
subject.type, subject.id, subject.properties | principal, only for a PEP that trustedPep accepts; properties.actor and properties.delegation (scopes / authorization_details) fill the agent half of the subject under the same rule. Otherwise the authenticated caller from subject is the subject and the body's subject is ignored |
action.name | joined with resource.type to look up findPermission(permissions, 'post.update'); action.properties.scope accepted as an alternative |
resource.type, resource.id, resource.properties | the resource instance: properties used directly when complete, otherwise loaded via resources.<type>.load |
context | context.<key> values available to conditions |
- The decision runs;
evaluationsusessimulateso a plan is evaluated as one boxcar with shared subject resolution. - The response is built:
decision: true|falseplus acontextobject carryingoutcome,denials(role and reason only) andtokenforapproval-required. Matched grants, conditions andalternativesare never included. on('decision')fires once per evaluation with the AuthZEN request id for correlation.
What it validates
- Request bodies against the AuthZEN schemas; malformed requests get
400with Problem Details. resource.propertiesagainst the resource's Standard Schema when they are used as the instance (boundary validation): a PEP is a trust boundary. When the handler loads the row itself, no validation runs.- Unknown
resource.typeoraction.name:decision: falsewith acontext.permdock.reasonofunknown-permission; never an exception. - Decision-endpoint authentication must be real authentication (bearer tokens, mTLS, session), not a shared static secret; Kilpi's public-secret obfuscation is an explicit anti-pattern (threat model). In-app,
subjectreads the application's session or asubjectFromJwtresult; on the hosted ADS, callers present a Vercel OIDC token or an OAuth client-credentials token verified withpermdock/jwt.
How denials surface
evaluation:{ "decision": false, "context": { "permdock": { "outcome": "denied", "denials": [{ "role": "member", "reason": "condition" }] } } }. Thecontextmember is optional in AuthZEN and PermDock always fills it so a PEP can explain the refusal. A PEP that wants what else the subject may do askssearch/action.approval-required:decision: falsewithcontext.permdock.outcome: 'approval-required'andcontext.permdock.token; the PEP decides how to obtain approval. AuthZEN has no third outcome, so this is afalsewith a reason incontext, not a profile-specific extension.evaluations: one result per item, in order; a batch never fails partially because one item is denied.options.evaluations_semanticisexecute_all(the default),deny_on_first_denyorpermit_on_first_permit; the two short-circuit semantics evaluate in order and stop after the first matching result, and any other value is a400. A request without anevaluationsarray, or with an empty one, is a single evaluation and answers{ decision, context }.- Search endpoints return the permitted subset; an empty page is the denial. Resource search filters the rows
resources.<type>.listyields in memory and returns each as a{ type, id }entity, readingidfrom the resource'sidfield and dropping rows without one. Subject search answers only forsubject.typeuser(or no type). Pagination follows the AuthZENpageobject: the request'spage.limit(default 50, at most 200) andpage.token, the response's offsetnext_token(empty on the last page),countandtotal. - Discovery at
/.well-known/authzen-configuration/<path>names<origin>/<path>aspolicy_decision_pointand lists every endpoint beneath it, so a path-qualified PDP identifier resolves to its own metadata. - Every response echoes the request's
X-Request-IDheader. - Transport errors use RFC 9457 Problem Details (
400,401,413for oversized batches).
Why
An AuthZEN PEP is another party: a gateway, a service in another language, sometimes another team's system. It needs enough to enforce and explain a refusal, and no more. The context therefore carries the outcome, the denial reasons and the approval token, and never the matched grant, its conditions or alternatives. Conditions reveal policy structure (which column gates a row), and alternatives reveal what else the subject could do; a PEP that needs the second asks search/action, which is an explicit, auditable question rather than a side effect of every denial. The application's own decision endpoint (permdockHandler) keeps returning the full Decision because its caller is the application's UI, inside the same trust boundary. Both nest PermDock's data under context.permdock: AuthZEN leaves context open to every PDP, so one namespaced member cannot collide with another vendor's keys, and a PEP that talks to several PDPs reads PermDock's fields from the same place in both responses.
Example app
apps/examples/authzen-pdp: node:http wrapper around the Fetch permdockHandler on 127.0.0.1:3470. GET /health. POST /access/v1/evaluation with Authorization: Bearer test grants post.update on the member's own post and denies post.publish.
Related standards
- AuthZEN: request and response schemas, search semantics, discovery, certification levels.
- Wire formats: the evaluation
contextshape. - Problem Details: transport errors.
Last updated on
A2A
permdock/a2a emits A2A Agent Cards whose skills carry security requirements derived from permissions and filters the authenticated extended card by the caller's grants.
Approvals
permdock/approvals is the pluggable store behind every approval-required decision, an in-memory default, a Fetch handler for approvers, and the interface that self-hosted stores and PermDock Cloud implement.