@bedrock/vc-delivery 7.13.2 → 7.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,13 +2,18 @@
2
2
  * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
- import {deepEqual, getWorkflowIssuerInstances} from '../helpers.js';
5
+ import * as draft13 from './oid4vciDraft13.js';
6
+ import {issue as defaultIssue, getIssueRequestsParams} from '../issue.js';
7
+ import {getWorkflowIssuerInstances, setVariable} from '../helpers.js';
6
8
  import {importJWK, SignJWT} from 'jose';
9
+ import {timingSafeEqual, randomUUID as uuid} from 'node:crypto';
10
+ import {
11
+ authorizationDetails as authorizationDetailsSchema
12
+ } from '../../schemas/bedrock-vc-workflow.js';
7
13
  import {checkAccessToken} from '@bedrock/oauth2-verifier';
8
- import {issue as defaultIssue} from '../issue.js';
14
+ import {compile} from '@bedrock/validation';
9
15
  import {ExchangeProcessor} from '../ExchangeProcessor.js';
10
16
  import {getStepAuthorizationRequest} from './oid4vp.js';
11
- import {timingSafeEqual} from 'node:crypto';
12
17
  import {verifyDidProofJwt} from '../verify.js';
13
18
 
14
19
  const {util: {BedrockError}} = bedrock;
@@ -16,6 +21,17 @@ const {util: {BedrockError}} = bedrock;
16
21
  const PRE_AUTH_GRANT_TYPE =
17
22
  'urn:ietf:params:oauth:grant-type:pre-authorized_code';
18
23
 
