Skip to content

Commit f5b3b33

Browse files
committed
UPDATE OID4VCI and OID4VP protocol documentation
1 parent 98c5ab7 commit f5b3b33

4 files changed

Lines changed: 456 additions & 138 deletions

File tree

backend/internal/protocols/oid4vci/plugin.go

Lines changed: 255 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -180,129 +180,352 @@ func (p *Plugin) GetFlowDefinitions() []plugin.FlowDefinition {
180180
{
181181
ID: "oid4vci-pre-authorized",
182182
Name: "OID4VCI Pre-Authorized Code",
183-
Description: "Wallet resolves an offer URI, exchanges pre-authorized code for an access token, and requests an SD-JWT VC.",
183+
Description: "Wallet resolves a credential offer URI, discovers issuer metadata, exchanges a pre-authorized code for an access token with c_nonce, and requests an SD-JWT VC bound to a proof JWT.",
184184
Executable: true,
185185
Category: "issuance",
186186
Steps: []plugin.FlowStep{
187187
{
188188
Order: 1,
189189
Name: "Resolve Credential Offer",
190-
Description: "Wallet resolves credential_offer_uri and validates issuer metadata relationship.",
190+
Description: "Wallet receives credential_offer_uri out-of-band and resolves it by fetching the credential offer object from the Credential Issuer (OID4VCI §4.1).",
191191
From: "Wallet",
192192
To: "Credential Issuer",
193193
Type: "request",
194194
Parameters: map[string]string{
195-
"credential_offer_uri": "Reference URI returned out-of-band to the wallet",
195+
"credential_offer_uri": "Reference URI delivered out-of-band to the wallet",
196196
},
197197
Security: []string{
198-
"Credential offer envelope must include exactly one of credential_offer or credential_offer_uri",
198+
"Credential offer envelope must include exactly one of credential_offer or credential_offer_uri (XOR)",
199+
"Offer must contain credential_issuer, credential_configuration_ids, and grants with pre-authorized_code",
199200
},
200201
},
201202
{
202203
Order: 2,
203-
Name: "Token Request (Pre-Authorized)",
204-
Description: "Wallet uses the pre-authorized code to get a credential access token and c_nonce challenge.",
204+
Name: "Discover Issuer Metadata",
205+
Description: "Wallet fetches Credential Issuer Metadata from /.well-known/openid-credential-issuer to discover supported credential configurations, endpoints, and proof requirements (OID4VCI §5).",
205206
From: "Wallet",
206-
To: "Authorization Server",
207+
To: "Credential Issuer",
207208
Type: "request",
208209
Parameters: map[string]string{
209-
"grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code",
210-
"pre-authorized_code": "Code from offer grants block",
210+
"metadata_path": "/.well-known/openid-credential-issuer/{issuer_path}",
211211
},
212212
Security: []string{
213-
"If tx_code is required by the offer, token request must include tx_code",
214-
"Token response should include c_nonce when nonce freshness checks are enforced",
213+
"Wallet must verify credential_issuer in metadata matches the offer's credential_issuer",
214+
"Metadata must declare credential_configurations_supported for offered configuration IDs",
215215
},
216216
},
217217
{
218218
Order: 3,
219+
Name: "Token Request (Pre-Authorized Code)",
220+
Description: "Wallet exchanges the pre-authorized code from the offer's grant block for an access token at the token endpoint (OID4VCI §6.1).",
221+
From: "Wallet",
222+
To: "Authorization Server",
223+
Type: "request",
224+
Parameters: map[string]string{
225+
"grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code (REQUIRED)",
226+
"pre-authorized_code": "Code from credential offer grants block (REQUIRED)",
227+
},
228+
Security: []string{
229+
"Content-Type must be application/x-www-form-urlencoded",
230+
"Pre-authorized code is single-use and expires within offer TTL",
231+
"If offer grant includes tx_code object, token request must include tx_code",
232+
},
233+
},
234+
{
235+
Order: 4,
236+
Name: "Token Response",
237+
Description: "Authorization Server validates the pre-authorized code and returns an access token with a c_nonce challenge for proof binding (OID4VCI §6.2).",
238+
From: "Authorization Server",
239+
To: "Wallet",
240+
Type: "response",
241+
Parameters: map[string]string{
242+
"access_token": "Bearer token authorizing credential issuance (REQUIRED)",
243+
"token_type": "Bearer (REQUIRED)",
244+
"expires_in": "Token lifetime in seconds",
245+
"scope": "Granted scope for credential issuance",
246+
"c_nonce": "Challenge nonce for proof JWT binding (REQUIRED when nonce freshness enforced)",
247+
"c_nonce_expires_in": "Nonce lifetime in seconds",
248+
},
249+
Security: []string{
250+
"Access token is bound to specific credential_configuration_ids from the offer",
251+
"c_nonce must be used in the next credential request proof and is single-use",
252+
},
253+
},
254+
{
255+
Order: 5,
219256
Name: "Credential Request",
220-
Description: "Wallet submits proof(s) with c_nonce binding and requests selected credential configuration.",
257+
Description: "Wallet submits a credential request with proof JWT(s) bound to the active c_nonce and the issuer audience (OID4VCI §7).",
221258
From: "Wallet",
222259
To: "Credential Issuer",
223260
Type: "request",
224261
Parameters: map[string]string{
225-
"credential_configuration_id": "Requested credential configuration",
226-
"proofs": "Proof object(s) with jwt proof type",
262+
"credential_configuration_id": "Requested credential configuration (REQUIRED)",
263+
"proof / proofs": "Proof object(s) with proof_type=jwt containing signed JWT (REQUIRED when proof_types_supported declared)",
227264
},
228265
Security: []string{
229-
"proofs are mandatory when proof_types_supported is declared",
230-
"Proof audience and nonce must match issuer and fresh c_nonce challenge",
266+
"Proof JWT header typ must be openid4vci-proof+jwt",
267+
"Proof must include iss, sub, aud (issuer ID), nonce (active c_nonce), iat, exp, and cnf.jwk",
268+
"Proof audience must match credential_issuer identifier",
269+
"c_nonce is consumed on use — replayed or expired nonces are rejected",
231270
},
232271
},
233272
{
234-
Order: 4,
235-
Name: "Credential Issued",
236-
Description: "Issuer returns SD-JWT VC serialization bound to verified proof context.",
273+
Order: 6,
274+
Name: "Credential Response",
275+
Description: "Credential Issuer validates proof key binding, nonce freshness, and audience, then returns the issued SD-JWT VC with a fresh c_nonce for subsequent requests (OID4VCI §7.3).",
237276
From: "Credential Issuer",
238277
To: "Wallet",
239278
Type: "response",
279+
Parameters: map[string]string{
280+
"format": "dc+sd-jwt (SD-JWT VC serialization)",
281+
"credential": "Issuer-signed JWT with selective disclosure segments",
282+
"c_nonce": "Next challenge nonce for any follow-up credential requests",
283+
"c_nonce_expires_in": "Next nonce lifetime in seconds",
284+
},
285+
Security: []string{
286+
"Credential must be signed by the issuer using advertised key material and algorithm",
287+
"SD-JWT includes _sd digests for selectively disclosable claims",
288+
"Wallet stores issued credential material securely for later presentation",
289+
},
240290
},
241291
},
242292
},
243293
{
244294
ID: "oid4vci-pre-authorized-tx-code",
245295
Name: "OID4VCI Pre-Authorized Code + tx_code",
246-
Description: "Same as pre-authorized flow but with transaction code enforcement.",
296+
Description: "Pre-authorized credential issuance with mandatory transaction code enforcement at token exchange — the offer's grant block declares a tx_code object requiring wallet to include a user-entered code (OID4VCI §6.1).",
247297
Executable: true,
248298
Category: "issuance",
249299
Steps: []plugin.FlowStep{
250300
{
251301
Order: 1,
252-
Name: "Resolve Offer with tx_code Constraint",
253-
Description: "Offer declares a tx_code object requiring user-entered transaction code.",
302+
Name: "Resolve Credential Offer with tx_code",
303+
Description: "Wallet resolves credential_offer_uri and observes the tx_code object in the pre-authorized_code grant, indicating a transaction code is required at token exchange (OID4VCI §4.1.1).",
254304
From: "Wallet",
255305
To: "Credential Issuer",
256306
Type: "request",
307+
Parameters: map[string]string{
308+
"credential_offer_uri": "Reference URI delivered out-of-band to the wallet",
309+
},
310+
Security: []string{
311+
"Offer grant must include tx_code object with description, length, and input_mode",
312+
"Wallet must prompt user for the transaction code before proceeding to token exchange",
313+
},
257314
},
258315
{
259316
Order: 2,
317+
Name: "Discover Issuer Metadata",
318+
Description: "Wallet fetches Credential Issuer Metadata to discover supported configurations, endpoints, and proof requirements (OID4VCI §5).",
319+
From: "Wallet",
320+
To: "Credential Issuer",
321+
Type: "request",
322+
Parameters: map[string]string{
323+
"metadata_path": "/.well-known/openid-credential-issuer/{issuer_path}",
324+
},
325+
Security: []string{
326+
"Metadata credential_issuer must match the offer's credential_issuer",
327+
},
328+
},
329+
{
330+
Order: 3,
260331
Name: "Token Request with tx_code",
261-
Description: "Wallet includes tx_code alongside pre-authorized code to obtain access token.",
332+
Description: "Wallet includes the user-entered tx_code alongside the pre-authorized code to obtain an access token (OID4VCI §6.1).",
262333
From: "Wallet",
263334
To: "Authorization Server",
264335
Type: "request",
336+
Parameters: map[string]string{
337+
"grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code (REQUIRED)",
338+
"pre-authorized_code": "Code from credential offer grants block (REQUIRED)",
339+
"tx_code": "User-entered transaction code (REQUIRED when tx_code object present in offer)",
340+
},
341+
Security: []string{
342+
"Missing or incorrect tx_code results in invalid_grant error",
343+
"tx_code is delivered via an out-of-band channel (e.g., email, SMS)",
344+
"Content-Type must be application/x-www-form-urlencoded",
345+
},
265346
},
266347
{
267-
Order: 3,
348+
Order: 4,
349+
Name: "Token Response",
350+
Description: "Authorization Server validates pre-authorized code and tx_code, then returns access token with c_nonce challenge (OID4VCI §6.2).",
351+
From: "Authorization Server",
352+
To: "Wallet",
353+
Type: "response",
354+
Parameters: map[string]string{
355+
"access_token": "Bearer token authorizing credential issuance (REQUIRED)",
356+
"token_type": "Bearer (REQUIRED)",
357+
"c_nonce": "Challenge nonce for proof JWT binding (REQUIRED)",
358+
"c_nonce_expires_in": "Nonce lifetime in seconds",
359+
},
360+
Security: []string{
361+
"Access token is bound to credential_configuration_ids from the offer",
362+
"c_nonce is single-use and must appear in the proof JWT nonce claim",
363+
},
364+
},
365+
{
366+
Order: 5,
268367
Name: "Credential Request",
269-
Description: "Wallet sends proof bound to c_nonce and receives issued credential.",
368+
Description: "Wallet submits credential request with proof JWT bound to c_nonce and issuer audience (OID4VCI §7).",
270369
From: "Wallet",
271370
To: "Credential Issuer",
272371
Type: "request",
372+
Parameters: map[string]string{
373+
"credential_configuration_id": "Requested credential configuration (REQUIRED)",
374+
"proof / proofs": "Proof object(s) with proof_type=jwt (REQUIRED)",
375+
},
376+
Security: []string{
377+
"Proof JWT typ must be openid4vci-proof+jwt",
378+
"Proof must bind to active c_nonce and credential issuer audience",
379+
},
380+
},
381+
{
382+
Order: 6,
383+
Name: "Credential Response",
384+
Description: "Credential Issuer validates proof and returns the issued SD-JWT VC with a fresh c_nonce (OID4VCI §7.3).",
385+
From: "Credential Issuer",
386+
To: "Wallet",
387+
Type: "response",
388+
Parameters: map[string]string{
389+
"format": "dc+sd-jwt (SD-JWT VC serialization)",
390+
"credential": "Issuer-signed JWT with selective disclosure segments",
391+
"c_nonce": "Next challenge nonce",
392+
},
393+
Security: []string{
394+
"Credential is signed by the issuer and returned as SD-JWT VC serialization",
395+
},
273396
},
274397
},
275398
},
276399
{
277400
ID: "oid4vci-deferred-issuance",
278401
Name: "OID4VCI Deferred Credential Issuance",
279-
Description: "Wallet receives transaction_id and polls deferred_credential endpoint until issuance is ready.",
402+
Description: "Full pre-authorized code flow where the credential endpoint returns a transaction_id for deferred issuance — wallet polls the deferred_credential endpoint until the credential is ready (OID4VCI §9).",
280403
Executable: true,
281404
Category: "issuance",
282405
Steps: []plugin.FlowStep{
283406
{
284407
Order: 1,
285-
Name: "Initial Credential Request",
286-
Description: "Issuer returns transaction_id instead of immediate credential.",
408+
Name: "Resolve Credential Offer",
409+
Description: "Wallet resolves credential_offer_uri from the Credential Issuer. The offer uses a pre-authorized_code grant and the issuer marks issuance for deferred delivery (OID4VCI §4.1).",
287410
From: "Wallet",
288411
To: "Credential Issuer",
289412
Type: "request",
413+
Parameters: map[string]string{
414+
"credential_offer_uri": "Reference URI delivered out-of-band to the wallet",
415+
},
416+
Security: []string{
417+
"Offer envelope must include exactly one of credential_offer or credential_offer_uri",
418+
},
290419
},
291420
{
292421
Order: 2,
293-
Name: "Poll Deferred Endpoint",
294-
Description: "Wallet polls deferred_credential endpoint using transaction_id.",
422+
Name: "Discover Issuer Metadata",
423+
Description: "Wallet fetches Credential Issuer Metadata to discover supported configurations, token endpoint, nonce endpoint, and deferred_credential_endpoint (OID4VCI §5).",
295424
From: "Wallet",
296425
To: "Credential Issuer",
297426
Type: "request",
427+
Parameters: map[string]string{
428+
"metadata_path": "/.well-known/openid-credential-issuer/{issuer_path}",
429+
},
430+
Security: []string{
431+
"Metadata must declare deferred_credential_endpoint for deferred issuance support",
432+
},
298433
},
299434
{
300435
Order: 3,
301-
Name: "Deferred Credential Ready",
302-
Description: "Issuer returns final credential once issuance backend marks transaction ready.",
436+
Name: "Token Request (Pre-Authorized Code)",
437+
Description: "Wallet exchanges pre-authorized code for an access token with c_nonce (OID4VCI §6.1).",
438+
From: "Wallet",
439+
To: "Authorization Server",
440+
Type: "request",
441+
Parameters: map[string]string{
442+
"grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code (REQUIRED)",
443+
"pre-authorized_code": "Code from credential offer grants block (REQUIRED)",
444+
},
445+
Security: []string{
446+
"Content-Type must be application/x-www-form-urlencoded",
447+
},
448+
},
449+
{
450+
Order: 4,
451+
Name: "Token Response",
452+
Description: "Authorization Server returns access token and c_nonce challenge for proof binding (OID4VCI §6.2).",
453+
From: "Authorization Server",
454+
To: "Wallet",
455+
Type: "response",
456+
Parameters: map[string]string{
457+
"access_token": "Bearer token (REQUIRED)",
458+
"c_nonce": "Challenge nonce for proof binding (REQUIRED)",
459+
"c_nonce_expires_in": "Nonce lifetime in seconds",
460+
},
461+
Security: []string{
462+
"Access token lineage is verified on deferred polling — only the original token can retrieve the credential",
463+
},
464+
},
465+
{
466+
Order: 5,
467+
Name: "Credential Request",
468+
Description: "Wallet submits credential request with proof JWT bound to c_nonce. Issuer accepts proof but defers credential delivery (OID4VCI §7).",
469+
From: "Wallet",
470+
To: "Credential Issuer",
471+
Type: "request",
472+
Parameters: map[string]string{
473+
"credential_configuration_id": "Requested credential configuration (REQUIRED)",
474+
"proof / proofs": "Proof object(s) with proof_type=jwt (REQUIRED)",
475+
},
476+
Security: []string{
477+
"Proof JWT typ must be openid4vci-proof+jwt",
478+
"Proof must bind to active c_nonce and credential issuer audience",
479+
},
480+
},
481+
{
482+
Order: 6,
483+
Name: "Deferred Credential Response",
484+
Description: "Credential Issuer validates proof but responds with a transaction_id instead of an immediate credential, signalling deferred issuance (OID4VCI §9).",
303485
From: "Credential Issuer",
304486
To: "Wallet",
305487
Type: "response",
488+
Parameters: map[string]string{
489+
"transaction_id": "Opaque identifier for the pending issuance transaction (REQUIRED)",
490+
"c_nonce": "Next challenge nonce",
491+
"c_nonce_expires_in": "Next nonce lifetime in seconds",
492+
},
493+
Security: []string{
494+
"transaction_id is bound to the original access token — a different token cannot poll for this credential",
495+
},
496+
},
497+
{
498+
Order: 7,
499+
Name: "Poll Deferred Endpoint",
500+
Description: "Wallet polls the deferred_credential endpoint with transaction_id until the credential is ready. Issuer returns issuance_pending while processing (OID4VCI §9.1).",
501+
From: "Wallet",
502+
To: "Credential Issuer",
503+
Type: "request",
504+
Parameters: map[string]string{
505+
"transaction_id": "Transaction ID from deferred credential response (REQUIRED)",
506+
"Authorization": "Bearer {access_token} (REQUIRED — must match original token lineage)",
507+
},
508+
Security: []string{
509+
"Access token must match the token used in the original credential request",
510+
"Wallet should poll according to server guidance and apply reasonable backoff",
511+
"issuance_pending error indicates credential is not yet ready",
512+
},
513+
},
514+
{
515+
Order: 8,
516+
Name: "Credential Ready",
517+
Description: "Credential Issuer returns the final SD-JWT VC once the issuance backend marks the transaction as ready (OID4VCI §9.1).",
518+
From: "Credential Issuer",
519+
To: "Wallet",
520+
Type: "response",
521+
Parameters: map[string]string{
522+
"format": "dc+sd-jwt (SD-JWT VC serialization)",
523+
"credential": "Issuer-signed JWT with selective disclosure segments",
524+
},
525+
Security: []string{
526+
"Credential is identical to what would have been returned in an immediate response",
527+
"Transaction is consumed — subsequent polls for the same transaction_id are rejected",
528+
},
306529
},
307530
},
308531
},

0 commit comments

Comments
 (0)