IDPartnersidp-auth-peps · reference
demo/ · compose

The demo

The demo compares configured, resource-based and federation-based PDP discovery. It runs three instances of coaz-pep against stub resources and PDPs. The console shows the metadata retrieved and the decision for each request. Optional Compose profiles add Kong and PingAccess.

Run it

cd demo && docker compose up --build -d && ./demo.sh        # the scripted walk
open http://localhost:8088                                        # the console

docker compose --profile kong up -d            # add Kong on :8000, the plugin doing discovery in Lua
docker compose --profile pingaccess up -d      # add PingAccess on :3000 (needs Ping DevOps credentials in demo/.env)

demo/run-local.sh --console                    # the same without Docker; needs Go

Demo services

StubPortWhat it is
anchor9000A federation trust anchor. Its policy for members: authzen_policy_set is the estate PDP then Bank A's, and acr_values_required is [MFA], whatever the member says. It onboards entities on request.
member9001A federated resource. Its own RFC 9728 document names Bank A's PDP and says a password is enough; its entity configuration's set names the rogue PDP first. The anchor's policy replaces that set with its own, the estate PDP then Bank A's, and raises the required authentication level to MFA.
pdp-a9002Bank A's PDP, identifier …:9002/tenants/bank-a. Holds the token to what the resource published; steps up payments over 1000; advertises a batch endpoint.
rogue-pdp9003Permits everything, logs loudly when asked, advertises no batch endpoint.
plain9004Not federated. RFC 9728 metadata names Bank A's PDP; accepts a password.
impostor9005Not federated. RFC 9728 metadata names the rogue PDP.
broken9006Federated, but signs with a key the anchor never vouched for.
stray9007No metadata of any kind.
pdp-b9008Bank B's PDP, identifier …:9008/tenants/bank-b. Same product, stricter threshold: steps up payments over 100.
bank-b9009Not federated. RFC 9728 metadata has an authzen_policy_set that puts the estate PDP in front of Bank B's; requires MFA.
pdp-estate9098A generic PDP for the whole estate: judges the token and the client (agent-risky is on its watch list), knows nothing about any resource. The first layer.
control9099The event feed the console traces, and the controls it pulls. Not part of any spec.
the gateway's own APIpep-federation:9192Not a stub. pep-federation is the federation entity for the API it fronts: it holds a key, publishes a minimal entity configuration, and republishes what the anchor resolves as that API's RFC 9728 document.

The demo also runs three coaz-pep instances. pep-static on :9192 uses the configured PDP. pep-resource on :9193 uses resource metadata. pep-federation on :9194 resolves federation metadata.

The compose file

demo/docker-compose.yml, complete. Everything is plain http on a private network, hence PDP_DISCOVERY_INSECURE; do not copy that line anywhere real. Allowlists list each stub with its port because an entry matches scheme, host and port.

