OpenID4VCI credential-uitgifte uitgelegd: van aanbod tot geaccepteerde credential
OpenID4VCI, OpenID for Verifiable Credential Issuance, bepaalt hoe een wallet een credential aanvraagt en ontvangt van een uitgever. Eén uitgifte doorloopt meerdere afzonderlijke stappen voordat de wallet daadwerkelijk een bruikbare credential heeft, en elke stap heeft zijn eigen faalmodus. Deze pagina behandelt die volledige levenscyclus, met originele uitgewerkte voorbeelden.
Twee manieren om te starten: pre-authorized code en authorization code
Uitgifte start vaak vanuit een Credential Offer die door de uitgever wordt verstuurd, en de daarin genoemde grant bepaalt hoe de wallet wordt geautoriseerd. Een wallet kan de uitgifte ook zelf starten, zonder aanbod, via de authorization code flow. Beide flows eindigen op dezelfde plek: de wallet heeft een accesstoken waarmee de credential kan worden aangevraagd.
Pre-authorized code flow
1. Uitgever
Kent de houder al en geeft een Credential Offer af met een pre-authorized_code
2. Wallet
Wisselt de code in bij het token endpoint, eventueel met een transactiecode
3. Wallet
Vraagt de credential aan met een bewijs van bezit van de sleutel
Authorization code flow
1. Wallet
Scant een Credential Offer met een authorization_code grant, of start de flow zelf zonder aanbod
2. Autorisatieserver
Laat de houder inloggen en toestemming geven, en geeft dan een code af
3. Wallet
Wisselt de code in voor een token en vraagt daarna de credential aan
De volledige levenscyclus van een credential-aanbod
De specificatie definieert geen benoemde states, maar een door de uitgever geïnitieerde flow waarbij de credential direct wordt uitgegeven, is het makkelijkst te volgen zoals hieronder weergegeven. Elke stap kan op zijn eigen manier mislukken, en een wallet-implementatie moet die paden afhandelen, niet alleen het gunstige scenario.
Uitgewerkt voorbeeld: een keuringsbewijs voor een transportvloot
Een keuringsinstantie geeft na een routinekeuring een keuringsbewijs uit aan de zakelijke wallet van een transportbedrijf. De keurmeester heeft de wagenparkbeheerder al op de keuringslocatie geauthenticeerd, dus gebruikt de uitgever de pre-authorized code flow. Onderstaande stappen volgen die ene uitgifte van aanbod tot geaccepteerde credential.
Het voorbeeld toont de basis OpenID4VCI. Deployments met hoge betrouwbaarheidseisen, zoals de EUDI Wallet, volgen daarbovenop het HAIP-profiel, dat DPoP-gebonden accesstokens, wallet attestation bij het token endpoint en key attestation voor de credential-sleutels toevoegt. Die zijn hier weggelaten om elke stap leesbaar te houden.
1. Credential offer
De terminal van de keuringsinstantie toont een QR-code. Die bevat een URI die begint met openid-credential-offer:// en het onderstaande aanbod draagt, URL-gecodeerd in een credential_offer parameter, of een credential_offer_uri waarvan de wallet het aanbod ophaalt. De wallet scant deze en leest welke credential wordt aangeboden en hoe die te verkrijgen is.
Uitgever
Toont een QR-code met een Credential Offer
Noemt de credential-configuratie en een pre-authorized_code grant
{
"credential_issuer": "https://issuer.fleetinspect.example",
"credential_configuration_ids": ["roadworthiness_certificate"],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "fi-8f2c0b3a",
"tx_code": {
"length": 6,
"input_mode": "numeric",
"description": "Enter the code sent to your phone by text message"
}
}
}
}2. Ontdekking van uitgeversmetadata
Voordat er iets wordt aangevraagd, haalt de wallet de metadata van de uitgever op om te achterhalen wat roadworthiness_certificate bevat en welke endpoints moeten worden aangeroepen. De metadata vermeldt geen aparte autorisatieservers, dus is de uitgever zijn eigen autorisatieserver, en leest de wallet het token endpoint uit de metadata van die server.
Wallet
GET /.well-known/openid-credential-issuer
Leert het credentialformaat, de claims en de geaccepteerde proof types, plus het nonce-, credential-, deferred- en notification-endpoint
Metadata van de credential-uitgever (uittreksel)
{
"credential_issuer": "https://issuer.fleetinspect.example",
"nonce_endpoint": "https://issuer.fleetinspect.example/nonce",
"credential_endpoint": "https://issuer.fleetinspect.example/credential",
"deferred_credential_endpoint": "https://issuer.fleetinspect.example/deferred",
"notification_endpoint": "https://issuer.fleetinspect.example/notify",
"credential_configurations_supported": {
"roadworthiness_certificate": {
"format": "dc+sd-jwt",
"vct": "https://fleetinspect.example/vct/roadworthiness",
"cryptographic_binding_methods_supported": ["jwk"],
"credential_signing_alg_values_supported": ["ES256"],
"proof_types_supported": {
"jwt": { "proof_signing_alg_values_supported": ["ES256"] }
},
"credential_metadata": {
"claims": [
{ "path": ["vehicle_registration"] },
{ "path": ["inspection_result"] },
{ "path": ["valid_until"] }
]
}
}
}
}Metadata van de autorisatieserver (uittreksel), van /.well-known/oauth-authorization-server
{
"issuer": "https://issuer.fleetinspect.example",
"token_endpoint": "https://issuer.fleetinspect.example/token",
"pre-authorized_grant_anonymous_access_supported": true
}3. Tokenverzoek
De wallet wisselt de pre-authorized code in bij het token endpoint, samen met de transactiecode die de keuringsinstantie naar de telefoon van de wagenparkbeheerder stuurde. Door die code via een tweede kanaal te sturen, kan iemand die de QR-code over de schouder fotografeert deze alsnog niet inwisselen.
Wallet
POST /token
Stuurt de pre-authorized_code en tx_code, ontvangt een accesstoken dat aan dit aanbod gebonden is
POST /token HTTP/1.1 Host: issuer.fleetinspect.example Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code &pre-authorized_code=fi-8f2c0b3a &tx_code=482913
Response
{
"access_token": "fi-at-3d91e0",
"token_type": "Bearer",
"expires_in": 86400
}4. Bewijs van bezit
Omdat de metadata een nonce_endpoint vermeldt, haalt de wallet daar eerst een verse c_nonce op. Vervolgens bewijst de wallet dat het de private key bezit waaraan de credential wordt gebonden, door een proof JWT te ondertekenen over de identifier van de uitgever en die c_nonce.
Wallet
POST /nonce, ondertekent daarna een proof JWT met de sleutel waaraan de credential wordt gebonden
Bindt de credential aan die sleutel, niet enkel aan wie het accesstoken bezit
POST /nonce HTTP/1.1 Host: issuer.fleetinspect.example
Response
{
"c_nonce": "fi-nonce-77aa"
}De wallet bouwt nu de proof JWT op uit twee JSON-objecten, een header en een payload, en ondertekent deze met de private key waaraan de credential wordt gebonden.
Header: wat deze JWT is en welke sleutel heeft ondertekend
{
"typ": "openid4vci-proof+jwt",
"alg": "ES256",
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}typ: markeert dit als een OpenID4VCI key proof, zodat het niet wordt aangezien voor een ander soort JWTalg: het ondertekeningsalgoritme, een van de algoritmen die de uitgever vermeldde in proof_signing_alg_values_supportedjwk: de publieke sleutel waaraan de credential wordt gebonden; de uitgever controleert de handtekening ertegen
Payload: voor wie de proof is en wanneer die is gemaakt
{
"aud": "https://issuer.fleetinspect.example",
"iat": 1789376400,
"nonce": "fi-nonce-77aa"
}aud: de identifier van de uitgever, zodat de proof niet bij een andere uitgever kan worden hergebruiktiat: het tijdstip waarop de proof werd gemaakt, in seconden sinds 1970nonce: de c_nonce van het nonce endpoint, die aantoont dat de proof vers is
Ondertekend resultaat
Header en payload worden elk base64url-gecodeerd en met een punt samengevoegd. De wallet ondertekent die string met de private key en voegt na een tweede punt de base64url-gecodeerde handtekening toe. De resulterende string is de proof JWT die de wallet in stap 5 meestuurt in het credentialverzoek.
base64url(header) . base64url(payload) . base64url(signature) eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIs... .eyJhdWQiOiJodHRwczovL2lzc3Vlci5mbGVldGluc3BlY3QuZXhhbXBsZSIs... .<ES256 signature>
5. Credentialverzoek
De wallet roept het credential endpoint aan met het accesstoken en de proof, en de uitgever maakt de ondertekende credential aan en geeft die terug.
Wallet
POST /credential
Stuurt het accesstoken, de configuratie-id en de proof JWT, ontvangt de ondertekende credential en een notification_id
POST /credential HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0
{
"credential_configuration_id": "roadworthiness_certificate",
"proofs": {
"jwt": ["<proof JWT from step 4>"]
}
}Response
{
"credentials": [
{ "credential": "<issuer-signed SD-JWT VC>" }
],
"notification_id": "fi-notif-9012"
}De hele flow in één oogopslag
Dit sequence-diagram brengt de vijf stappen van het uitgewerkte voorbeeld samen, van het aanbod tot de melding. Doorgetrokken pijlen zijn verzoeken, gestreepte pijlen zijn responses, en de gestippelde pijl is de transactiecode die buiten het protocol om per sms reist.
Wallet
Zakelijke wallet van het transportbedrijf
Autorisatieserver
In dit voorbeeld door de uitgever zelf gedraaid
Credential-uitgever
Keuringsinstantie
- Credential-uitgever naar Wallet: Credential Offer, getoond als QR-code
- Credential-uitgever naar Wallet: tx_code, per sms verstuurd naar de telefoon van de wagenparkbeheerder
- Wallet naar Credential-uitgever: GET /.well-known/openid-credential-issuer
- Credential-uitgever naar Wallet: metadata van de credential-uitgever
- Wallet naar Autorisatieserver: GET /.well-known/oauth-authorization-server
- Autorisatieserver naar Wallet: metadata van de autorisatieserver
- Wallet naar Autorisatieserver: POST /token: pre-authorized_code, tx_code
- Autorisatieserver naar Wallet: access_token
- Wallet naar Credential-uitgever: POST /nonce
- Credential-uitgever naar Wallet: c_nonce
- Wallet: ondertekent proof JWT
- Wallet naar Credential-uitgever: POST /credential: accesstoken, proofs
- Credential-uitgever naar Wallet: credentials, notification_id
- Wallet: valideert en slaat op
- Wallet naar Credential-uitgever: POST /notify: credential_accepted
- Credential-uitgever naar Wallet: 204 No Content
Wanneer de credential nog niet klaar is: uitgestelde uitgifte
Het voorbeeld hierboven gaat ervan uit dat het keuringsresultaat al definitief is. Als de keuringsinstantie in plaats daarvan een grensgeval moet voorleggen aan een senior keurmeester, kan het credential endpoint de credential niet meteen teruggeven en stelt het de uitgifte uit.
Directe uitgifte
Het credential endpoint geeft de ondertekende credential terug in dezelfde response als het verzoek.
Uitgestelde uitgifte
Het credential endpoint antwoordt met HTTP 202, een transaction_id en een interval. De wallet bevraagt het deferred credential endpoint met dat id, met minstens interval seconden tussen de verzoeken, tot de credential klaar is.
Uitgestelde response van /credential
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"transaction_id": "fi-tx-55c2",
"interval": 900
}Bevragen van /deferred tot deze klaar is
POST /deferred HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0
{ "transaction_id": "fi-tx-55c2" }
// While the review is open: 202 with the same transaction_id and interval.
// Once approved: 200 with the credentials, optionally with a notification_id.
// If the review rejects the result: a credential_request_denied error.De cirkel rond: het notification endpoint
Na uitgifte kan de wallet de uitgever laten weten wat er met de credential is gebeurd, met behulp van de notification_id uit de credentialresponse. Wallets zijn niet verplicht deze meldingen te sturen en aflevering is niet gegarandeerd, dus een uitgever kan een ontbrekende melding niet interpreteren.
Wallet
Plaatst een event bij het notification_endpoint van de uitgever voor het ontvangen notification_id, wat elke credential in die response omvat
Opgeslagen in de wallet
Uitgifte mislukt om een andere reden, bijvoorbeeld doordat de credential niet valideerde
Uitgifte mislukt door toedoen van de houder, bijvoorbeeld omdat die weigerde de credential op te slaan
POST /notify HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0
{
"notification_id": "fi-notif-9012",
"event": "credential_accepted"
}Gerelateerde termen
Veelgestelde vragen
Hoe weet een wallet welk grant type te gebruiken?
De Credential Offer noemt de grant in het grants-object. authorization_code is aanwezig wanneer de uitgever wil dat de houder als onderdeel van de flow inlogt. pre-authorized_code is aanwezig wanneer de houder al was geauthenticeerd op het kanaal waar het aanbod werd aangemaakt, bijvoorbeeld door de keurmeester op de keuringslocatie in het onderstaande voorbeeld. Een aanbod kan beide vermelden, waarna de wallet er een kiest. Heeft het aanbod geen grants-object, dan zoekt de wallet in de metadata van de autorisatieserver op welke grant types deze ondersteunt.
Waarom haalt de wallet uitgeversmetadata op voordat er iets wordt aangevraagd?
De Credential Offer noemt alleen credential_configuration_ids, de URL van de uitgever en de grants. De metadata van de uitgever, aangeboden op een well-known pad, beschrijft elke configuratie: het formaat, de claims en de geaccepteerde proof types, plus het nonce-, credential-, deferred- en notification-endpoint. Ook staat erin welke autorisatieserver moet worden gebruikt, en de metadata van die server levert het token endpoint. Zonder beide zou de wallet niet weten hoe geldige verzoeken te bouwen of wat aan de houder te tonen voordat die toestemming geeft.
Wat bewijst het bewijs van bezit eigenlijk?
Het bewijst dat de wallet die de credential aanvraagt de private key bezit waaraan de credential wordt gebonden, niet enkel dat er een geldig accesstoken is. De wallet ondertekent een proof JWT over de identifier van de uitgever en een verse c_nonce van het nonce endpoint van de uitgever, met die sleutel. De uitgever neemt de bijbehorende publieke sleutel op in de credential. Een verifier die key binding vereist, vraagt de houder om bij presentatie opnieuw met dezelfde sleutel te ondertekenen, zodat een gekopieerde credential zonder de sleutel die controle niet doorstaat.
Waarom zou een uitgever de uitgifte uitstellen in plaats van de credential meteen terug te geven?
Sommige controles die de uitgever uitvoert voordat een credential wordt aangemaakt, kunnen niet binnen één HTTP-verzoek worden afgerond, bijvoorbeeld een handmatige beoordeling of een aanroep van een traag extern register. Bij uitgestelde uitgifte antwoordt het credential endpoint meteen met een transaction_id in plaats van de verbinding te blokkeren, en de wallet bevraagt het deferred endpoint met dat id tot de controle klaar is en de credential kan worden opgehaald.
Is het notification endpoint verplicht voor een uitgever?
Nee. Het is optioneel voor uitgevers, en wallets zijn ook niet verplicht het te gebruiken. Wanneer beide het ondersteunen, verneemt de uitgever wat er na de uitgifte gebeurde: de credentials zijn opgeslagen (credential_accepted), de houder heeft de uitgifte gestopt, bijvoorbeeld door te weigeren ze op te slaan (credential_deleted), of het is om een andere reden mislukt (credential_failure). Aflevering is niet gegarandeerd, dus een uitgever moet een melding als nuttige informatie behandelen, nooit als betrouwbare registratie, en kan aan een ontbrekende melding geen conclusies verbinden.
Bronnen
Deze pagina is informatief en vormt geen juridisch advies. Raadpleeg voor gezaghebbende richtlijnen rechtstreeks de OpenID Foundation en de Europese Commissie.