# OpenID AuthZEN

Source: https://permdock.com/docs/standards/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](#certification-checklist)).

## What it is [#what-it-is]

The [OpenID AuthZEN Authorization API 1.0](https://openid.net/specs/authorization-api-1_0.html) 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](https://github.com/openid/authzen/issues/433) 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 [#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](/docs/standards/authzen) and [wire formats](/docs/concepts/wire-formats).

## How PermDock uses it [#how-permdock-uses-it]

### PermDock as a PDP [#permdock-as-a-pdp]

```ts
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](/docs/security/threat-model) on why a shared public secret is not enough.

### PermDock as a hosted ADS [#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](/docs/adapters/cloud)).

### PermDock as a PEP [#permdock-as-a-pep]

The [`pdp` provider](/docs/adapters/pdp) 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 [#request-and-response]

An evaluation request for `permdock.decide(permissions.post.update, post)`:

```json
{
  "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](/docs/concepts/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):

```json
{
  "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 [#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](/docs/concepts/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 |
| `resource.properties` | The instance passed to `decide`; validated at the boundary |
| `context` | `delegation` (scopes, `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` |
| `/access/v1/evaluations` (boxcar) | `permdock.simulate([...])` and the batched React client |
| `/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](/docs/standards/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 [#conformance]

`testAuthZen(permdockHandler, { vectors })` from [`permdock/testing`](/docs/adapters/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](https://github.com/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 [#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](https://github.com/openid/authzen/issues/433) 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 [#sources]

* [OpenID AuthZEN Authorization API 1.0](https://openid.net/specs/authorization-api-1_0.html).
* [AuthZEN certification programme](https://github.com/openid/authzen/issues/433).
* [Landscape research](/docs/research/landscape) for the PDP vendor context and the Cerbos AuthZEN-with-MCP discussion.
* [Wire formats](/docs/concepts/wire-formats) for the AuthZEN mapping.