services:
  stubs:
    build: { context: ../core, target: demo-stubs }
    environment:
      STUB_HOST: stubs
      ANCHORS_FILE: /shared/anchors.json
    volumes: [ "shared:/shared" ]
    ports: [ "9000-9009:9000-9009", "9098-9099:9098-9099" ]
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9099/healthz"]
      interval: 2s
      timeout: 2s
      retries: 15

  pep-static:
    build: { context: ../core, target: coaz-pep }
    environment: &pep-env
      AUTHZEN_URL: http://stubs:9002/tenants/bank-a
      AUTHZEN_API_KEY: static-pdp-key
      CHECK_API_TOKEN: demo
      PEP_ALLOW_INSECURE: "true"   # unsigned demo tokens, no JWKS: coaz-pep refuses to start without this. Never anywhere real
      PDP_DISCOVERY_INSECURE: "true"
      MCP_UPSTREAM_ALLOWLIST: http://stubs:9001,http://stubs:9004,http://stubs:9005,http://stubs:9006,http://stubs:9009
      PDP_METADATA_TTL: 15s        # short so the console's trace shows fetches on every run; the shipped default is 5m
    ports: [ "9192:9192" ]
    depends_on: { stubs: { condition: service_healthy } }

  pep-resource:
    build: { context: ../core, target: coaz-pep }
    environment:
      <<: *pep-env
      PDP_DISCOVERY: resource
      PDP_DISCOVERY_INSECURE: "true"
      RESOURCE_METADATA_ALLOWLIST: http://stubs:9001,http://stubs:9004,http://stubs:9005,http://stubs:9006,http://stubs:9007,http://stubs:9009,http://pep-federation:9192
      # deliberately no PDP_ALLOWLIST (logged at boot): the impostor may name the rogue
    ports: [ "9193:9192" ]
    depends_on: { stubs: { condition: service_healthy } }

  pep-federation:
    build: { context: ../core, target: coaz-pep }
    environment:
      <<: *pep-env
      PDP_DISCOVERY: federation
      PDP_DISCOVERY_INSECURE: "true"
      RESOURCE_METADATA_ALLOWLIST: http://stubs:9001,http://stubs:9004,http://stubs:9005,http://stubs:9006,http://stubs:9007,http://stubs:9009,http://pep-federation:9192
      PDP_ALLOWLIST: http://stubs:9002,http://stubs:9008,http://stubs:9098
      FEDERATION_TRUST_ANCHORS_FILE: /shared/anchors.json
      FEDERATION_FETCH_ALLOWLIST: http://stubs:9000
      FEDERATION_ENTITY_ID: http://pep-federation:9192
      FEDERATION_ENTITY_KEY_FILE: /tmp/entity-key.json
      FEDERATION_ENTITY_KEY_GENERATE: "true"
      FEDERATION_AUTHORITY_HINTS: http://stubs:9000
    volumes: [ "shared:/shared:ro" ]
    ports: [ "9194:9192" ]
    depends_on: { stubs: { condition: service_healthy } }

  console:
    build: { context: ../core, target: demo-console }
    environment:
      LISTEN: ":8088"
      STUBS_BASE: http://stubs
      STUBS_CONTROL: http://stubs:9099
      PEP_STATIC: http://pep-static:9192
      PEP_RESOURCE: http://pep-resource:9192
      PEP_FEDERATION: http://pep-federation:9192
      GATEWAY_ENTITY: http://pep-federation:9192
      CHECK_API_TOKEN: demo
      DOCS_DIR: /srv/docs
    volumes: [ "../docs/reference:/srv/docs:ro" ]
    ports: [ "8088:8088" ]
    depends_on: { stubs: { condition: service_healthy } }

  kong:
    profiles: ["kong"]
    image: kong:3.9
    environment:
      KONG_DATABASE: "off"
      KONG_DECLARATIVE_CONFIG: /kong/kong.yml
      KONG_PLUGINS: bundled,authzen-pdp
      KONG_LUA_PACKAGE_PATH: /opt/?.lua;;
      KONG_PROXY_LISTEN: 0.0.0.0:8000
      KONG_ADMIN_LISTEN: "off"
      KONG_LOG_LEVEL: notice
      KONG_PROXY_ERROR_LOG: /dev/stderr
    volumes:
      - ./kong/kong.yml:/kong/kong.yml:ro
      - ../gateways/kong/authzen-pdp:/opt/kong/plugins/authzen-pdp:ro
    ports: [ "8000:8000" ]
    depends_on: { stubs: { condition: service_healthy } }

  pingaccess:
    profiles: ["pingaccess"]
    build: { context: .., dockerfile: demo/pingaccess/Dockerfile }
    environment:
      PING_IDENTITY_ACCEPT_EULA: "YES"
      PING_IDENTITY_DEVOPS_USER: ${PING_IDENTITY_DEVOPS_USER:-}
      PING_IDENTITY_DEVOPS_KEY: ${PING_IDENTITY_DEVOPS_KEY:-}
      AUTHZEN_URL: http://stubs:9002
      AUTHZEN_API_KEY: static-pdp-key
      RESOURCE: http://stubs:9004
      SITE_TARGET: stubs:9004
      PEP_LABEL: "PingAccess (resource discovery)"
    ports: [ "3000:3000", "9443:9000" ]      # the engine; the admin console (administrator / 2FederateM0re)
    depends_on: { stubs: { condition: service_healthy } }

volumes:
  shared:

The console

Pick an API and a request, press Discover, and each of the three gateways shows the chain it followed, top to bottom: the document it read to find the PDP, the PDP's own metadata and the endpoint used, the context it handed the PDP, and last the decision.

The controls

The demo console with API and request selectors, token details, Discover and scenario controls
The controls. The API behind the gateway, the request, and the controls. The disclosure below Discover holds the token and the route's policy.
The token and route policy disclosure opened: client, acr, scopes, forward, layers
The token and the route's policy. Who the token was issued to, how the customer authenticated, which scopes it carries, whether the route forwards it, and the route's layers. Each maps to a per-route setting on the PEPs: forward_access_token, pdp_layers, and the token's claims.
ControlOptionsWhat it sets
The API behind the gatewayplain, bank-b, impostor, member, broken, stray, the gateway's own API, no resourceThe route's resource, which discovery starts from.
The requestread a balance, pay 50, pay 500, pay 5000, MCP transfer (batch)The method, path and body; the MCP one is a tools/call whose mapping is a two-evaluation boxcar.
The token was issued toagent-1, agent-riskyThe token's client_id. The estate PDP has agent-risky on its watch list.
Authenticated withpassword, MFAThe token's acr.
Scopesread only, read + paymentsThe token's scope.
Forward the raw tokenyes, noThe route's forward_access_token.
Policy layersthe API's PDP alone; the estate PDP first; the same, estate layer fail-openThe route's pdp_layers: nothing, http://stubs:9098,resource, or http://stubs:9098 fail-open,resource.

The chain

The member resource in three discovery modes. Federation metadata requires MFA and the PDP denies the request.
The member request uses a read-only token authenticated with a password. The static and resource-discovery evaluations permit it. Federation metadata policy removes the rogue PDP and requires multi-factor authentication (MFA). The PDP receives that resolved metadata and denies the request.
The console after discovering bank-b with a password token: the discovering gateways deny for acr
Bank B publishes an MFA requirement in its resource metadata. The static evaluation permits the password-authenticated request. Both discovery modes forward the requirement to the PDP, which denies the request.

