Issuance with OpenID4VCI
We’ve implemented issuance with OpenID4VCI draft 13, with attestation preview as a custom addition.
We currently (2025-12-11) have two issuance implementations: The pid_issuer,
a specialized issuer specifically for Dutch PIDs, which this page is about,
and issuance_server a disclosure-based-issuance service that can issue all
kinds of things (which you can read about here).
PID issuance
PID issuance is done by the pid_issuer which is a part of the
wallet_server crate. It is created to issue PIDs specifically.
This diagram below shows how the pid_issuer uses OpenID4VCI in a
Pre-Authorized Code Flow to issue a [PID] to the wallet.
Using this protocol, the wallet starts a normal OpenID Connect session at an
AuthServer (which is a so-called OpenID Provider; in the case of the NL
Wallet, this usually means DigiD through nl-rdo-max), from which it
obtains an authorization code.
Next, the wallet uses this code to start the OpenID4VCI issuance protocol in the
pre-authorized code flow with a wallet_server component called pid_issuer.
The pid_issuer finishes the OpenID Connect session with the AuthServer to
discover the identity of the wallet user, allowing it to finish issuance.
In the diagram below we introduce an actor called the PidAttributeService,
whose responsibility it is to produce the attributes to be issued when given a
valid pre-authorized code.
In the case of PID issuance, the pid_issuer can do this by finishing the
OpenID Connect session that the wallet started. (The PidAttributeService is
a part of the pid_issuer in the wallet_server crate, as opposed to a
separate HTTP server; we include it here as separate actor to clearly visualize
separate responsibilities.)
The protocol works as follows:
The wallet starts an OpenID Connect session at the
AuthServerby sending it an Authorization Request, receiving an authorization code from theAuthServerin response;Using the received authorization code, the wallet starts OpenID4VCI issuance in a so-called pre-authorized code flow by sending a
POSTrequest with the previously obtained code as a pre-authorized code in a Token Request to thepid_issuer;The
pid_issuerfeeds the Token Request with the pre-authorized code to itsPidAttributeServicecomponent. ThePidAttributeServicePOST’s the Token Request to theAuthServer, transforming only the pre-authorized code in it to a normal authorization code but keeping the other parameters (such as thestateand the PKCEcode_verifier) in the Token Request as-is, thereby continuing the OpenID Connect session that the wallet previously started, and obtaining anaccess_token;Using the resulting
access_token, thePidAttributeServiceinvokes the/userinfoendpoint of theAuthServerto retrieve the BSN, with which it does a query to the BRP, resulting in the attributes to be issued;The
pid_issuerthen generates thec_nonceand anaccess_tokenof its own, and a preview of the attestations as a custom addition to the OpenID4VCI protocol, all of which it returns to the wallet;With the
access_tokenand a valid set of proofs of possession (signatures over thec_noncevalidating against the public keys that the wallet wants to have in its PID), the wallet can access thebatch_credentialendpoint of thepid_issuerto obtain the attestations.
Notice that from the perspective of the AuthServer, the mobile operating
system (abreviated as “OS” in the diagram) acts as the User Agent in an OpenID
Connect session, and the pid_issuer’s PidAttributeService acts as the OpenID
Client. The former starts the session by navigating to the AuthServer with an
Authorization Request, and the latter resumes the session with Token and User
Info Requests.
The NL Wallet requires at least the SD JWT format for PID attestations. The MSO
mDoc can be issued as well. The attestation type and paths to the login claim
and the recovery_code are dynamically configured. See the pid_attributes
field in the wallet-config.json for details.
sequenceDiagram
autonumber
actor User
participant OS
participant Wallet
participant WP as Wallet Backend
participant WalletServer as PidIssuer
participant PidAttributeService
participant AuthServer
User->>+Wallet: click "issue PID"
Wallet->>-OS: navigate to AuthServer/authorize?redirect_uri=...
OS->>+AuthServer: GET /authorize?redirect_uri=...
note over User, AuthServer: authenticate user with DigiD app
AuthServer->>AuthServer: generate & store code
AuthServer->>-OS: GET universal_link?code=...
OS->>Wallet: openWallet(code)
activate Wallet
Wallet->>+WalletServer: POST /token(pre-authorized_code)
WalletServer->>+PidAttributeService: getAttributes(pre-authorized_code)
PidAttributeService->>+AuthServer: POST /token(code)
AuthServer->>AuthServer: lookup(code)
AuthServer->>-PidAttributeService: access_token
PidAttributeService->>+AuthServer: GET /userinfo(access_token)
AuthServer->>-PidAttributeService: claims(BSN)
PidAttributeService->>PidAttributeService: obtain attributes from BRP
PidAttributeService->>-WalletServer: attributes
WalletServer->>WalletServer: generate c_nonce, access_token
WalletServer->>-Wallet: access_token, c_nonce, attestation_previews
Wallet->>+User: Show attributes, ask consent
deactivate Wallet
User->>-Wallet: approve with PIN
activate Wallet
Wallet ->> WP: request PoPs with nonce<br/>(PerformIssuanceWithWua instruction)
WP ->> Wallet: Return WUA and Signed PoP and PoA
Wallet->>+WalletServer: POST /batch_credential(access_token, PoPs)
note over Wallet: WUA and PoA are included here
WalletServer->>WalletServer: verify proofs, WUA and PoA
WalletServer->>-Wallet: attestations
deactivate Wallet