Home Supported Standards Terms & Conditions Privacy Policy Documentation Contact About
  1. 0. Choose a use caseHow the issuance starts
  2. 1. Discover the issuerIssuer + authorization server metadata
  3. 2. Authorize & approveAuthenticate, review tokens, request it
  4. 3. The credentialThe issued SD-JWT VC and its disclosures
  5. 4. Refresh itRefresh Token, then the issuer again

← Back to step 1 (discovery & configuration)

VC Issuance — Step 2 of 4: Approve and Request the Credential

The user has authenticated through the OIDC Authorization Code flow and oauth2_oidc_2.html has exchanged the code for tokens. What follows is the OID4VCI credential request: the wallet proves possession of a key it just generated, the user approves the issuance, and the access token is presented to the issuer's Credential Endpoint.

Tip: click a pane's title to collapse/expand it.

Tokens from the OIDC Authorization Code Flow

The token set oauth2_oidc_2.html obtained from the authorization server. The access token is what authorizes the credential request below; the ID token says who was authenticated.

 
Presented to the Credential Endpoint as a Bearer credential. The issuer decides what to put in the credential based on who this token belongs to.

Inspect

The OIDC authentication result. Not sent to the credential endpoint — shown so you can see who the credential will be about.

Inspect

Not used by the issuance itself — but it is what lets step 4 refresh the credential later without troubling the End-User (OID4VCI section 14.5). An authorization server is not obliged to issue one, and the pre-authorized code grant normally does not.
The decoded ID token payload — the authenticated subject the issuer will describe.
 
DPoP — Sender-Constrained Access Token
What DPoP is, and why OID4VCI recommends it

A Bearer access token (RFC 6750) is a password: anything that can read the bytes can spend them. DPoP binds the token to a key — the token carries cnf.jkt, the RFC 7638 thumbprint of a public key — and every request presenting it must also carry a fresh signature from the matching private key over that request's method and URI. A stolen token is then useless without the key.

OID4VCI says the use of DPoP is RECOMMENDED for sender-constrained access tokens, because mTLS (RFC 8705) — the other way to do this — is impractical for a native-app wallet. It applies to the Token Endpoint and to every protected endpoint the issuer publishes: Credential, Deferred Credential and Notification.

DPoP is indifferent to the credential format: it binds an OAuth access token, not a credential, so it works the same for dc+sd-jwt, jwt_vc_json and ldp_vc.

Whether to send a DPoP proof on the Token Request. With it, the authorization server binds the access token (and the refresh token) to this wallet's key and answers token_type: DPoP; without it the token comes back as a Bearer token, which is what the rest of this workflow demonstrates by default.
 
Which key the CREDENTIAL is bound to, which is a different question from whether the TOKEN is bound. Either way the Credential Request carries an openid4vci-proof+jwt key proof — this only decides whose key signs it. Holder of Key reuses the DPoP key, so one key is both the token's cnf.jkt and the credential's cnf.jwk. Unchecked, the credential is bound to a holder key of its own, with a lifetime independent of the token's.
 
The JWS algorithm the DPoP proofs are signed with. RFC 9449 requires an asymmetric algorithm and forbids none and any MAC. Which ones the authorization server accepts is in its dpop_signing_alg_values_supported metadata, shown below.
 
dpop_signing_alg_values_supported from the authorization server's RFC 8414 metadata. Its presence is the only signal that DPoP is on offer at all, so an absent value means the server has not said it supports DPoP.
The public half, which travels in the jwk header of every proof. It is not secret: the point is not to hide the key but to make the token useless without the private half.
 
The JWK Thumbprint of the public key: the SHA-256 of its required members in lexicographic order with no whitespace. This is the value the access token carries as cnf.jkt.
RFC 9449 section 10: sending dpop_jkt on the AUTHORIZATION request binds the code itself to this key, so a stolen code cannot be redeemed even by someone who also has the PKCE code_verifier. It has to be set before step 1 runs, which is why this only says whether it happened.
What the token endpoint actually answered: token_type and the cnf.jkt inside the access token. Asking for a binding does not create one — a server that ignores the proof would answer Bearer, and this row is how you would notice.
RFC 9449 sections 8 and 9: a server may require a nonce of its own choosing in every proof, asking for it with use_dpop_nonce and supplying it in a DPoP-Nonce header. The wallet retries once and remembers the nonce, so later calls carry it without being asked again.
The most recent DPoP proof this page sent, decoded. htm and htu tie it to one method and one endpoint; ath is the hash of the access token it accompanied; jti makes it single use.
 
