PID Issuer architecture
This page is a code-oriented companion to the protocol-level sequence diagram at
Issuance with OpenID4VCI.
That diagram treats the PID Issuer as a black box and shows what goes over the
wire between the wallet, the PID Issuer and RDO Max. This page opens that box:
which crates contribute which pieces, where state lives, and which traits are
the extension points for plugging in a different backend.
Crate map
The PID Issuer process is assembled from three crates:
wallet_core/lib/openid4vc— protocol types and traits, with no HTTP or storage baked in. Relevant pieces:authorization::PushedAuthorizationRequest,authorization::PushedAuthorizationResponse,authorization::AuthorizationRequest,token::TokenRequest,token::TokenResponse,credential::CredentialRequest(s),credential::CredentialResponse(s).issuer::AttributeService— the trait that produces the attributes to be issued given aTokenRequest.issuer::Issuer— the protocol state machine: verifies token/credential requests, drives the session, calls into theAttributeService, generates access tokens, etc.process_token_requestaccepts an optionalUpstreamCodeVerifierthat it forwards verbatim to theAttributeService— theIssuernever inspects or interprets the value.par::ParStore,nonce::store::NonceStore,server_state::SessionStore<IssuanceData>,pkce::PkceFlowStore— abstractions over where PAR entries, c_nonces, issuance sessions and upstream-PKCE bridging entries live. Default in-memory impls (MemoryParStore,MemoryNonceStore,MemorySessionStore,MemoryPkceFlowStore) ship alongside.
wallet_core/lib/openid4vc_server— generic axum wiring for an OpenID4VCI issuer, knows nothing about DigiD or BRP:issuer::create_issuance_routermounts the handlers on/.well-known/openid-credential-issuer,/.well-known/oauth-authorization-server,/issuance/par,/issuance/authorize,/issuance/token,/issuance/nonce,/issuance/credential, and/issuance/credential_preview(an extension we support on top of the spec).issuer::ApplicationStatebundles theIssuer, theParStore, thePkceFlowStore, the optional upstream adapter, and the accepted walletclient_ids. Both/authorize(write) and/token(consume) consult thePkceFlowStoreto bridge the decoupled wallet/upstream PKCE pairs.issuer::UpstreamAuthorizationAdapter— the extension point the/authorizehandler uses to resolve the upstream authorization endpoint and rewrite the wallet’sAuthorizationRequestinto one the upstream provider accepts. Letting the implementer own the full request mutation keeps non-standard quirks (e.g. nl-rdo-max requires anonce) out of the generic handler.
wallet_core/wallet_server/pid_issuer— the PID-specific concretions:pid::attributes::BrpPidAttributeService— implementsAttributeServiceby obtaining the BSN via DigiD and then looking up attributes in the BRP.pid::digid::DigidMetadataCache— fetches and holds the upstream OIDC discovery document; shared between the adapter andOpenIdClient.pid::digid::DigidAuthorizationAdapter— implementsUpstreamAuthorizationAdapter; consults theDigidMetadataCacheto resolve the upstreamauthorization_endpoint, rewrites the wallet’sclient_idto the DigiDclient_id, setsscope=openid, and adds a fresh randomnonce(required by nl-rdo-max).pid::digid::OpenIdClient— drives the upstream token + userinfo exchange viapid::userinfo::request_userinfo: POST to RDO Max’s/token, GET/userinfoas a signed-and-encrypted JWT, fetch the JWKS in parallel (via a per-callpid::jwks::HttpJwksClientwrapper), JWE-decrypt the payload and verify the JWS signature against the JWKS to extract the BSN.pid::brp::client::HttpBrpClient(implementsBrpClient) — queries the BRP for personal data by BSN.server::servewires all of the above intocreate_issuance_routerand serves it.
Component diagram
flowchart LR
subgraph ovcs["openid4vc_server (HTTP wiring)"]
direction TB
Router["create_issuance_router<br/>axum handlers"]
AppState["ApplicationState"]
UpAd_trait["trait<br/>UpstreamAuthorizationAdapter"]
end
subgraph ovc["openid4vc (protocol types)"]
direction TB
Issuer["struct Issuer"]
AS_trait["trait AttributeService"]
Store_traits["trait ParStore<br/>trait PkceFlowStore<br/>trait SessionStore of IssuanceData<br/>trait NonceStore"]
MemStores["MemoryParStore<br/>MemoryPkceFlowStore"]
end
subgraph pidi["pid_issuer (PID-specific)"]
direction TB
BrpAttr["BrpPidAttributeService"]
DigidAdapter["DigidAuthorizationAdapter"]
OpenIdClient["OpenIdClient"]
DigidCache["DigidMetadataCache<br/>(holds OIDC metadata)"]
BrpClient["HttpBrpClient"]
end
subgraph ext["external"]
direction TB
RDO[("RDO Max / DigiD")]
BRP[("BRP")]
end
Router --> AppState
AppState -->|holds| Issuer
AppState -->|holds| MemStores
AppState -->|holds| UpAd_trait
Issuer -->|calls| AS_trait
Issuer -->|reads/writes| Store_traits
MemStores -.implements.-> Store_traits
BrpAttr -.implements.-> AS_trait
DigidAdapter -.implements.-> UpAd_trait
BrpAttr -->|owns| OpenIdClient
BrpAttr -->|owns| BrpClient
OpenIdClient -. shares Arc .-> DigidCache
DigidAdapter -. shares Arc .-> DigidCache
DigidCache -->|GET /.well-known/<br/>openid-configuration| RDO
OpenIdClient -->|POST /token,<br/>GET /userinfo,<br/>GET jwks_uri| RDO
BrpClient -->|get_person_by_bsn| BRP
Two extension points are worth noting:
AttributeService— swappingBrpPidAttributeServicefor a different implementation is how the genericissuance_server(disclosure-based issuance) reuses the sameopenid4vc_serverhandlers. The/tokenhandler doesn’t know what a BSN is.UpstreamAuthorizationAdapter— plugs in the upstream OIDC provider. Today the only implementor isDigidAuthorizationAdapter; a different IdP would get a sibling implementation here.
State lives behind the four *Store traits. The in-memory variants shown here
can all be replaced by stateful variants.