OpenID AuthZEN
How PermDock speaks the OpenID AuthZEN Authorization API 1.0 as a policy decision point (permdock/authzen) and as a policy enforcement point (the pdp provider).
permdock/authzen serves the full endpoint set, the React adapter's batched client speaks the same schemas, and the pdp provider calls a remote PDP. Basic, Batch, Search and Discovery conformance runs in the repository through testAuthzen from permdock/testing, including the official interop Todo vectors; submitting for external certification is the last step (checklist).
What it is
The OpenID AuthZEN Authorization API 1.0 went final in January 2026. It standardises the request and response between a policy enforcement point (PEP, the thing asking) and a policy decision point (PDP, the thing answering):
POST /access/v1/evaluation: one decision for asubject,action,resourceand optionalcontext.POST /access/v1/evaluations: a batched, "boxcar" request evaluating many subject/action/resource tuples in one round trip.POST /access/v1/search/subject,/search/resource,/search/action: given two of the three, list the third (who can do this, which resources can this subject act on, what can this subject do on this resource).- A
.well-knownPDP metadata document for discovery. - A certification programme with Basic, Batch, Search and Discovery levels.
Keycloak and the NLgov profile implement it, and PDP vendors including Cerbos, Topaz, Axiomatics and PlainID participate in the interop work.
Why it matters for PermDock
PermDock needs a decision endpoint anyway: the React client asks the server for grants backed by closures, the pdp provider defers to a remote PDP, and simulate() batch-evaluates an agent's plan. Inventing a wire format for that (as Kilpi's endpoint plugin did) means every other tool has to learn it. Speaking AuthZEN means:
- Cerbos, Topaz, Keycloak or any certified PDP can call PermDock, and PermDock can call them, without translation.
- The catalog question "what can this user do on this resource" is AuthZEN action search;
filterandwhereare resource search;simulateis a boxcarevaluationsrequest. - Certification is a concrete, external proof of correctness that no TypeScript permissions library has today.
See AuthZEN and wire formats.
How PermDock uses it
PermDock as a PDP
import { createPermDock } from "permdock/authzen";
export const { permdockHandler } = createPermDock(policy, {
subject: fromBearer,
});
// Serves /access/v1/evaluation, /evaluations, /search/action, /search/resource, /search/subject,
// and .well-known/authzen-configuration.The Next.js permdockHandler() and the React endpoint use the same request and response schemas, so the client decision endpoint is an AuthZEN PDP with a restricted policy: it only answers for the authenticated caller's own subject and ignores any subject in the request body. The endpoint must sit behind the application's real authentication; see the threat model on why a shared public secret is not enough.
PermDock as a hosted ADS
PermDock Cloud runs the same permdock/authzen handler against a published policy as an Authorization Decision Service. Because AuthZEN is the wire format, an API gateway with an AuthZEN policy enforcement point (Kong, Envoy, Tyk, Zuplo, WSO2) or a service in Go, Python or Java enforces a TypeScript-authored policy with no PermDock SDK. Callers authenticate with a Vercel OIDC token or OAuth client credentials; the response is the standard decision plus context.permdock carrying outcome, denial reasons and the approval token, exactly as from the embedded handler. Running the handler yourself remains the default (Cloud adapter).
PermDock as a PEP
The pdp provider turns a remote AuthZEN PDP into a grant source: a role's grants can be resolved by calling /access/v1/evaluation (or /evaluations for simulate), and the response is folded into the same Decision union as local grants. deny still overrides allow, and a network failure is a denial, never a grant.
Zero trust roles (NIST SP 800-207)
Posture: name. NIST SP 800-207 (Zero Trust Architecture, August 2020) splits access control into logical components. AuthZEN gives the PDP-to-PEP leg a wire format, and PermDock fills each role in process or over that wire:
| SP 800-207 component | PermDock |
|---|---|
| Policy engine (PE) | decide over the policy: grants, conditions, deny-overrides, delegation intersection |
| Policy administrator (PA) | The application, or PermDock Cloud for hosted grants and memberships; approvalsHandler for per-request approval |
| Policy decision point (PDP = PE + PA) | An in-process instance, or permdock/authzen serving /access/v1/evaluation |
| Policy enforcement point (PEP) | protect in the HTTP adapters, protectServer in MCP, the agent adapters' tool hooks, generated RLS in Postgres; permdock/pdp when the decision is remote |
| Policy information point (PIP) | The subject resolver, data loaders, MembershipSource, RelationSource and customRoles; identity from the token, never from the request body |
| Continuous diagnostics and mitigation | on('decision') and permdock/otel as inputs; CAEP and RevocationFeed invalidate snapshots when a session or credential changes (Shared Signals) |
- Every request is decided per resource and per call (800-207 tenets 3 and 6); a snapshot is a client-side listing hint, and the server decides again.
- An unknown subject, a missing PIP answer or an unreachable PDP denies, matching the architecture's default-deny stance.
- PermDock does not manage devices, networks or micro-segmentation; those inputs reach a decision only as
contextthe application supplies.
Request and response
An evaluation request for permdock.decide(permissions.post.update, post):
{
"subject": {
"type": "user",
"id": "u_123",
"properties": {
"roles": [],
"memberships": [
{ "tenant": "org_9", "roles": ["member"] },
{
"tenant": "org_9",
"team": "t_design",
"roles": ["lead"],
"via": "group:9f2c"
}
],
"actor": { "type": "mcp-client", "id": "https://agent.example/cimd.json" }
}
},
"action": { "name": "post.update" },
"resource": {
"type": "post",
"id": "p_42",
"properties": { "authorId": "u_123", "orgId": "org_9", "published": false }
},
"context": { "tenant": "org_9", "delegation": { "scope": "post:update" } }
}subject.properties.memberships carries the tenancy memberships (AuthZEN 1.0 names group memberships as an example subject property) and context.tenant the active tenant, because the tenant is a property of the request, not of the subject. A single-tenant policy that still uses orgId on the principal sends it as a property as before.
The response carries the boolean AuthZEN decision plus a context.permdock member so PEPs that understand it get the outcome, the denial reasons and the approval token; the matched grant and alternatives stay inside the PDP (search/action answers the second):
{
"decision": false,
"context": {
"permdock": {
"outcome": "denied",
"denials": [{ "role": "member", "reason": "condition" }]
},
"reason_admin": { "en": "post.update denied: member (condition)." },
"reason_user": { "en": "post.update is not permitted." }
}
}A grant is "decision": true with context.permdock.outcome equal to granted; an approval-required outcome is "decision": false so AuthZEN-only PEPs fail closed, with context.permdock.outcome equal to approval-required and context.permdock.token for the approval flow. A denied or approval-required result also carries the AuthZEN 1.0 section 7.2.1 reasons: reason_admin names the denying roles and reasons, and reason_user names only the permission. The application's own decision endpoint (permdockHandler) returns the full Decision under the same member, because its caller is the application's UI. PermDock does not propose a separate AuthZEN-level signal for it: a false that a PermDock-aware PEP can refine is the fail-closed reading every other PEP already gets.
The context keys for tenant, actor and delegation are PermDock's names, listed in the mapping table below. delegation uses the OAuth wire names, a space-separated scope string and RFC 9396 authorization_details. If AuthZEN defines names for them, PermDock adopts the specification's names.
The boxcar form wraps many { action, resource } pairs under one subject; a top-level subject, action, resource or context is the default for every item that omits it, and is what simulate() sends; resource search maps to filter / where; action search maps to iterating listPermissions(permissions) for one resource and returning the granted keys.
Mapping table
| AuthZEN 1.0 concept | PermDock concept |
|---|---|
subject.type, subject.id | principal (principal.id in policy conditions) |
subject.properties | Principal fields returned by definePolicy's subject function, plus actor |
subject.properties.memberships | principal.memberships (named-scope and resource roles, tenancy) |
context.tenant | principal.tenant, the active tenant for this request; absent means no tenant, never a default |
.well-known/authzen-configuration/<tenant> | Per-tenant PDP metadata served by permdockHandler and the hosted ADS when the deployment is multi-tenant |
action.name | permission.key (post.update) resolved with findPermission, or the bare action (update) of the resource named by resource.type |
resource.type | Resource node name (post) |
resource.id | Value of the resource's id field; "*" names the collection, a check with no row |
resource.properties | The instance passed to decide; validated at the boundary |
context | delegation (scope, authorization_details) and request context |
decision: true | outcome: 'granted' |
decision: false | outcome: 'denied' or 'approval-required' (distinguished in context.permdock.outcome) |
Response context.permdock | outcome, denials as { role, reason }, token; the full Decision only from the application's own permdockHandler |
Response context.reason_admin, context.reason_user | Section 7.2.1 reasons in en on a denied or approval-required result; reason_user names no role, grant or subject |
/access/v1/evaluations (boxcar) | permdock.simulate([...]) and the batched React client |
options.evaluations_semantic | execute_all (default), deny_on_first_deny or permit_on_first_permit; an unknown value is a 400 |
X-Request-ID | Echoed on every response when the request sends one |
/search/action | Granted keys from listPermissions for one resource (the catalog question) |
/search/resource | filter for arrays, where for query compilers; each permitted row is returned as { type, id }, with id read from the resource's id field and rows without one dropped |
/search/subject | Iterating the subjects returned by the subjects.list option; without it the route answers 404 and the metadata document omits search_subject_endpoint |
/search/resource as a claim source (AuthZEN claims draft) | permdock.heldRoles({ tenant }) and memberships() for the authenticated subject (JWT authorization claims) |
.well-known metadata | .well-known/authzen-configuration served by permdockHandler |
| Certification levels Basic / Batch / Search / Discovery | testAuthzen from permdock/testing over the interop Todo domain; external submission per the checklist below |
Conformance
testAuthzen(permdockHandler(), { vectors }) from permdock/testing runs vectors in the interop harness layout (evaluation and evaluations arrays of { request, expected }, and search.subject, search.resource, search.action lists compared order-insensitively) against any (Request) => Promise<Response>, and checks /.well-known/authzen-configuration names the same origin as every endpoint it lists. It works for PermDock's own permdockHandler and for any other AuthZEN PDP.
The package ships the interop Todo domain as authzenTodoPermissions, authzenTodoPolicy, authzenTodoUsers and authzenTodoData (Rick is admin and evil_genius, Morty and Summer are editor, Beth and Jerry are viewer; editors update and delete only todos whose ownerID is their email), and authzenTodoVectors() generates PermDock's own vectors from those rules. The official vectors live in openid/authzen, which carries no licence file, so the repository does not vendor them: pnpm authzen:vectors (the root scripts/fetch-authzen-vectors.ts) fetches decisions-authorization-api-1_0-02.json at commit 78a5165a0048895a345e4ac5b0f2b9c7904bb110 into the gitignored fixtures/authzen/, and the suite runs it whenever the file is present.
Certification checklist
- Fetch the official vectors and run
pnpm --filter permdock/testing test; every Basic and Batch vector passes againstpermdock/authzen. - Deploy the interop Todo PDP (the Todo policy behind
permdock/authzen, withtrustedPepfor the harness) at a public HTTPS origin whose/.well-known/authzen-configurationlists evaluation, evaluations and the three search endpoints. - Run the upstream
authzen-todo-backendandauthzen-search-demoharnesses against that origin and keep their reports. - Add PermDock to the interop results in the openid/authzen repository and file the submission on the certification programme for Basic, Batch, Search and Discovery.
- Re-run the suite when the pinned commit moves; bump the commit in the root
scripts/fetch-authzen-vectors.tsand on this page together.
Sources
- OpenID AuthZEN Authorization API 1.0.
- NIST SP 800-207, Zero Trust Architecture, sections 2.1 (tenets) and 3 (logical components).
- AuthZEN certification programme.
- Landscape research for the PDP vendor context and the Cerbos AuthZEN-with-MCP discussion.
- Wire formats for the AuthZEN mapping.
Last updated on
MCP Apps
How permdock/mcp decides who may see an MCP Apps ui:// view and call the tools it drives.
OAuth for agent delegation
How RFC 9396 Rich Authorization Requests, RFC 8693 token exchange, DPoP, CIBA and the IETF agent-delegation drafts shape PermDock's two-principal subject and the rule that an agent can never exceed its user.