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.
Status: planned Phase: 2
permdock/authzen exposes a PermDock policy as an AuthZEN Policy Decision Point. One handler 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 (decision 0011) and aims for certification at all four levels.
API
import { createPermDock } from 'permdock/authzen'
export const { handler } = 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) => handler(c.req.raw))
app.get('/.well-known/authzen-configuration', (c) => handler(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 | 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 |
handleris 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 sameDecisionundercontext.permdockthat the embedded engine produces. Running the handler yourself and using the Cloud are interchangeable; the decision semantics are one code path (Cloud adapter, ADR 0021). 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.
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; properties.actor and properties.delegation (scopes / authorization_details) fill the agent half of the subject when present |
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 | subject.context 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 carrying the PermDock Decision (outcome,denials,alternatives,tokenforapproval-required). 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.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. This closes roadmap open question 8.
How denials surface
evaluation:{ "decision": false, "context": { "outcome": "denied", "denials": [...], "alternatives": [...] } }. Thecontextmember is optional in AuthZEN and PermDock always fills it so a PEP can explain the refusal.approval-required:decision: falsewithcontext.outcome: 'approval-required'andcontext.token; the PEP decides how to obtain approval. AuthZEN has no third outcome, so this is afalsewith a reason.evaluations: one result per item, in order; a batch never fails partially because one item is denied.- Search endpoints return the permitted subset; an empty page is the denial. Pagination follows the AuthZEN
pageobject. - Transport errors use RFC 9457 Problem Details (
400,401,413for oversized batches).
Example app
apps/examples/authzen-pdp: a Hono app serving the full endpoint set for the post policy, a metadata document, a script that runs the AuthZEN interop test suite request shapes against it, and a second process using the pdp provider as a PEP so both halves are exercised in one repo.
Related standards
- AuthZEN: request and response schemas, search semantics, discovery, certification levels.
- Wire formats: the PermDock Decision embedded in
context. - Problem Details: transport errors.
- Decision 0011: why AuthZEN rather than a bespoke format.
Open questions
search/subjectneeds a subject enumerator (a users table or directory query) that PermDock does not own; the handler may declaresubjects.listas an optional capability and omit the endpoint from discovery when absent.- How
approval-requiredshould be signalled given AuthZEN's booleandecision:falsepluscontext(current) versus a profile-specific extension. - Whether resource search should return ids only or full
properties, and how large results are paginated forwherecompilers that produce SQL. - How the React decision endpoint's batching and per-permission cache keys map onto
evaluationswithout over-fetching. - Certification: which test vectors the interop suite requires for Search and Discovery beyond the published profile.
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.