Naar hoofdinhoud

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.

Aanbod aangemaakt
QR-code of link opent wallet
Aanbod ontvangen
grant ingewisseld
Token verkregen
nonce en bewijs van bezit
Credential aangevraagd
credential aangemaakt
Credential uitgegeven
wallet valideert en slaat op
Credential geaccepteerd
Waar het mis kan gaan, in de volgorde van de stappen:
Code verlopen voor inwisseling
Tokenverzoek geweigerd
Credentialverzoek afgewezen
Credential niet opgeslagen

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 JWT
  • alg: het ondertekeningsalgoritme, een van de algoritmen die de uitgever vermeldde in proof_signing_alg_values_supported
  • jwk: 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 hergebruikt
  • iat: het tijdstip waarop de proof werd gemaakt, in seconden sinds 1970
  • nonce: 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

  1. Credential-uitgever naar Wallet: Credential Offer, getoond als QR-code
  2. Credential-uitgever naar Wallet: tx_code, per sms verstuurd naar de telefoon van de wagenparkbeheerder
  3. Wallet naar Credential-uitgever: GET /.well-known/openid-credential-issuer
  4. Credential-uitgever naar Wallet: metadata van de credential-uitgever
  5. Wallet naar Autorisatieserver: GET /.well-known/oauth-authorization-server
  6. Autorisatieserver naar Wallet: metadata van de autorisatieserver
  7. Wallet naar Autorisatieserver: POST /token: pre-authorized_code, tx_code
  8. Autorisatieserver naar Wallet: access_token
  9. Wallet naar Credential-uitgever: POST /nonce
  10. Credential-uitgever naar Wallet: c_nonce
  11. Wallet: ondertekent proof JWT
  12. Wallet naar Credential-uitgever: POST /credential: accesstoken, proofs
  13. Credential-uitgever naar Wallet: credentials, notification_id
  14. Wallet: valideert en slaat op
  15. Wallet naar Credential-uitgever: POST /notify: credential_accepted
  16. 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.

transaction_idbevraag deferred_credential_endpoint

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

credential_accepted

Opgeslagen in de wallet

credential_failure

Uitgifte mislukt om een andere reden, bijvoorbeeld doordat de credential niet valideerde

credential_deleted

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

  1. OpenID for Verifiable Credential Issuance 1.0
  2. OpenID4VC High Assurance Interoperability Profile 1.0
  3. EUDI Wallet Architecture and Reference Framework

Deze pagina is informatief en vormt geen juridisch advies. Raadpleeg voor gezaghebbende richtlijnen rechtstreeks de OpenID Foundation en de Europese Commissie.

Praat met ons over EUDI Wallet-integratie