OID4VCI Credential Request

Everything the request is built from. The holder key pair is generated in this browser; only its public half leaves, inside the key proof, and the issuer copies it into the credential's cnf claim so a verifier can later demand proof that the presenter holds the private half.

credential_endpoint from the credential issuer metadata retrieved in step 1.
credential_issuer — also the audience of the key proof.
credential_configuration_id, its format and its vct — the credential the issuer is being asked to mint.
The public half of the key pair generated in this browser, as a JWK. It goes in the proof JWT header and ends up in the credential's cnf claim.

 
Whether the PRIVATE half of the holder key pair may be written to this browser's localStorage. It has to be kept for the workflow to continue past this page: step 4 needs it to refresh the credential, and the presentation pages need it to sign the Key Binding JWT. Turn it off to keep it out of storage, and carry it yourself with Download Key Pair.
 
A fresh nonce from the issuer's Nonce Endpoint, carried by the proof so the issuer knows the proof was made for this request.
The CREDENTIAL's key proof, OID4VCI section 8.2: a JWT with typ openid4vci-proof+jwt, signed by the holder key, naming the credential issuer as its audience and carrying the c_nonce. It is what binds the credential to that key, so every Credential Request carries one whether or not DPoP is in use. It is NOT the DPoP proof — that is a typ dpop+jwt sent as a header, and the pane above shows it.
The header of the proof JWT carried in the request body's proofs.jwt, decoded. typ marks it as an OID4VCI key proof, alg is what it was signed with, and jwk is the holder public key the credential will be bound to.
The payload of the proof JWT carried in the request body's proofs.jwt, decoded: the wallet as iss, the credential issuer as aud, when it was signed (iat), and the c_nonce that ties it to this request.
The JSON that will be POSTed to the credential endpoint with the access token as a Bearer credential.
 
Credential Request Options

Three things the wallet gets to decide, each of which changes the request below.

OID4VCI section 8.2. If the token response granted credential_identifiers (because the authorization used authorization_details), the request MUST use credential_identifier and MUST NOT use credential_configuration_id. Otherwise it is the other way round. This is not a preference — it follows from what was granted.
One proof per key, one credential per proof (section 8.3). The issuer's batch_credential_issuance.batch_size is the most it will accept; asking for more than one is how a wallet gets several copies of the same credential bound to different keys.
 
Section 10. The wallet sends a public key and the issuer returns a JWE instead of JSON, which this page then decrypts. Only offered when the issuer advertises credential_response_encryption.
 
Section 10, the other direction. The ISSUER publishes keys in credential_request_encryption.jwks and the wallet encrypts the Credential Request to one of them, sending it as application/jwt instead of JSON. Unlike the response side there is no alg_values_supported: the JWE alg is the alg of the key chosen from the JWKS. Only offered when the issuer advertises credential_request_encryption.
 
Approve Issuance

Issue this credential?

Approving will send the Credential Request shown below to the issuer, exactly as it appears. The issuer will mint an SD-JWT VC describing the authenticated user and bound to the holder key generated on this page.

  • Issuer:
  • Credential:
  • Subject:
  • Claims this credential will carry, as chosen in step 1's Claims to Request pane:
The fully assembled HTTP request: the method and URL of the Credential Endpoint, the headers (including the access token presented as a Bearer credential), and the JSON body carrying the key proof. This is exactly what Approve sends.

 
VC Tools

Tools that are useful at any point in these workflows, and belong to none of their steps.

Resolve a DID to its document, read the keys it publishes, check that one of them really signed the credential this browser is holding, and check that the DID and an origin are the same entity.
Resolve a Decentralized Identifier (did:jwk, did:key, did:web) to its document, verify that a key it publishes signed the credential in this browser, and verify a domain linkage (DIF Well Known DID Configuration). Opens in this tab; the workflow's state is untouched.
Build a Root, Intermediate and Issuing CA, issue certificates with any X.509v3 extension, and test a real TLS or mutual-TLS handshake with what you issued.
Build a certificate authority — Root, Intermediate and Issuing — and issue TLS server, TLS client, code-signing and S/MIME certificates with full X.509v3 extension control. Useful when the issuer or verifier is behind TLS you control. Opens in this tab; the workflow's state is untouched.