Émission de credentials OpenID4VCI expliquée : de l'offre au credential accepté
OpenID4VCI, OpenID for Verifiable Credential Issuance, définit comment un wallet demande et reçoit un credential auprès d'un émetteur. Une seule émission traverse plusieurs étapes distinctes avant que le wallet ne détienne réellement un credential utilisable, et chacune a son propre mode d'échec. Cette page parcourt ce cycle de vie complet, avec des exemples détaillés originaux.
Deux façons de démarrer : pre-authorized code et authorization code
L'émission démarre souvent à partir d'une Credential Offer envoyée par l'émetteur, et le grant qu'elle indique décide comment le wallet est autorisé. Un wallet peut aussi démarrer l'émission lui-même, sans aucune offre, via le flux authorization code. Les deux flux aboutissent au même point : le wallet détient un token d'accès qu'il peut utiliser pour demander le credential.
Flux pre-authorized code
1. Émetteur
Connaît déjà le détenteur et remet une Credential Offer avec un pre-authorized_code
2. Wallet
Échange le code auprès du token endpoint, éventuellement avec un code de transaction
3. Wallet
Demande le credential avec une preuve de possession de sa clé
Flux authorization code
1. Wallet
Scanne une Credential Offer indiquant un grant authorization_code, ou démarre le flux elle-même sans offre
2. Serveur d'autorisation
Fait passer le détenteur par la connexion et le consentement, puis émet un code
3. Wallet
Échange le code contre un token, puis demande le credential
Le cycle de vie complet d'une offre de credential
La spécification ne définit pas d'états nommés, mais un flux initié par l'émetteur où le credential est émis immédiatement est le plus simple à suivre, comme dans la séquence ci-dessous. Chaque étape peut échouer à sa manière, et une implémentation de wallet doit gérer ces chemins, pas seulement le scénario favorable.
Exemple détaillé : un certificat de contrôle technique pour une flotte de transport
Un organisme de contrôle technique des véhicules émet un certificat de contrôle technique au wallet professionnel d'une entreprise de transport après un contrôle de routine. L'inspecteur a déjà authentifié le gestionnaire de flotte au point de contrôle, l'émetteur utilise donc le flux pre-authorized code. Les étapes ci-dessous suivent cette émission unique, de l'offre au credential accepté.
L'exemple présente l'OpenID4VCI de base. Les déploiements à haute garantie, comme l'EUDI Wallet, appliquent en plus le profil HAIP, qui ajoute des tokens d'accès liés par DPoP, une wallet attestation au token endpoint et une key attestation pour les clés du credential. Ces éléments sont omis ici pour garder chaque étape lisible.
1. Credential offer
Le terminal de l'organisme de contrôle affiche un QR code. Il contient une URI commençant par openid-credential-offer:// qui transporte l'offre ci-dessous, encodée en URL dans un paramètre credential_offer, ou une credential_offer_uri à partir de laquelle le wallet la récupère. Le wallet la scanne et lit quel credential est proposé et comment l'obtenir.
Émetteur
Affiche un QR code avec une Credential Offer
Indique la configuration de credential et un grant pre-authorized_code
{
"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. Découverte des métadonnées de l'émetteur
Avant de demander quoi que ce soit, le wallet récupère les métadonnées de l'émetteur pour savoir ce que contient roadworthiness_certificate et quels endpoints appeler. Les métadonnées ne listent pas de serveur d'autorisation séparé, donc l'émetteur est son propre serveur d'autorisation, et le wallet lit le token endpoint dans les métadonnées de ce serveur.
Wallet
GET /.well-known/openid-credential-issuer
Découvre le format du credential, les claims et les types de proof acceptés, ainsi que les endpoints nonce, credential, deferred et notification
Métadonnées de l'émetteur de credentials (extrait)
{
"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"] }
]
}
}
}
}Métadonnées du serveur d'autorisation (extrait), depuis /.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. Requête de token
Le wallet échange le pre-authorized code auprès du token endpoint, avec le code de transaction que l'organisme de contrôle a envoyé au téléphone du gestionnaire de flotte. Envoyer ce code par un second canal signifie que quelqu'un qui photographie le QR code par-dessus l'épaule ne peut toujours pas l'échanger.
Wallet
POST /token
Envoie le pre-authorized_code et le tx_code, reçoit un token d'accès limité à cette offre
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
Réponse
{
"access_token": "fi-at-3d91e0",
"token_type": "Bearer",
"expires_in": 86400
}4. Preuve de possession
Comme les métadonnées listent un nonce_endpoint, le wallet y récupère d'abord un c_nonce récent. Il prouve ensuite qu'il détient la clé privée à laquelle le credential sera lié, en signant un proof JWT sur l'identifiant de l'émetteur et ce c_nonce.
Wallet
POST /nonce, puis signe un proof JWT avec la clé à laquelle le credential sera lié
Lie le credential à cette clé, pas simplement à qui détient le token d'accès
POST /nonce HTTP/1.1 Host: issuer.fleetinspect.example
Réponse
{
"c_nonce": "fi-nonce-77aa"
}Le wallet construit maintenant le proof JWT à partir de deux objets JSON, un header et un payload, et les signe avec la clé privée à laquelle le credential sera lié.
Header : ce qu'est ce JWT et quelle clé l'a signé
{
"typ": "openid4vci-proof+jwt",
"alg": "ES256",
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}typ: marque ceci comme une key proof OpenID4VCI, afin qu'elle ne soit pas confondue avec un autre type de JWTalg: l'algorithme de signature, l'un de ceux que l'émetteur a listés dans proof_signing_alg_values_supportedjwk: la clé publique à laquelle le credential sera lié ; l'émetteur vérifie la signature avec celle-ci
Payload : pour qui est la proof et quand elle a été créée
{
"aud": "https://issuer.fleetinspect.example",
"iat": 1789376400,
"nonce": "fi-nonce-77aa"
}aud: l'identifiant de l'émetteur, afin que la proof ne puisse pas être rejouée chez un autre émetteuriat: le moment où la proof a été créée, en secondes depuis 1970nonce: le c_nonce provenant du nonce endpoint, qui montre que la proof est récente
Résultat signé
Le header et le payload sont chacun encodés en base64url puis reliés par un point. Le wallet signe cette chaîne avec sa clé privée et ajoute la signature encodée en base64url après un second point. La chaîne résultante est le proof JWT que le wallet envoie dans la requête de credential à l'étape 5.
base64url(header) . base64url(payload) . base64url(signature) eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIs... .eyJhdWQiOiJodHRwczovL2lzc3Vlci5mbGVldGluc3BlY3QuZXhhbXBsZSIs... .<ES256 signature>
5. Requête de credential
Le wallet appelle le credential endpoint avec le token d'accès et la proof, et l'émetteur génère et renvoie le credential signé.
Wallet
POST /credential
Envoie le token d'accès, l'id de configuration et le proof JWT, reçoit le credential signé et un 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>"]
}
}Réponse
{
"credentials": [
{ "credential": "<issuer-signed SD-JWT VC>" }
],
"notification_id": "fi-notif-9012"
}Tout le flux en un coup d'œil
Ce diagramme de séquence rassemble les cinq étapes de l'exemple détaillé, de l'offre à la notification. Les flèches pleines sont des requêtes, les flèches en pointillés sont des réponses, et la flèche en pointillé fin représente le code de transaction voyageant hors du protocole par SMS.
Wallet
Wallet professionnel de l'entreprise de transport
Serveur d'autorisation
Géré ici par l'émetteur lui-même dans cet exemple
Émetteur de credentials
Organisme de contrôle technique des véhicules
- Émetteur de credentials vers Wallet: Credential Offer, présentée sous forme de QR code
- Émetteur de credentials vers Wallet: tx_code, envoyé par SMS au téléphone du gestionnaire de flotte
- Wallet vers Émetteur de credentials: GET /.well-known/openid-credential-issuer
- Émetteur de credentials vers Wallet: métadonnées de l'émetteur de credentials
- Wallet vers Serveur d'autorisation: GET /.well-known/oauth-authorization-server
- Serveur d'autorisation vers Wallet: métadonnées du serveur d'autorisation
- Wallet vers Serveur d'autorisation: POST /token: pre-authorized_code, tx_code
- Serveur d'autorisation vers Wallet: access_token
- Wallet vers Émetteur de credentials: POST /nonce
- Émetteur de credentials vers Wallet: c_nonce
- Wallet: signe le proof JWT
- Wallet vers Émetteur de credentials: POST /credential : token d'accès, proofs
- Émetteur de credentials vers Wallet: credentials, notification_id
- Wallet: valide et stocke
- Wallet vers Émetteur de credentials: POST /notify: credential_accepted
- Émetteur de credentials vers Wallet: 204 No Content
Quand le credential n'est pas encore prêt : l'émission différée
L'exemple ci-dessus suppose que le résultat du contrôle est déjà définitif. Si l'organisme de contrôle doit au contraire faire remonter un résultat limite à un inspecteur senior, le credential endpoint ne peut pas renvoyer le credential immédiatement, il diffère donc l'émission.
Émission immédiate
Le credential endpoint renvoie le credential signé dans la même réponse que la requête.
Émission différée
Le credential endpoint répond avec un code HTTP 202, un transaction_id et un interval. Le wallet interroge le deferred credential endpoint avec cet id, en attendant au moins interval secondes entre les requêtes, jusqu'à ce que le credential soit prêt.
Réponse différée de /credential
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"transaction_id": "fi-tx-55c2",
"interval": 900
}Interrogation de /deferred jusqu'à ce qu'il soit prêt
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.Boucler la boucle : le notification endpoint
Après l'émission, le wallet peut informer l'émetteur de ce qu'il est advenu du credential, à l'aide du notification_id reçu dans la réponse de credential. Les wallets ne sont pas tenus d'envoyer ces notifications et la livraison n'est pas garantie, donc un émetteur ne peut rien interpréter d'une notification absente.
Wallet
Envoie un événement au notification_endpoint de l'émetteur pour le notification_id reçu, ce qui couvre tous les credentials de cette réponse
Stocké dans le wallet
L'émission a échoué pour une autre raison, par exemple parce que le credential n'a pas été validé
L'émission a échoué du fait du détenteur, par exemple parce qu'il a refusé de le stocker
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"
}Termes associés
Questions fréquentes
Comment un wallet sait-il quel type de grant utiliser ?
La Credential Offer indique le grant dans son objet grants. authorization_code est présent lorsque l'émetteur souhaite que le détenteur se connecte dans le cadre du flux. pre-authorized_code est présent lorsque le détenteur était déjà authentifié sur le canal où l'offre a été créée, par exemple par l'inspecteur au point de contrôle dans l'exemple détaillé ci-dessous. Une offre peut lister les deux, le wallet en choisit alors un. Si l'offre ne comporte aucun objet grants, le wallet recherche dans les métadonnées du serveur d'autorisation quels types de grant celui-ci prend en charge.
Pourquoi le wallet récupère-t-il les métadonnées de l'émetteur avant de demander quoi que ce soit ?
La Credential Offer indique seulement les credential_configuration_ids, l'URL de l'émetteur et les grants. Les métadonnées de l'émetteur, servies depuis un chemin well-known, décrivent chaque configuration : son format, ses claims et les types de proof acceptés, ainsi que les endpoints nonce, credential, deferred et notification. Elles indiquent aussi quel serveur d'autorisation utiliser, et les métadonnées de ce serveur fournissent le token endpoint. Sans les deux, le wallet ne saurait pas comment construire des requêtes valides ni quoi montrer au détenteur avant qu'il ne consente.
Que prouve réellement la preuve de possession ?
Elle prouve que le wallet demandant le credential détient la clé privée à laquelle le credential sera lié, et non simplement qu'il dispose d'un token d'accès valide. Le wallet signe un proof JWT sur l'identifiant de l'émetteur et un c_nonce récent obtenu du nonce endpoint de l'émetteur, à l'aide de cette clé. L'émetteur intègre la clé publique correspondante dans le credential. Un verifier qui exige le key binding demande au détenteur de signer à nouveau avec la même clé lors de la présentation, de sorte qu'un credential copié sans la clé échoue à ce contrôle.
Pourquoi un émetteur différerait-il l'émission au lieu de renvoyer le credential immédiatement ?
Certains contrôles que l'émetteur effectue avant de générer un credential ne peuvent pas s'achever dans une seule requête HTTP, par exemple une révision manuelle ou un appel à un registre externe lent. L'émission différée permet au credential endpoint de répondre immédiatement avec un transaction_id plutôt que de bloquer la connexion, et le wallet interroge l'endpoint deferred avec cet id jusqu'à ce que le contrôle se termine et que le credential soit prêt à être récupéré.
Le notification endpoint est-il obligatoire pour un émetteur ?
Non. Il est optionnel pour les émetteurs, et les wallets ne sont pas non plus tenus de l'utiliser. Lorsque les deux le prennent en charge, l'émetteur apprend ce qui s'est passé après l'émission : les credentials ont été stockés (credential_accepted), le détenteur a arrêté l'émission, par exemple en refusant de les stocker (credential_deleted), ou cela a échoué pour une autre raison (credential_failure). La livraison n'est pas garantie, donc un émetteur doit traiter une notification comme une information utile, jamais comme un enregistrement fiable, et ne peut rien déduire d'une notification absente.
Sources
Cette page est informative et ne constitue pas un avis juridique. Pour des conseils faisant autorité, consultez directement l'OpenID Foundation et la Commission européenne.