Demo controls

The console controls change the demo state while it runs. Enforcement points read the updated metadata on their next refresh. They keep their existing configuration.

The console after relocating Bank A's PDP: the discovering gateways follow, the static one posts to a stale base
Relocate Bank A's PDP. Its metadata advertises the same AuthZEN endpoints under a new base, /tenants/bank-a-v2. The two gateways that read metadata follow; the static one keeps posting to the base it assumed, marked stale. A gateway using a configured endpoint needs a configuration update to use the new address.
The console with the estate PDP down and the estate layer marked fail-open: permits marked FAILED OPEN
Take the estate PDP down, with the fail-open layers option. The estate layer's PDP answers 503. Because the route marked that layer fail-open, it is skipped and Bank A's PDP alone decides; the permit is marked and X-PDP-Fail-Open names what was skipped. With the plain layers option the same outage is a 503.
The gateway's own API before onboarding: the entity configuration, and the anchor knowing nothing about it
The gateway's own API, before onboarding. pep-federation publishes a minimal entity configuration; the anchor's fetch endpoint knows nothing about it, so there is no chain and the RFC 9728 document is self-asserted.
The gateway's resource metadata after federation enrolment
After enrolment, the stub trust anchor issues a subordinate statement for the gateway. The gateway resolves the trust chain and republishes the federation metadata. Its RFC 9728 document now names Bank A's PDP and the required scopes and authentication level.

The control API

The console relays /api/control to the stubs' :9099/control endpoint. GET returns the current state. POST {"op": …} changes it.

OpEffect
move_pdpBank A's PDP advertises its endpoints under /tenants/bank-a-v2. It still answers on the old base, so nothing breaks mid-flight; the trace shows who followed.
repoint_plainThe plain resource's metadata names Bank B's PDP instead. No gateway is touched.
estate_down / estate_upThe estate PDP's evaluation endpoints answer 503; its metadata stays up.
onboard / offboard with "entity"The anchor fetches the entity's configuration, checks it is self-signed, and starts (or stops) vouching for its keys, saying what the resource requires in its subordinate statement.
resetEverything back.
curl -s -X POST -H 'Content-Type: application/json' -d '{"op":"onboard","entity":"http://pep-federation:9192"}' http://localhost:9099/control
curl -s http://localhost:9099/control | jq .
{ "onboarded": ["http://pep-federation:9192"], "pdp_down": [],
  "pdp_evaluation_path": { "pdp-a": "/tenants/bank-a/access/v1/evaluation", … },
  "pdp_home": { "pdp-a": "/tenants/bank-a", … },
  "resource_pdps": { "plain": ["http://stubs:9002/tenants/bank-a"], … } }

The scripted walkthrough

demo/demo.sh sends the same requests to the three PEPs' HTTP check APIs. It prints each decision with the response status and body. The walkthrough covers discovery and federation metadata policy. It also demonstrates policy layers and challenge responses. Set token claims through CLIENT, ACR and SCOPE. Set FORWARD=no or LAYERS for each call.

5. Failing open, deliberately: the estate PDP goes down estate DOWN, layers as before (fail-closed): 503 {"decision":false,"status":503,"body":{"error":"temporarily_unavailable","error_description":"Authorization service unavailable; denying (fail-closed).","status":503}} estate DOWN, estate layer marked fail-open: Bank A decides {"decision":true,"status":null,"body":null,"failed_open":"http://stubs:9098 (PDP returned 503)"} estate back up, same fail-open policy: no marker {"decision":true,"status":null,"body":null} 6. The gateway as the resource's metadata publisher: it holds the key, the anchor holds the policy its RFC 9728 document before onboarding {"authzen_policy_set":{"combining":"deny-overrides","members":["http://stubs:9002/tenants/bank-a"]},"jwks_uri":"http://pep-federation:9192/.well-known/oauth-protected-resource/jwks.json","metadata_source":"self","resource":"http://pep-federation:9192"} after onboarding (the PEP re-walks its chain when its cache lapses) {"acr_values_required":["urn:idp:loa:mfa"],"authzen_policy_set":{"combining":"deny-overrides","members":["http://stubs:9098","http://stubs:9002/tenants/bank-a"]},"jwks_uri":"http://pep-federation:9192/.well-known/oauth-protected-resource/jwks.json","metadata_source":"federation","resource":"http://pep-federation:9192","scopes_supported":["accounts:read","payments:write"]}

Hosted, on the site

The demo runs at federated-enforcement.idpartners.global/demo/, in the site's own container. site/Dockerfile builds the PEPs (coaz-pep), the stubs and the console from core/, or takes them from the coaz-pep and coaz-pep-demo images named by the COAZ_PEP_IMAGE and DEMO_IMAGE build arguments; site/start.sh starts them on localhost and nginx serves the console at /demo/. The same, on your machine, from the repository root:

docker build -f site/Dockerfile -t federated-enforcement .
docker run --rm -p 8080:8080 federated-enforcement      # then http://localhost:8080/demo/

The controls change shared state and require no authentication. Concurrent users can change each other's scenarios.