PermDock
Standards

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 a subject, action, resource and optional context.
  • 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-known PDP 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; filter and where are resource search; simulate is a boxcar evaluations request.
  • 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.

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": { "scopes": ["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" }]
    }
  }
}

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. 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. 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 conceptPermDock concept
subject.type, subject.idprincipal (principal.id in policy conditions)
subject.propertiesPrincipal fields returned by definePolicy's subject function, plus actor
subject.properties.membershipsprincipal.memberships (named-scope and resource roles, tenancy)
context.tenantprincipal.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.namepermission.key (post.update) resolved with findPermission, or the bare action (update) of the resource named by resource.type
resource.typeResource node name (post)
resource.idValue of the resource's id field
resource.propertiesThe instance passed to decide; validated at the boundary
contextdelegation (scopes, authorization_details) and request context
decision: trueoutcome: 'granted'
decision: falseoutcome: 'denied' or 'approval-required' (distinguished in context.permdock.outcome)
Response context.permdockoutcome, denials as { role, reason }, token; the full Decision only from the application's own permdockHandler
/access/v1/evaluations (boxcar)permdock.simulate([...]) and the batched React client
/search/actionGranted keys from listPermissions for one resource (the catalog question)
/search/resourcefilter 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/subjectIterating 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 / DiscoverytestAuthZen 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

  1. Fetch the official vectors and run pnpm --filter permdock/testing test; every Basic and Batch vector passes against permdock/authzen.
  2. Deploy the interop Todo PDP (the Todo policy behind permdock/authzen, with trustedPep for the harness) at a public HTTPS origin whose /.well-known/authzen-configuration lists evaluation, evaluations and the three search endpoints.
  3. Run the upstream authzen-todo-backend and authzen-search-demo harnesses against that origin and keep their reports.
  4. 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.
  5. Re-run the suite when the pinned commit moves; bump the commit in the root scripts/fetch-authzen-vectors.ts and on this page together.

Sources

Last updated on

On this page