Skip to main content

OpenID for Verifiable Credential Issuance (OpenID4VCI)

OpenID for Verifiable Credential Issuance (OpenID4VCI) is the protocol wallets use to request a credential from an issuer. ThunderID acts as the credential issuer: it authenticates the user through the OAuth 2.1 authorization code flow and issues a signed SD-JWT VC bound to the wallet's key. The credential can later be presented at relying parties without disclosing unnecessary attributes.

Credential configurations are managed resources you create through the ThunderID Console. The issuer engine reflects them immediately in the public metadata document.

How It Works

ThunderID uses the authorization code grant as the issuance flow. The wallet acts as an OAuth client; the user signs in through the normal ThunderID login pages.

  1. An operator creates a credential configuration in the ThunderID Console, specifying the credential type (vct) and which user profile claims to include as selectively disclosable attributes.
  2. ThunderID advertises the credential configuration in its issuer metadata at GET /.well-known/openid-credential-issuer.
  3. A relying party (or ThunderID itself) generates a credential offer: a compact openid-credential-offer:// deep link the user opens in their wallet.
  4. The wallet resolves the offer and redirects the user to the ThunderID authorization endpoint with the credential configuration handle as the OAuth scope.
  5. The user authenticates. ThunderID issues an access token scoped to the credential configuration.
  6. The wallet requests a fresh c_nonce from POST /openid4vci/nonce to bind the credential to its key.
  7. The wallet posts a credential request to POST /openid4vci/credential with the access token and a holder proof JWT signed with its private key.
  8. ThunderID verifies the access token and holder proof, resolves the user's profile claims, signs an SD-JWT VC bound to the wallet's public key, and returns it.
How ThunderID Implements It
AspectBehavior
Credential formatdc+sd-jwt. Every configured claim is individually selectively disclosable.
Holder bindingThe wallet's public key is embedded in the SD-JWT VC cnf claim as a JWK. One credential is issued per holder proof JWT.
Issuer-initiated offerGET /openid4vci/offer?credential_configuration_id=<handle> returns the JSON offer and an openid-credential-offer:// deep link. The stored offer resolves at GET /openid4vci/credential-offer/{id} and expires after 5 minutes.
Proof typeHolder proofs must use proof_type: "jwt" with typ: openid4vci-proof+jwt. The aud must equal the credential issuer URL; the nonce must match a valid, unexpired c_nonce.
Batch issuanceMultiple proofs may be submitted via proofs.jwt. One SD-JWT VC is issued per proof, up to the configured batch_size. When batch_size > 1, batch_credential_issuance is advertised in the metadata.
DPoPWhen the access token carries cnf.jkt, the credential request must include a matching DPoP proof header (RFC 9449).
Claim sourcingClaims are resolved from the user's profile attributes. Missing attributes are silently omitted from the issued credential. Only claims with a displayName are advertised in the metadata claims object; all configured claims are issued.
SigningSigned with the key set as signing_key_id. The certificate chain is included in the SD-JWT VC x5c header. Omitting signing_key_id disables the issuer engine at startup.
Scope enforcementWhen enforce_scope is enabled, the access token must carry a scope matching the credential_configuration_id.

Credential Configurations

A credential configuration defines one credential type the issuer can produce. Its handle becomes the credential_configuration_id in the issuer metadata document and the OAuth scope wallets request.

Manage configurations in Verifiable Credentials → Templates in the ThunderID Console, or declare them in YAML for file-based deployments (resource_type: credential_configuration).

FieldDescription
HandleUnique identifier for this credential type. Becomes the OAuth scope and credential_configuration_id in issuer metadata. Required.
Credential Type (VCT)The vct URI that identifies this credential type to wallets and verifiers. Required.
ClaimsEach entry maps a user profile attribute name to an optional wallet display name. The attribute value is sourced from the user's profile at issuance. Claims with a display name are advertised in the metadata claims object; all configured claims are included in the issued credential regardless.
DisplayWallet presentation display settings: locale (BCP 47 tag, e.g. en-US) and logoUri (a hosted image URL shown as the credential logo in the wallet).
ValidityLifetime of issued credentials in seconds. Overrides the server-level credential_validity_seconds when set.

Server Configurations

Configure the issuer engine under the openid4vci: key in deployment.yaml. All values can be overridden there; the defaults below come from the built-in server defaults.

KeyDefaultDescription
signing_key_idecdsa-keyKey used to sign issued SD-JWT VCs. Must be certificate-backed (x5c required). Omitting this key disables the issuer engine entirely.
credential_issuerserver URLThe credential_issuer identifier in the metadata document.
base_urlserver URLBase URL for endpoint URLs advertised in metadata.
authorization_servers[server URL]Authorization server list advertised in metadata.
nonce_ttl_seconds300How long a c_nonce is valid (seconds).
proof_max_age_seconds300Maximum age of the holder proof JWT iat claim.
credential_validity_seconds2592000Lifetime of issued SD-JWT VCs (seconds; default is 30 days).
batch_size5Maximum number of proofs per credential request. Values above 1 enable batch issuance.
enforce_scopefalseWhen true, the access token must carry a scope matching the credential_configuration_id.

Issuer Metadata

GET /.well-known/openid-credential-issuer returns the credential issuer metadata document built dynamically from active credential configurations.

{
"credential_issuer": "https://auth.example.com",
"credential_endpoint": "https://auth.example.com/openid4vci/credential",
"nonce_endpoint": "https://auth.example.com/openid4vci/nonce",
"authorization_servers": ["https://auth.example.com"],
"credential_configurations_supported": {
"example-pid": {
"format": "dc+sd-jwt",
"scope": "example-pid",
"vct": "urn:example:pid:1",
"cryptographic_binding_methods_supported": ["jwk"],
"credential_signing_alg_values_supported": ["ES256"],
"proof_types_supported": {
"jwt": { "proof_signing_alg_values_supported": ["ES256"] }
},
"display": [{ "name": "Example PID" }],
"claims": {
"given_name": { "display": [{ "name": "First Name" }] }
}
}
}
}

Try It in ThunderID

Generate a Credential Offer

GET /openid4vci/offer?credential_configuration_id=example-pid

Response includes the offer JSON and a deep link:

{
"credential_offer": {
"credential_issuer": "https://auth.example.com",
"credential_configuration_ids": ["example-pid"],
"grants": { "authorization_code": {} }
},
"credential_offer_uri": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fauth.example.com%2Fopenid4vci%2Fcredential-offer%2Fabc123"
}

Request a c_nonce and Issue a Credential

POST /openid4vci/nonce
{ "c_nonce": "wKI4LTs208I3EgjkuYpvRQ" }

Then issue the credential with the access token (from the authorization code flow) and a holder proof JWT:

POST /openid4vci/credential
Authorization: Bearer <access-token>
Content-Type: application/json

{
"credential_configuration_id": "example-pid",
"proof": {
"proof_type": "jwt",
"jwt": "<holder-proof-jwt>"
}
}

Response:

{
"credentials": [{ "credential": "<sd-jwt-vc>" }]
}

For batch issuance, pass multiple proof JWTs in proofs.jwt: one SD-JWT VC is returned per proof.

Explore with AI

ThunderID LogoThunderID Logo

Product

DocsAPIsSDKs
© Copyright Linux Foundation Europe.For web site terms of use, trademark policy and other project policies please see https://linuxfoundation.eu/en/policies.