NGINX-based reference implementation that enforces authorization decisions from Knooppunt’s PDP.
The PEP uses nginx subrequests to proxy calls to Nuts node and PDP via internal locations.
# Start PEP with rest of stack
docker compose --profile pep up -d
# Test requires a valid OAuth token from Nuts node
# The PEP will introspect the token and validate DPoP if present
Endpoints:
http://localhost:9080http://localhost:8081/pdp (internal API)Authorization header/internal/auth/v2/accesstoken/introspect (RFC 7662)cnf.jkt claim via /internal/auth/v2/dpop/validate (RFC 9449)Environment variables in docker-compose.yml:
# FHIR API path exposed by PEP (default: /fhir)
FHIR_BASE_PATH=/fhir
# FHIR path on backend server (default: same as FHIR_BASE_PATH)
# Use for HAPI multi-tenancy: /fhir/DEFAULT
FHIR_UPSTREAM_PATH=/fhir
# Backend connections
FHIR_BACKEND_HOST=hapi-fhir
FHIR_BACKEND_PORT=7050
KNOOPPUNT_PDP_HOST=knooppunt
KNOOPPUNT_PDP_PORT=8081
# Nuts node connection (Authorization Server)
NUTS_NODE_HOST=knooppunt
NUTS_NODE_INTERNAL_PORT=8081
# Data holder (this organization)
DATA_HOLDER_ORGANIZATION_URA=00000666
DATA_HOLDER_FACILITY_TYPE=Z3
# Security: Configure expected hostname for DPoP URL validation
# Prevents Host header spoofing attacks. Falls back to Host header if not set.
PEP_HOSTNAME=pep.example.com
The Presentation Definition (PD) is configured on the authorization server (Nuts node), not in the PEP. For BgZ use cases, the PD typically requires:
See test/e2e/pep/testdata/accesspolicy.json for an example PD used in e2e tests.
Claims flow directly from the Presentation Definition to the PDP without any mapping in the PEP:
Verifiable Credentials Presentation Definition Introspection Response PDPInput
│ │ │ │
│ ┌────────────────────┘ │ │
▼ ▼ │ │
VC claims extracted using PD field constraints ──────────►│ │
│ │ │
│ The `id` field in each PD constraint │ │
│ becomes the claim name in introspection │ │
│ ▼ │
│ { "active": true, │
│ "subject_id": "...", │
│ "subject_role": "..." │
│ } │
│ │ │
│ │ passed through │
│ └───────────────────►│
The PEP passes claims directly from introspection to the PDP. The Presentation Definition must use
id fields that match what the PDP expects.
The PEP passes all non-standard claims through to the PDP. The PDP expects these id values in the PD constraints:
PD Constraint id |
VC Path Example | Description |
|---|---|---|
subject_id |
$.credentialSubject.identifier |
Employee/practitioner identifier |
subject_role |
$.credentialSubject.roleName |
Role code (e.g., “Medisch Specialist”) |
subject_organization_id |
$.credentialSubject.san.otherName |
URA number |
subject_organization |
$.credentialSubject.subject.O |
Organization name |
subject_facility_type |
$.credentialSubject.roleCodeNL |
Vektis facility type code |
Additionally, the PDP receives from OAuth:
client_id - OAuth client identifier (DID)scope - OAuth scopes (converted to client_qualifications array)The PEP sends requests matching the Go PDPInput struct:
POST /pdp
{
"input": {
"subject": {
"type": "organization",
"id": "did:web:example.com",
"properties": {
"client_id": "did:web:example.com",
"client_qualifications": [
"bgz"
],
"subject_id": "000095254",
"subject_role": "medical-specialist",
"subject_organization_id": "87654321",
"subject_organization": "Test Hospital B.V.",
"subject_facility_type": "Z3"
}
},
"request": {
"method": "GET",
"protocol": "HTTP/1.1",
"path": "/Condition",
"query_params": {
"patient": [
"Patient/patient-123"
]
},
"header": {},
"body": ""
},
"context": {
"data_holder_organization_id": "12345678",
"data_holder_facility_type": "Z3",
"patient_bsn": ""
}
}
}
Expected response:
{
"result": {
"allow": true,
"reasons": []
}
}
When the introspection response includes a cnf.jkt claim, the PEP validates DPoP:
DPoP header from request/internal/auth/v2/dpop/validate via internal subrequestThis ensures the access token is bound to the client’s proof-of-possession key.
cd pep/nginx/js
npm install
npm test
# Run from repository root
go test ./test/e2e/pep/... -v
The e2e test (in test/e2e/pep/authorization_test.go):
bgzThe PEP requires:
/internal/auth/v2/accesstoken/introspect/internal/auth/v2/dpop/validateLimitations:
Considerations for production: