PermDock
Adapters

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))
EndpointPermDock callCertification level
POST /access/v1/evaluationdecide(permission, resource)Basic
POST /access/v1/evaluationssimulate([[permission, resource], ...])Batch
POST /access/v1/search/actioncatalog of permitted actions on a resource for the subject ("what can I do")Search
POST /access/v1/search/resourcefilter / where over a resource type, materialised via resources.<type>.listSearch
POST /access/v1/search/subjectsubjects permitted for an action on a resource (requires a subject enumerator)Search
GET /.well-known/authzen-configurationPDP metadata: endpoint URLs, supported featuresDiscovery
  • handler is a Fetch handler (Request to Response), 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/evaluation with a Vercel OIDC or client-credentials token and gets the same Decision under context.permdock that 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).
  • resources tells the handler how to load an instance by id (for where conditions on instance actions) and how to enumerate for resource search.
  • subject authenticates the calling PEP or end user; see "decision endpoint auth" below.

Request lifecycle

  1. The handler authenticates the request via subject. Unauthenticated requests get 401; there is no anonymous evaluation unless the policy declares anonymous grants and the deployment opts in.
  2. The AuthZEN request is validated: subject, action, resource, optional context, each with type, id and properties.
  3. Mapping to PermDock:
AuthZEN fieldPermDock
subject.type, subject.id, subject.propertiesprincipal; properties.actor and properties.delegation (scopes / authorization_details) fill the agent half of the subject when present
action.namejoined with resource.type to look up findPermission(permissions, 'post.update'); action.properties.scope accepted as an alternative
resource.type, resource.id, resource.propertiesthe resource instance: properties used directly when complete, otherwise loaded via resources.<type>.load
contextsubject.context values available to conditions
  1. The decision runs; evaluations uses simulate so a plan is evaluated as one boxcar with shared subject resolution.
  2. The response is built: decision: true|false plus a context object carrying the PermDock Decision (outcome, denials, alternatives, token for approval-required).
  3. 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 400 with Problem Details.
  • resource.properties against 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.type or action.name: decision: false with a context.reason of unknown-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, subject reads the application's session or a subjectFromJwt result; on the hosted ADS, callers present a Vercel OIDC token or an OAuth client-credentials token verified with permdock/jwt. This closes roadmap open question 8.

How denials surface

  • evaluation: { "decision": false, "context": { "outcome": "denied", "denials": [...], "alternatives": [...] } }. The context member is optional in AuthZEN and PermDock always fills it so a PEP can explain the refusal.
  • approval-required: decision: false with context.outcome: 'approval-required' and context.token; the PEP decides how to obtain approval. AuthZEN has no third outcome, so this is a false with 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 page object.
  • Transport errors use RFC 9457 Problem Details (400, 401, 413 for 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.

  • 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/subject needs a subject enumerator (a users table or directory query) that PermDock does not own; the handler may declare subjects.list as an optional capability and omit the endpoint from discovery when absent.
  • How approval-required should be signalled given AuthZEN's boolean decision: false plus context (current) versus a profile-specific extension.
  • Whether resource search should return ids only or full properties, and how large results are paginated for where compilers that produce SQL.
  • How the React decision endpoint's batching and per-permission cache keys map onto evaluations without over-fetching.
  • Certification: which test vectors the interop suite requires for Search and Discovery beyond the published profile.

On this page