OpenID for Verifiable Presentations (OpenID4VP)
OpenID for Verifiable Presentations (OpenID4VP) lets a relying party request cryptographic proof of identity from a credential held in a user's digital wallet. Instead of a username and password, the wallet presents a Selective Disclosure JWT VC (SD-JWT VC). The credential is signed by a trusted authority: a government identity provider or a ThunderID credential issuer.
ThunderID acts as the verifier: it generates a signed authorization request, the wallet responds with an encrypted presentation, and ThunderID verifies the SD-JWT VC against a configured trust store. On success, it issues a signed result token to the relying party.
Presentation definitions are managed resources you create through the ThunderID Console. The verifier engine picks them up immediately when a relying party initiates a verification transaction.
How It Works
The protocol uses a polling model: a back-channel between ThunderID and the relying party, and an out-of-band channel to the wallet via QR code or deep link.
- An operator creates a presentation definition in the ThunderID Console, specifying which credential type (
vct) to request and which claims are mandatory or optional. - The relying party calls
POST /openid4vp/initiatewith the definitionhandleand receives atxn_id,wallet_url, andstatus_url. - The relying party displays
wallet_urlas a QR code or deep link. - The user scans. The wallet fetches the signed authorization request from
GET /openid4vp/request. - The wallet selects matching credentials, applies selective disclosure, and posts the encrypted response to
POST /openid4vp/response. - ThunderID decrypts the response, verifies the SD-JWT VC, and enforces the presentation policy.
- The relying party polls
GET /openid4vp/status/{txn_id}untilCOMPLETEDwith a signedresult_token. - The relying party validates the
result_tokento read the verified subject and claims.
How ThunderID Implements It
| Aspect | Behavior |
|---|---|
| Client identification | The verifier's client_id is derived from client_id_scheme and the signing certificate at startup, not configured directly. Supported schemes: x509_hash (SHA-256 thumbprint), x509_san_dns (first DNS SAN), redirect_uri (response URI). The full certificate chain is included in the request object's x5c header. |
| Request object (JAR) | Signed as a compact JWS and served at GET /openid4vp/request?state=... with Content-Type: application/oauth-authz-req+jwt. |
| Response mode | direct_post.jwt. The wallet encrypts its VP token as a compact JWE using ECDH-ES key agreement with A128GCM content encryption (configurable via response_enc_values). The ephemeral public key is advertised in client_metadata.jwks. |
| Query language | DCQL (Digital Credentials Query Language). The query targets a specific vct and claims list. |
| Credential format | dc+sd-jwt. |
| Selective disclosure | Disclosures beyond the combined requested claims list fail verification. Optional claims may be omitted by the wallet. |
| Key binding | When enforce_key_binding is enabled, the wallet must include a kb+jwt binding the presentation to the nonce and verifier audience. |
| Issuer trust | Each definition may override the engine-level default via enforceTrustedIssuer. When enabled, the SD-JWT issuer is verified against a pinned certificate. A definition's trustedAuthorities restricts which named anchors are acceptable. Active trust anchors are listed at GET /openid4vp/trust-anchors (returns name, subject, ski, not_after per anchor). |
| Claim value constraints | claimValues maps a dotted claim path to an allowed set of values. The constraint is advertised in the DCQL query and enforced at verification, but only when the claim is disclosed; undisclosed claims pass silently. |
| Result token | Signed JWT issued on COMPLETED. Claims: iss, sub, aud (issuer URL), jti, txn, definition_id, subject, verified_claims, verifier. |
| State TTL | Transactions expire after state_ttl_seconds. The status endpoint returns EXPIRED for past-TTL transactions. |
| Wallet errors | A wallet may POST an OAuth error instead of a VP token (OpenID4VP §6.4). ThunderID records the error and reflects it as FAILED in the status response. |
Presentation Definitions
A presentation definition specifies the credential type to request and which claims the wallet must or may disclose. Its handle is the definition_id used when initiating a verification.
Manage definitions in Verifiable Credentials → Presentations in the ThunderID Console, or declare them in YAML for file-based deployments (resource_type: presentation_definition).
| Field | Description |
|---|---|
| Handle | Unique identifier for this definition. Used as the definition_id in the initiate request and in flow step configuration. Required. |
| Credential Type (VCT) | The vct URI the wallet must present to satisfy this request. Required. |
| Claims | Each entry has a dotted claim path, a requirement (Mandatory or Optional), and an optional allowed-values list. Mandatory claims must be disclosed; optional claims may be withheld. When allowed values are set, the disclosed value must match one, enforced at verification. Undisclosed optional claims pass silently regardless of allowed values. |
| Enforce Trusted Issuer | When enabled, the SD-JWT VC issuer certificate chain is validated against the engine-level trust anchors. |
| Trusted Issuers | Restricts which named trust anchors are acceptable for this definition. Leave empty to accept any trust anchor configured at the engine level. |
Server Configurations
Configure the verifier engine under the openid4vp: key in deployment.yaml. All values can be overridden there; the defaults below come from the built-in server defaults.
| Key | Default | Description |
|---|---|---|
client_id_scheme | x509_san_dns | How the verifier client_id is derived from the signing certificate: x509_san_dns (first DNS SAN), x509_hash (SHA-256 thumbprint), or redirect_uri (response URI). |
signing_key_id | ecdsa-key | Key used to sign request objects. Must be certificate-backed. |
ephemeral_key_id | vp-enc | Key used for ECDH-ES decryption of wallet responses. |
registration_cert_file | - | Path to a PEM file containing the signing certificate chain advertised in client_metadata. |
trusted_anchors | [] | List of {name, cert_file} entries. Each entry pins a root CA whose chains are accepted as SD-JWT VC issuer certificates. Empty list disables certificate-chain validation. |
enforce_key_binding | true | Require wallets to include a kb+jwt binding the presentation to the nonce and verifier. |
response_enc_values | ["A128GCM","A256GCM","A128CBC-HS256","A256CBC-HS512"] | Supported content-encryption algorithms for the wallet's JWE response. |
request_audience | - | aud claim placed in the signed request object sent to the wallet. Omit for vp_token flows (some wallets treat a non-empty aud as a SIOP request). |
request_validity_seconds | 300 | How long the signed request object is valid. |
state_ttl_seconds | 300 | Verification transaction lifetime (seconds). |
leeway_seconds | 30 | Clock-skew tolerance when validating credential timestamps. |
key_binding_max_age_seconds | 300 | Maximum age of the key-binding JWT iat. |
result_token_validity_seconds | 300 | Lifetime of the signed result token. |
trusted_anchors is a list of objects:
openid4vp:
trusted_anchors:
- name: my-gov-ca
cert_file: /path/to/ca.pem
Result Token Claims
When the status endpoint returns COMPLETED, the response includes a signed JWT result_token. Validate it against the ThunderID JWKS before trusting its claims.
| Claim | Description |
|---|---|
iss | The ThunderID instance base URL. |
sub | The holder's stable identifier, from the credential sub claim, or a key-binding thumbprint URN when sub is absent. |
aud | The ThunderID issuer URL. |
jti | Unique token identifier (same value as txn). |
txn | The transaction identifier (txn_id). |
definition_id | The presentation definition handle that was requested. |
subject | Same as sub. |
verified_claims | Map of selectively disclosed claim values from the credential. |
verifier | The verifier's client_id. |
Try It in ThunderID
Initiate a Presentation Transaction
POST /openid4vp/initiate
Content-Type: application/json
{ "definition_id": "example-pid" }
Response:
{
"txn_id": "a1b2c3d4e5f6",
"wallet_url": "openid4vp://?client_id=x509_hash%3A...&request_uri=https%3A%2F%2Fauth.example.com%2Fopenid4vp%2Frequest%3Fstate%3Da1b2c3d4e5f6&request_uri_method=get",
"status_url": "/openid4vp/status/a1b2c3d4e5f6",
"expires_at": "2026-01-01T12:05:00Z"
}
Display wallet_url as a QR code or deep link. Then poll until the wallet responds:
GET /openid4vp/status/a1b2c3d4e5f6
{
"status": "COMPLETED",
"result_token": "<signed-jwt>"
}
Verify the result_token signature against the ThunderID JWKS endpoint, then read verified_claims.
Use in a Sign-In Flow
To integrate OpenID4VP into an authentication flow, configure the OpenID4VP Verify executor. It manages the initiate-and-poll lifecycle automatically, presenting the wallet QR to the user and resolving their identity on completion.
See Advanced Configurations: OpenID4VP Verify for the executor reference.
Related Guides
- OpenID for Verifiable Credential Issuance, issue SD-JWT VCs that users can later present
- JWKS, verify the
result_tokensignature against the ThunderID public key endpoint - Flows, build authentication flows that include credential presentation steps
- Advanced Configurations, flow executor reference including the OpenID4VP Verify executor