24
+ const VALIDATORS = {
25
+ authorizationDetails: null
26
+ };
27
+
28
+ bedrock.events.on('bedrock.init', () => {
29
+ // create validators for x-www-form-urlencoded parsed data
30
+ VALIDATORS.authorizationDetails = compile({
31
+ schema: authorizationDetailsSchema()
32
+ });
33
+ });
34
+
19
35
  export async function getAuthorizationServerConfig({req}) {
20
36
  // note that technically, we should not need to serve any credential
21
37
  // issuer metadata, but we do for backwards compatibility purposes as
@@ -25,14 +41,21 @@ export async function getAuthorizationServerConfig({req}) {
25
41
 
26
42
  export async function getCredentialIssuerConfig({req}) {
27
43
  const {config: workflow} = req.serviceObject;
28
- const {exchange} = await req.getExchange();
44
+ const exchangeRecord = await req.getExchange();
45
+ const {exchange} = exchangeRecord;
29
46
  _assertOID4VCISupported({exchange});
30
47
 
48
+ // use exchange processor get current step of the exchange
49
+ const exchangeProcessor = new ExchangeProcessor({workflow, exchangeRecord});
50
+ const step = await exchangeProcessor.getStep();
51
+
52
+ // fetch credential configurations for the step
31
53
  const credential_configurations_supported =
32
- _createCredentialConfigurationsSupported({workflow, exchange});
54
+ _getSupportedCredentialConfigurations({workflow, exchange, step});
33
55
 
34
56
  const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
35
57
  return {
58
+ authorization_details_types_supported: ['openid_credential'],
36
59
  batch_credential_endpoint: `${exchangeId}/openid/batch_credential`,
37
60
  credential_configurations_supported,
38
61
  credential_endpoint: `${exchangeId}/openid/credential`,
@@ -47,7 +70,8 @@ export async function getCredentialIssuerConfig({req}) {
47
70
 
48
71
  export async function getCredentialOffer({req}) {
49
72
  const {config: workflow} = req.serviceObject;
50
- const {exchange} = await req.getExchange();
73
+ const exchangeRecord = await req.getExchange();
74
+ const {exchange} = exchangeRecord;
51
75
  _assertOID4VCISupported({exchange});
52
76
 
53
77
  // start building OID4VCI credential offer
@@ -61,13 +85,17 @@ export async function getCredentialOffer({req}) {
61
85
  }
62
86
  };
63
87
 
64
- const supported = _createCredentialConfigurationsSupported({
65
- workflow, exchange
66
- });
88
+ // use exchange processor get current step of the exchange
89
+ const exchangeProcessor = new ExchangeProcessor({workflow, exchangeRecord});
90
+ const step = await exchangeProcessor.getStep();
91
+
92
+ // fetch credential configurations for the step
93
+ const credential_configurations_supported =
94
+ _getSupportedCredentialConfigurations({workflow, exchange, step});
67
95
 
68
96
  // offer all configuration IDs and support both spec version ID-1 with
69
- // `credentials` and draft 14 with `credential_configuration_ids`
70
- const configurationIds = Object.keys(supported);
97
+ // `credentials` and draft 13 with `credential_configuration_ids`
98
+ const configurationIds = Object.keys(credential_configurations_supported);
71
99
  offer.credentials = configurationIds;
72
100
  offer.credential_configuration_ids = configurationIds;
73
101
 
@@ -111,6 +139,14 @@ export async function initExchange({workflow, exchange} = {}) {
111
139
  }
112
140
 
113
141
  export async function processAccessTokenRequest({req}) {
142
+ // parse `authorization_details` from request
143
+ let authorizationDetails;
144
+ if(req.body.authorization_details) {
145
+ authorizationDetails = JSON.parse(req.body.authorization_details);
146
+ _validate(VALIDATORS.authorizationDetails, authorizationDetails);
147
+ }
148
+
149
+ // check exchange
114
150
  const exchangeRecord = await req.getExchange();
115
151
  const {exchange} = exchangeRecord;
116
152
  _assertOID4VCISupported({exchange});
@@ -119,7 +155,8 @@ export async function processAccessTokenRequest({req}) {
119
155
  pre-authz code:
120
156
  grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code
121
157
  &pre-authorized_code=SplxlOBeZQQYbYS6WxSbIA
122
- &user_pin=493536
158
+ &tx_code=493536
159
+ &authorization_details=<URI-component-encoded JSON array>
123
160
 
124
161
  authz code:
125
162
  grant_type=authorization_code
@@ -131,9 +168,7 @@ export async function processAccessTokenRequest({req}) {
131
168
 
132
169
  const {
133
170
  grant_type: grantType,
134
- 'pre-authorized_code': preAuthorizedCode,
135
- // FIXME: `user_pin` now called `tx_code`
136
- //user_pin: userPin
171
+ 'pre-authorized_code': preAuthorizedCode
137
172
  } = req.body;
138
173
 
139
174
  if(grantType !== PRE_AUTH_GRANT_TYPE) {
@@ -154,15 +189,132 @@ export async function processAccessTokenRequest({req}) {
154
189
  }
155
190
  }
156
191
 
192
+ // if authorization details provided in request, generate the set of
193
+ // requested credential configuration IDs
194
+ let requestedIds;
195
+ if(authorizationDetails) {
196
+ // populate token authorization details by matching each requested
197
+ // credential configuration ID with its credential IDs
198
+ requestedIds = new Set(authorizationDetails.map(
199
+ detail => detail.credential_configuration_id));
200
+ }
201
+
202
+ // process exchange to generate supported credential requests
203
+ let supportedCredentialRequestsStored;
204
+ let supportedCredentialRequests;
205
+ const exchangeProcessor = new ExchangeProcessor({
206
+ workflow, exchangeRecord,
207
+ async prepareStep({exchange, step}) {
208
+ // do not generate any VPR yet
209
+ step.verifiablePresentationRequest = undefined;
210
+
211
+ // if not generated, generate supported credential requests and store
212
+ // in step results
213
+ const stepResults = exchange.variables.results[exchange.step];
214
+ supportedCredentialRequests = stepResults
215
+ ?.openId?.supportedCredentialRequests;
216
+ if(supportedCredentialRequests) {
217
+ supportedCredentialRequestsStored = true;
218
+ } else {
219
+ // get all possible supported credential requests
220
+ supportedCredentialRequests = _createSupportedCredentialRequests({
221
+ workflow, exchange, step
222
+ });
223
+
224
+ // if specific credential configuration IDs were requested, filter
225
+ // the supported requests by that list, limiting them to what the
226
+ // client is interested in, thereby disallowing any others in this
227
+ // exchange and establishing completion conditions for it
228
+ if(requestedIds) {
229
+ supportedCredentialRequests = supportedCredentialRequests
230
+ .filter(r => requestedIds.has(r.credentialConfigurationId));
231
+
232
+ // throw an error if *none* of the requested IDs are available
233
+ if(supportedCredentialRequests.length === 0) {
234
+ throw new BedrockError(
235
+ 'None of the requested credential(s) are available.', {
236
+ name: 'NotAllowedError',
237
+ details: {httpStatusCode: 403, public: true}
238
+ });
239
+ }
240
+ }
241
+
242
+ // store supported requests
243
+ exchange.variables.results[exchange.step] = {
244
+ ...exchange.variables.results[exchange.step],
245
+ openId: {
246
+ ...exchange.variables.results[exchange.step]?.openId,
247
+ supportedCredentialRequests
248
+ }
249
+ };
250
+ }
251
+ },
252
+ inputRequired({step}) {
253
+ // if the supported credential requests haven't been stored in the
254
+ // step result yet, then input is not required but issuance needs to
255
+ // be disabled to allow them to be stored and then the step reprocessed
256
+ if(!supportedCredentialRequestsStored) {
257
+ step.issueRequests = [];
258
+ step.verifiablePresentation = undefined;
259
+ return false;
260
+ }
261
+ // requests stored and input is now required via credential endpoint
262
+ return true;
263
+ },
264
+ isStepComplete() {
265
+ // getting an access token never completes the step
266
+ return false;
267
+ }
268
+ });
269
+ await exchangeProcessor.process();
270
+
271
+ // process `authorizationDetails` request, if any, to create token
272
+ // authorization details
273
+ let tokenAuthorizationDetails;
274
+ if(authorizationDetails) {
275
+ // create map of unprocessed credential configuration ID => credential IDs
276
+ const idMap = new Map();
277
+ for(const request of supportedCredentialRequests) {
278
+ if(!supportedCredentialRequests.processed) {
279
+ let credentialIds = idMap.get(request.credentialConfigurationId);
280
+ if(!credentialIds) {
281
+ credentialIds = [];
282
+ idMap.set(request.credentialConfigurationId, credentialIds);
283
+ }
284
+ credentialIds.push(request.credentialIdentifier);
285
+ }
286
+ }
287
+
288
+ // populate token authorization details by matching each requested
289
+ // credential configuration ID with its credential IDs
290
+ tokenAuthorizationDetails = [];
291
+ for(const credentialConfigurationId of requestedIds) {
292
+ const credentialIds = idMap.get(credentialConfigurationId);
293
+ if(credentialIds) {
294
+ tokenAuthorizationDetails.push({
295
+ type: 'openid_credential',
296
+ credential_configuration_id: credentialConfigurationId,
297
+ credential_identifiers: credentialIds
298
+ });
299
+ }
300
+ }
301
+ }
302
+
157
303
  // create access token
158
304
  const {accessToken, ttl} = await _createExchangeAccessToken({
159
305
  workflow, exchangeRecord
160
306
  });
161
- return {
307
+
308
+ // return token info
309
+ const tokenInfo = {
162
310
  access_token: accessToken,
163
311
  token_type: 'bearer',
164
312
  expires_in: ttl
165
313
  };
314
+ if(tokenAuthorizationDetails) {
315
+ tokenInfo.authorization_details = tokenAuthorizationDetails;
316
+ }
317
+ return tokenInfo;
166
318
  }
167
319
 
168
320
  export async function processCredentialRequests({req, res, isBatchRequest}) {
@@ -174,62 +326,231 @@ export async function processCredentialRequests({req, res, isBatchRequest}) {
174
326
  // ensure oauth2 access token is valid
175
327
  await _checkAuthz({req, workflow, exchange});
176
328
 
177
- return _processExchange({req, res, workflow, exchangeRecord, isBatchRequest});
178
- }
329
+ // process exchange and capture values to return
330
+ let didProofRequired = false;
331
+ let format;
332
+ let issueResult;
333
+ let matchingCredentialIdentifiers;
334
+ let supportedCredentialRequests;
335
+ const exchangeProcessor = new ExchangeProcessor({
336
+ workflow, exchangeRecord,
337
+ async prepareStep({exchange, step}) {
338
+ // get `supportedCredentialRequests` from step results
339
+ supportedCredentialRequests = await _getSupportedCredentialRequests({
340
+ exchangeProcessor, workflow, exchange, step
341
+ });
179
342
 
180
- export function supportsOID4VCI({exchange}) {
181
- return exchange.openId?.expectedCredentialRequests !== undefined;
182
- }
343
+ // if the issue result has been generated, return early and allow
344
+ // `isInputRequired()` to handle further processing
345
+ if(issueResult) {
346
+ return;
347
+ }
183
348
 
184
- function _assertCredentialRequests({
185
- workflow, credentialRequests, expectedCredentialRequests
186
- }) {
187
- // ensure that every credential request is for the same format
188
- /* credential requests look like:
189
- {
190
- format: 'ldp_vc',
191
- credential_definition: { '@context': [Array], type: [Array] }
192
- }
193
- */
194
- let sharedFormat;
195
- if(!credentialRequests.every(({format}) => {
196
- if(sharedFormat === undefined) {
197
- sharedFormat = format;
198
- }
199
- return sharedFormat === format;
200
- })) {
201
- throw new BedrockError(
202
- 'Credential requests must all use the same format in this workflow.', {
203
- name: 'DataError',
204
- details: {httpStatusCode: 400, public: true}
205
- });
206
- }
349
+ // fetch credential configurations for the step
350
+ const supportedCredentialConfigurations =
351
+ _getSupportedCredentialConfigurations({workflow, exchange, step});
352
+
353
+ // get credential requests (only more than one w/`isBatchRequest=true`)
354
+ let credentialRequests = isBatchRequest ?
355
+ req.body.credential_requests : [req.body];
356
+
357
+ // normalize draft 13 requests
358
+ if(isBatchRequest || req.body?.format) {
359
+ credentialRequests = draft13.normalizeCredentialRequestsToVersion1({
360
+ credentialRequests,
361
+ supportedCredentialConfigurations
362
+ });
363
+ // set `format`
364
+ const configuration = supportedCredentialConfigurations[
365
+ credentialRequests[0].credential_configuration_id];
366
+ ({format} = configuration);
367
+ }
207
368
 
208
- // ensure that the shared format is supported by the workflow
209
- const supportedFormats = _getSupportedFormats({workflow});
210
- if(!supportedFormats.has(sharedFormat)) {
211
- throw new BedrockError(
212
- `Credential request format "${sharedFormat}" is not supported ` +
213
- 'by this workflow.', {
214
- name: 'DataError',
215
- details: {httpStatusCode: 400, public: true}
216
- });
217
- }
369
+ // map each credential request to an appropriate supported credential
370
+ // request; for OID4VCI 1.0+ clients, there will be a single request
371
+ // with a `credential_identifier`; for draft 13, one or more requests,
372
+ // each with `credential_configuration_id` set will be present and
373
+ // these must map to *every* matching supported request
374
+ const unprocessed = supportedCredentialRequests.filter(r => !r.processed);
375
+ matchingCredentialIdentifiers = new Set();
376
+ for(const credentialRequest of credentialRequests) {
377
+ const {
378
+ credential_identifier, credential_configuration_id
379
+ } = credentialRequest;
380
+
381
+ // OID4VCI 1.0+ case
382
+ if(credentialRequest.credential_identifier) {
383
+ const match = unprocessed.find(
384
+ r => r.credentialIdentifier === credential_identifier);
385
+ if(match) {
386
+ matchingCredentialIdentifiers.add(credential_identifier);
387
+ }
388
+ break;
389
+ }
218
390
 
219
- // ensure every credential request matches against an expected one and none
220
- // are missing; `expectedCredentialRequests` formats are ignored based on the
221
- // issuer instance supported formats and have already been checked
222
- if(!(credentialRequests.length === expectedCredentialRequests.length &&
223
- credentialRequests.every(cr => expectedCredentialRequests.some(
224
- expected => _matchCredentialRequest(expected, cr))))) {
225
- throw new BedrockError(
226
- 'Unexpected credential request.', {
227
- name: 'DataError',
228
- details: {httpStatusCode: 400, public: true}
391
+ // draft 13 case
392
+ unprocessed.filter(
393
+ r => r.credentialConfigurationId === credential_configuration_id)
394
+ .forEach(
395
+ r => matchingCredentialIdentifiers.add(r.credentialIdentifier));
396
+ }
397
+
398
+ // handle no match case
399
+ if(matchingCredentialIdentifiers.size === 0) {
400
+ throw new BedrockError(
401
+ 'The requested credential(s) have already been delivered.', {
402
+ name: 'NotAllowedError',
403
+ details: {httpStatusCode: 403, public: true}
404
+ });
405
+ }
406
+
407
+ // check to see if step supports OID4VP during OID4VCI
408
+ if(step.openId) {
409
+ // Note: either OID4VCI 1.1+ w/IAE (interactive authz endpoint) or
410
+ // OID4VCI-1.0/draft13+OID4VP will have received VP results which will
411
+ // be stored with this step; OID4VCI 1.0- does not have IAE so if this
412
+ // call is made, presume such a client and return an error with the
413
+ // OID4VP request, OID4VCI 1.1+ clients will know to use IAE instead
414
+
415
+ // if there is no verified presentation yet, request one
416
+ const {results} = exchange.variables;
417
+ if(!results[exchange.step]?.verifyPresentationResults?.verified) {
418
+ // note: only the "default" `clientProfileId` is supported at this
419
+ // time because there isn't presently a defined way to specify
420
+ // alternatives
421
+ const clientProfileId = step.openId.clientProfiles ?
422
+ 'default' : undefined;
423
+ // get authorization request
424
+ const {authorizationRequest} = await getStepAuthorizationRequest({
425
+ workflow, exchange, step, clientProfileId
426
+ });
427
+ return _requestOID4VP({authorizationRequest, res});
428
+ }
429
+ return;
430
+ }
431
+
432
+ // check to see if step requires a DID proof
433
+ if(step.jwtDidProofRequest) {
434
+ // handle OID4VCI specialized JWT DID Proof request...
435
+
436
+ // `proof` must be in every credential request; if any request is
437
+ // missing `proof` then request a DID proof
438
+ if(credentialRequests.some(cr => !cr.proofs?.jwt)) {
439
+ didProofRequired = true;
440
+ return _requestDidProof({res, exchangeRecord});
441
+ }
442
+
443
+ // verify every DID proof and get resulting DIDs
444
+ const results = await Promise.all(
445
+ credentialRequests.map(async cr => {
446
+ // FIXME: do not support more than one proof at this time
447
+ const {proofs: {jwt: [jwt]}} = cr;
448
+ const {did} = await verifyDidProofJwt({workflow, exchange, jwt});
449
+ return did;
450
+ }));
451
+ // require `did` to be the same for every proof
452
+ // FIXME: determine if this needs to be more flexible
453
+ const did = results[0];
454
+ if(results.some(d => did !== d)) {
455
+ // FIXME: improve error
456
+ throw new Error('every DID must be the same');
457
+ }
458
+ // store did results in variables associated with current step
459
+ exchange.variables.results[exchange.step] = {
460
+ ...exchange.variables.results[exchange.step],
461
+ // common use case of DID Authentication; provide `did` for ease
462
+ // of use in templates
463
+ did
464
+ };
465
+ }
466
+ },
467
+ inputRequired({exchange, step}) {
468
+ // if issue result has been generated...
469
+ if(issueResult) {
470
+ // reapply any stored credentials in case the exchange was concurrently
471
+ // updated by another credential request call
472
+ const {storedCredentials, matchingCredentialIdentifiers} = issueResult;
473
+ if(issueResult.storedCredentials) {
474
+ for(const stored of storedCredentials) {
475
+ setVariable({
476
+ variables: exchange.variables,
477
+ name: stored.name,
478
+ value: stored.value
479
+ });
480
+ }
481
+ }
482
+ // mark all matching `supportedCredentialRequests` as processed
483
+ supportedCredentialRequests
484
+ .filter(r =>
485
+ matchingCredentialIdentifiers.has(r.credentialIdentifier))
486
+ .forEach(r => r.processed = true);
487
+
488
+ // do not generate any VPR or issue anything else, additional requests
489
+ // must be made when using OID4VCI
490
+ step.verifiablePresentationRequest = undefined;
491
+ step.verifiablePresentation = undefined;
492
+ step.issueRequests = [];
493
+
494
+ // if any supported credential requests has not yet been processed,
495
+ // then input is required in the form of another credential request
496
+ return supportedCredentialRequests.some(r => !r.processed);
497
+ }
498
+
499
+ // otherwise, input is required if:
500
+ // 1. a `jwtDidProofRequest` is required and hasn't been provided
501
+ // 2. OID4VP is enabled and no OID4VP result has been stored yet
502
+ return didProofRequired || (step.openId && !exchange.variables
503
+ .results[exchange.step]?.openId?.authorizationRequest);
504
+ },
505
+ async issue({
506
+ workflow, exchange, step, issueRequestsParams,
507
+ verifiablePresentation
508
+ }) {
509
+ // issue result already generated, skip
510
+ if(issueResult) {
511
+ return issueResult;
512
+ }
513
+ // filter `supportedCredentialRequests` using matching credential
514
+ // identifiers and map to only those `issueRequestsParams` that are to
515
+ // be issued now
516
+ issueRequestsParams = supportedCredentialRequests
517
+ .filter(r => matchingCredentialIdentifiers.has(r.credentialIdentifier))
518
+ .map(r => issueRequestsParams[r.issueRequestsParamsIndex])
519
+ .filter(p => !!p);
520
+
521
+ // perform issuance and capture result to return it to the client and
522
+ // to prevent subsequent reissuance if a concurrent request is made for
523
+ // other credentials
524
+ issueResult = await defaultIssue({
525
+ workflow, exchange, step, issueRequestsParams,
526
+ verifiablePresentation, format
229
527
  });
528
+ issueResult.matchingCredentialIdentifiers =
529
+ matchingCredentialIdentifiers;
530
+ return issueResult;
531
+ },
532
+ isStepComplete() {
533
+ // step complete if all supported credential requests have been processed
534
+ return supportedCredentialRequests.every(r => r.processed);
535
+ }
536
+ });
537
+ // always allow retrying with OID4VCI; issued VCs will be stored in memory
538
+ // and an assumption is made that workflow steps WILL NOT change issue
539
+ // requests in a step once `supportedCredentialRequests` has been created
540
+ exchangeProcessor.canRetry = true;
541
+ await exchangeProcessor.process();
542
+ // use `issueResult` response
543
+ const response = issueResult?.response;
544
+ if(!response?.verifiablePresentation) {
545
+ return null;
230
546
  }
547
+ return {response, format};
548
+ }
231
549
 
232
- return {format: sharedFormat};
550
+ export function supportsOID4VCI({exchange}) {
551
+ // FIXME: might want something more explicit/or check in `workflow` and not
552
+ // exchange
553
+ return exchange.openId?.preAuthorizedCode !== undefined;
233
554
  }
234
555
 
235
556
  function _assertOID4VCISupported({exchange}) {
@@ -281,6 +602,39 @@ async function _createExchangeAccessToken({workflow, exchangeRecord}) {
281
602
  return {accessToken, ttl};
282
603
  }
283
604
 
605
+ function _createSupportedCredentialRequests({
606
+ workflow, exchange, step
607
+ }) {
608
+ let supportedCredentialRequests;
609
+
610
+ const issueRequestsParams = getIssueRequestsParams({
611
+ workflow, exchange, step
612
+ });
613
+
614
+ // determine if issue request params is legacy or modern
615
+ const isDraft13 = issueRequestsParams.some(
616
+ p => !p?.oid4vci?.credentialConfigurationId);
617
+ if(isDraft13) {
618
+ supportedCredentialRequests =
619
+ draft13.createSupportedCredentialRequests({
620
+ workflow, exchange, issueRequestsParams
621
+ });
622
+ } else {
623
+ supportedCredentialRequests = [];
624
+ for(const [index, params] of issueRequestsParams.entries()) {
625
+ const {credentialConfigurationId} = params.oid4vci;
626
+ supportedCredentialRequests.push({
627
+ credentialConfigurationId,
628
+ credentialIdentifier: uuid(),
629
+ issueRequestsParamsIndex: index,
630
+ processed: false
631
+ });
632
+ }
633
+ }
634
+
635
+ return supportedCredentialRequests;
636
+ }
637
+
284
638
  async function _createOAuth2AccessToken({
285
639
  privateKeyJwk, audience, action, target, exp, iss, nbf, typ = 'at+jwt'
286
640
  }) {
@@ -307,68 +661,6 @@ async function _createOAuth2AccessToken({
307
661
  return {accessToken, ttl};
308
662
  }
309
663
 
310
- function _createCredentialConfigurationId({format, credential_definition}) {
311
- let types = (credential_definition.type ?? credential_definition.types);
312
- if(types.length > 1) {
313
- types = types.filter(t => t !== 'VerifiableCredential');
314
- }
315
- return types.join('_') + '_' + format;
316
- }
317
-
318
- function _createCredentialConfigurations({
319
- credentialRequest, supportedFormats
320
- }) {
321
- const configurations = [];
322
-
323
- let {format: formats = supportedFormats} = credentialRequest;
324
- if(!Array.isArray(formats)) {
325
- formats = [formats];
326
- }
327
-
328
- for(const format of formats) {
329
- const {credential_definition} = credentialRequest;
330
- const id = _createCredentialConfigurationId({
331
- format, credential_definition
332
- });
333
- const configuration = {format, credential_definition};
334
- // FIXME: if `jwtDidProofRequest` exists in (any) step in the exchange,
335
- // then must include:
336
- /*
337
- "proof_types_supported": {
338
- "jwt": {
339
- "proof_signing_alg_values_supported": [
340
- "ES256"
341
- ]
342
- }
343
- }
344
- */
345
- configurations.push({id, configuration});
346
- }
347
-
348
- return configurations;
349
- }
350
-
351
- function _createCredentialConfigurationsSupported({workflow, exchange}) {
352
- // build `credential_configurations_supported`...
353
- const {openId: {expectedCredentialRequests}} = exchange;
354
- const supportedFormats = [..._getSupportedFormats({workflow})];
355
-
356
- // for every expected credential definition, set `format` default to
357
- // `supportedFormats` and for every format, generate a new supported
358
- // credential configuration
359
- const credential_configurations_supported = {};
360
- for(const credentialRequest of expectedCredentialRequests) {
361
- const configurations = _createCredentialConfigurations({
362
- credentialRequest, supportedFormats
363
- });
364
- for(const {id, configuration} of configurations) {
365
- credential_configurations_supported[id] = configuration;
366
- }
367
- }
368
-
369
- return credential_configurations_supported;
370
- }
371
-
372
664
  function _getAlgFromPrivateKey({privateKeyJwk}) {
373
665
  if(privateKeyJwk.alg) {
374
666
  return privateKeyJwk.alg;
@@ -390,162 +682,68 @@ function _getAlgFromPrivateKey({privateKeyJwk}) {
390
682
  return 'invalid';
391
683
  }
392
684
 
393
- function _getSupportedFormats({workflow}) {
394
- // get all supported formats from available issuer instances; for simple
395
- // workflow configs, a single issuer instance is used
396
- const supportedFormats = new Set();
397
- const issuerInstances = getWorkflowIssuerInstances({workflow});
398
- issuerInstances.forEach(
399
- instance => instance.supportedFormats.forEach(
400
- supportedFormats.add, supportedFormats));
401
- return supportedFormats;
402
- }
685
+ function _getSupportedCredentialConfigurations({workflow, exchange, step}) {
686
+ // get all OID4VCI credential configuration IDs from issue requests in step
687
+ const issueRequestsParams = getIssueRequestsParams({
688
+ workflow, exchange, step
689
+ });
690
+ const credentialConfigurationIds = new Set([
691
+ ...issueRequestsParams
692
+ .map(p => p?.oid4vci?.credentialConfigurationId)
693
+ .filter(id => id !== undefined)
694
+ ]);
403
695
 
404
- function _matchCredentialRequest(expected, cr) {
405
- const {credential_definition: {'@context': c1, type: t1}} = expected;
406
- const {credential_definition: {'@context': c2, type: t2}} = cr;
407
- // contexts must match exactly but types can have different order
408
- return (c1.length === c2.length && t1.length === t2.length &&
409
- deepEqual(c1, c2) && t1.every(t => t2.some(x => t === x)));
410
- }
696
+ // get all issuer instances for the workflow
697
+ const issuerInstances = getWorkflowIssuerInstances({workflow});
411
698
 
412
- function _normalizeCredentialDefinitionTypes({credentialRequests}) {
413
- // normalize credential requests to use `type` instead of `types`
414
- for(const cr of credentialRequests) {
415
- if(cr?.credential_definition?.types) {
416
- if(!cr?.credential_definition?.type) {
417
- cr.credential_definition.type = cr.credential_definition.types;
699
+ // in modern workflows, credential configuration IDs are explicitly provided
700
+ if(credentialConfigurationIds.size > 0) {
701
+ // map each ID to a credential configuration in an issuer instance
702
+ const supported = new Map();
703
+ for(const id of credentialConfigurationIds) {
704
+ const match = issuerInstances.find(
705
+ ii => ii.oid4vci?.supportedCredentialConfigurations?.[id]);
706
+ if(match) {
707
+ supported.set(id, match.oid4vci.supportedCredentialConfigurations[id]);
418
708
  }
419
- delete cr.credential_definition.types;
420
709
  }
710
+ return Object.fromEntries(supported.entries());
421
711
  }
712
+
713
+ // no explicit IDs; create legacy supported credential configurations
714
+ return draft13.createSupportedCredentialConfigurations({
715
+ exchange, issuerInstances
716
+ });
422
717
  }
423
718
 
424
- async function _processExchange({
425
- req, res, workflow, exchangeRecord, isBatchRequest
719
+ async function _getSupportedCredentialRequests({
720
+ exchangeProcessor, workflow, exchange, step
426
721
  }) {
427
- // process exchange and capture values to return
428
- let format;
429
- let didProofRequired = false;
430
- const exchangeProcessor = new ExchangeProcessor({
431
- workflow, exchangeRecord,
432
- async prepareStep({exchange, step}) {
433
- // FIXME: handle OID4VCI 1.0+ credential request
434
-
435
- // FIXME: OID4VCI Draft 13:
436
- // validate body against expected credential requests
437
- const {openId: {expectedCredentialRequests}} = exchange;
438
- let credentialRequests;
439
- if(isBatchRequest) {
440
- ({credential_requests: credentialRequests} = req.body);
441
- } else {
442
- if(expectedCredentialRequests.length > 1) {
443
- // FIXME: batch endpoint has been removed in OID4VCI 1.0+; if used,
444
- // then it is for a legacy OID4VCI draft 13 request, so this should
445
- // be reworked
446
- throw new Error('batch_credential_endpoint must be used');
447
- }
448
- credentialRequests = [req.body];
449
- }
450
-
451
- // before asserting, normalize credential requests to use `type` instead
452
- // of `types`; this is to allow for OID4VCI draft implementers that
453
- // followed the non-normative examples
454
- _normalizeCredentialDefinitionTypes({credentialRequests});
455
- ({format} = _assertCredentialRequests({
456
- workflow, credentialRequests, expectedCredentialRequests
457
- }));
458
-
459
- const {jwtDidProofRequest} = step;
460
-
461
- // check to see if step supports OID4VP during OID4VCI
462
- if(step.openId) {
463
- // FIXME: either OID4VCI 1.1+ w/IAE (interactive authz endpoint) or
464
- // OID4VCI-1.0/draft13+OID4VP will have received VP results which will
465
- // be stored with this step; OID4VCI 1.0- does not have IAE so if this
466
- // call is made, presume such a client and return an error with the
467
- // OID4VP request, OID4VCI 1.1+ clients will know to use IAE instead
468
-
469
- // if there is no verified presentation yet, request one
470
- const {results} = exchange.variables;
471
- if(!results[exchange.step]?.verifyPresentationResults?.verified) {
472
- // note: only the "default" `clientProfileId` is supported at this
473
- // time because there isn't presently a defined way to specify
474
- // alternatives
475
- const clientProfileId = step.openId.clientProfiles ?
476
- 'default' : undefined;
477
- // get authorization request
478
- const {authorizationRequest} = await getStepAuthorizationRequest({
479
- workflow, exchange, step, clientProfileId
480
- });
481
- return _requestOID4VP({authorizationRequest, res});
482
- }
483
- // otherwise drop down below to complete exchange...
484
- } else if(jwtDidProofRequest) {
485
- // handle OID4VCI specialized JWT DID Proof request...
486
-
487
- // `proof` must be in every credential request; if any request is
488
- // missing `proof` then request a DID proof
489
- if(credentialRequests.some(cr => !cr.proof?.jwt)) {
490
- didProofRequired = true;
491
- return _requestDidProof({res, exchangeRecord});
492
- }
493
-
494
- // verify every DID proof and get resulting DIDs
495
- const results = await Promise.all(
496
- credentialRequests.map(async cr => {
497
- const {proof: {jwt}} = cr;
498
- const {did} = await verifyDidProofJwt({workflow, exchange, jwt});
499
- return did;
500
- }));
501
- // require `did` to be the same for every proof
502
- // FIXME: determine if this needs to be more flexible
503
- const did = results[0];
504
- if(results.some(d => did !== d)) {
505
- // FIXME: improve error
506
- throw new Error('every DID must be the same');
507
- }
508
- // store did results in variables associated with current step
509
- exchange.variables.results[exchange.step] = {
510
- ...exchange.variables.results[exchange.step],
511
- // common use case of DID Authentication; provide `did` for ease
512
- // of use in templates
513
- did
514
- };
515
- }
516
- },
517
- inputRequired({exchange, step}) {
518
- // input is required if:
519
- // 1. a `jwtDidProofRequest` is required and hasn't been provided
520
- // 2. OID4VP is enabled and no OID4VP result has been stored yet
521
- return didProofRequired || (step.openId && !exchange.variables
522
- .results[exchange.step]?.openId?.authorizationRequest);
523
- },
524
- issue({
525
- workflow, exchange, step, issueRequestsParams,
526
- verifiablePresentation
527
- }) {
528
- return defaultIssue({
529
- workflow, exchange, step, issueRequestsParams,
530
- verifiablePresentation, format
531
- });
532
- },
533
- isStepComplete({exchange}) {
534
- // FIXME: check step's current `openId` results to see which issue
535
- // requests have been processed; if any still exist, then the step is not
536
- // yet complete
537
- const {results} = exchange.variables;
538
- if(!results[exchange.step]?.openId) {
539
- // FIXME: implement
722
+ // get `supportedCredentialRequests` from step results
723
+ const stepResults = exchange.variables.results[exchange.step];
724
+ let supportedCredentialRequests = stepResults
725
+ ?.openId?.supportedCredentialRequests;
726
+
727
+ // if `supportedCredentialRequests` is not set, create it; this can only
728
+ // happen in the degenerate case that an older version of the software
729
+ // provided the access token to the client
730
+ if(!supportedCredentialRequests) {
731
+ supportedCredentialRequests = _createSupportedCredentialRequests({
732
+ workflow, exchange, step
733
+ });
734
+ exchange.variables.results[exchange.step] = {
735
+ ...exchange.variables.results[exchange.step],
736
+ openId: {
737
+ ...exchange.variables.results[exchange.step]?.openId,
738
+ supportedCredentialRequests
540
739
  }
541
- return true;
542
- }
543
- });
544
- const response = await exchangeProcessor.process();
545
- if(!response.verifiablePresentation) {
546
- return null;
740
+ };
741
+ // explicitly update exchange to ensure `supportedCredentialRequests`
742
+ // are committed
743
+ await exchangeProcessor.updateExchange({step});
547
744
  }
548
- return {response, format};
745
+
746
+ return supportedCredentialRequests;
549
747
  }
550
748
 
551
749
  async function _requestDidProof({res, exchangeRecord}) {
@@ -623,3 +821,10 @@ function _sendOID4Error({res, error, description, status = 400, ...rest}) {
623
821
  ...rest
624
822
  });
625
823
  }
824
+
825
+ function _validate(validator, data) {
826
+ const result = validator(data);
827
+ if(!result.valid) {
828
+ throw result.error;
829
+ }
830
+ }