@bedrock/vc-delivery 7.12.0 → 7.13.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.
@@ -479,7 +479,6 @@ export class ExchangeProcessor {
479
479
 
480
480
  async function _getStep({workflow, exchange}) {
481
481
  const currentStep = exchange.step;
482
-
483
482
  if(!currentStep) {
484
483
  // return default empty step and set dummy stepname for exchange
485
484
  exchange.step = 'initial';
@@ -562,6 +561,7 @@ function _isInitialStep({workflow, exchange}) {
562
561
 
563
562
  async function _updateExchange({workflow, exchange, meta, step}) {
564
563
  try {
564
+ exchange.referenceId = globalThis.crypto.randomUUID();
565
565
  exchange.sequence++;
566
566
  if(exchange.state === 'complete') {
567
567
  await exchanges.complete({workflowId: workflow.id, exchange});
package/lib/helpers.js CHANGED
@@ -115,7 +115,7 @@ export async function evaluateTemplate({
115
115
  export async function evaluateExchangeStep({
116
116
  workflow, exchange, stepName = exchange.step
117
117
  }) {
118
- let step = workflow.steps[stepName];
118
+ let step = workflow.steps[stepName] ?? {};
119
119
  if(step.stepTemplate) {
120
120
  step = await evaluateTemplate({
121
121
  workflow, exchange, typedTemplate: step.stepTemplate
@@ -349,12 +349,6 @@ export function stripStacktrace(error) {
349
349
 
350
350
  export async function validateStep({step} = {}) {
351
351
  // FIXME: use `ajv` and do JSON schema check
352
- if(Object.keys(step).length === 0) {
353
- throw new BedrockError('Empty exchange step detected.', {
354
- name: 'DataError',
355
- details: {httpStatusCode: 500, public: true}
356
- });
357
- }
358
352
  if(step.issueRequests !== undefined && !Array.isArray(step.issueRequests)) {
359
353
  throw new BedrockError(
360
354
  'Invalid "issueRequests" in step.', {
@@ -16,6 +16,10 @@ const ENCRYPTED_RESPONSE_MODES = new Set([
16
16
  'direct_post.jwt', 'dc_api.jwt', 'dc_api'
17
17
  ]);
18
18
  const OID4VP_JWT_TYP = 'oauth-authz-req+jwt';
19
+ const SUPPORTED_CLIENT_ID_SCHEMES = new Set([
20
+ 'redirect_uri', 'x509_san_dns', 'x509_hash', 'decentralized_identifier'
21
+ ]);
22
+
19
23
  const TEXT_ENCODER = new TextEncoder();
20
24
 
21
25
  export async function create({
@@ -30,11 +34,14 @@ export async function create({
30
34
 
31
35
  // get params from step OID4VP client profile to apply to the AR
32
36
  const {
33
- client_id, client_id_scheme,
37
+ client_id,
38
+ client_id_scheme,
34
39
  dcql_query,
40
+ expected_origins,
35
41
  nonce,
36
42
  presentation_definition,
37
- response_mode, response_uri
43
+ response_mode,
44
+ response_uri
38
45
  } = clientProfile;
39
46
  const clientBaseUrl = getClientBaseUrl({workflow, exchange, clientProfileId});
40
47
 
@@ -57,10 +64,13 @@ export async function create({
57
64
  `${clientBaseUrl}/authorization/response`;
58
65
 
59
66
  // client_id (defaults to `response_uri`)
60
- // FIXME: newer versions of OID4VP require a prefix of `redirect_uri:` for
61
- // the default case -- which is incompatible with some draft versions
62
- authorizationRequest.client_id = client_id ??
63
- authorizationRequest.response_uri;
67
+ if(client_id) {
68
+ authorizationRequest.client_id = client_id;
69
+ } else if(authorizationRequest.client_id_scheme === 'redirect_uri') {
70
+ // use prefix; this is compatible with both draft 18 and 1.0+
71
+ authorizationRequest.client_id =
72
+ `redirect_uri:${authorizationRequest.response_uri}`;
73
+ }
64
74
 
65
75
  // `x509_san_dns` requires the `direct_post.jwt` response mode when using
66
76
  // `direct_post`
@@ -71,6 +81,10 @@ export async function create({
71
81
  authorizationRequest.response_mode += '.jwt';
72
82
  }
73
83
 
84
+ // expected origins; safe to always include
85
+ authorizationRequest.expected_origins = expected_origins ??
86
+ [new URL(authorizationRequest.response_uri).origin];
87
+
74
88
  // nonce
75
89
  if(nonce) {
76
90
  authorizationRequest.nonce = nonce;
@@ -117,6 +131,16 @@ export async function encode({
117
131
  return jwt;
118
132
  }
119
133
 
134
+ export function removeClientIdPrefix({clientId} = {}) {
135
+ for(const idScheme of SUPPORTED_CLIENT_ID_SCHEMES) {
136
+ const prefix = `${idScheme}:`;
137
+ if(clientId.startsWith(prefix)) {
138
+ return clientId.slice(prefix.length);
139
+ }
140
+ }
141
+ return clientId;
142
+ }
143
+
120
144
  async function _createClientMetaData({
121
145
  authorizationRequest, clientProfile
122
146
  } = {}) {
@@ -247,9 +271,7 @@ async function _createJwt({workflow, clientProfile, authorizationRequest}) {
247
271
  const kid = keyDescription.id;
248
272
 
249
273
  // create the JWT payload and header to be signed
250
- const payload = {
251
- ...authorizationRequest
252
- };
274
+ const payload = {...authorizationRequest};
253
275
  const protectedHeader = {typ: OID4VP_JWT_TYP, alg: 'ES256', kid, x5c};
254
276
 
255
277
  // create the JWT
package/lib/oid4/http.js CHANGED
@@ -328,21 +328,37 @@ export async function createRoutes({
328
328
  }));
329
329
 
330
330
  // an OID4VP verifier endpoint
331
- // serves the authorization request, including presentation definition
331
+ // serves the authorization request
332
332
  // associated with the current step in the exchange
333
+ app.options(routes.authorizationRequest, cors());
333
334
  app.get(
334
335
  routes.authorizationRequest,
335
336
  cors(),
336
337
  getConfigMiddleware,
337
338
  getExchange,
338
339
  asyncHandler(_handleOid4vpAuthzRequest));
340
+ // same as above but allows wallet to submit metadata
341
+ app.post(
342
+ routes.authorizationRequest,
343
+ cors(),
344
+ getConfigMiddleware,
345
+ getExchange,
346
+ asyncHandler(_handleOid4vpAuthzRequest));
339
347
  // same as above but handling is based on specific client profile
348
+ app.options(routes.profiledAuthorizationRequest, cors());
340
349
  app.get(
341
350
  routes.profiledAuthorizationRequest,
342
351
  cors(),
343
352
  getConfigMiddleware,
344
353
  getExchange,
345
354
  asyncHandler(_handleOid4vpAuthzRequest));
355
+ // same as above but allows wallet to submit metadata
356
+ app.post(
357
+ routes.profiledAuthorizationRequest,
358
+ cors(),
359
+ getConfigMiddleware,
360
+ getExchange,
361
+ asyncHandler(_handleOid4vpAuthzRequest));
346
362
 
347
363
  // an OID4VP verifier endpoint
348
364
  // receives an authorization response with vp_token
@@ -375,13 +391,16 @@ async function _handleOid4vpAuthzRequest(req, res) {
375
391
  const {clientProfileId} = req.params;
376
392
  let result;
377
393
  try {
394
+ // FIXME: consider passing `body` to modulate authz request that is
395
+ // returned in response; presently any wallet metadata is ignored
378
396
  const {
379
397
  authorizationRequest,
380
398
  clientProfile
381
399
  } = await oid4vp.getAuthorizationRequest({req, clientProfileId});
382
400
  const {config: workflow} = req.serviceObject;
383
401
  result = await oid4vp.encodeAuthorizationRequest({
384
- workflow, clientProfile, authorizationRequest
402
+ workflow, clientProfile, authorizationRequest,
403
+ requestMethod: req.method.toLowerCase()
385
404
  });
386
405
  res.set('content-type', 'application/oauth-authz-req+jwt');
387
406
  } catch(error) {
@@ -2,13 +2,16 @@
2
2
  * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
+ import {
6
+ create as createAuthorizationRequest,
7
+ removeClientIdPrefix
8
+ } from './authorizationRequest.js';
5
9
  import {
6
10
  evaluateExchangeStep,
7
11
  resolveVariableName,
8
12
  setVariable
9
13
  } from '../helpers.js';
10
14
  import {getClientBaseUrl, getClientProfile} from './clientProfiles.js';
11
- import {create as createAuthorizationRequest} from './authorizationRequest.js';
12
15
  import {verify as defaultVerify} from '../verify.js';
13
16
  import {ExchangeProcessor} from '../ExchangeProcessor.js';
14
17
  import {oid4vp} from '@digitalbazaar/oid4-client';
@@ -92,27 +95,26 @@ export async function getOID4VPProtocols({workflow, exchange, step}) {
92
95
  // profile name
93
96
  const protocols = {};
94
97
  for(const [clientProfileId, clientProfile] of clientProfiles) {
95
- // currently, only changing `name` and `scheme` are supported
96
- const {
97
- protocolUrlParameters: {
98
- name = 'OID4VP',
99
- scheme = 'openid4vp'
100
- } = {}
101
- } = clientProfile;
98
+ // get supported protocol URL parameters
99
+ const {name, scheme, version} = _getProtocolUrlParameters({clientProfile});
102
100
 
103
101
  // generate default OID4VP protocol URL
104
102
  const clientBaseUrl = getClientBaseUrl({
105
103
  workflow, exchange, clientProfileId
106
104
  });
107
105
  const {
108
- authorizationRequest: {client_id}
106
+ authorizationRequest
109
107
  } = await _getOrCreateStepAuthorizationRequest({
110
108
  workflow, exchange, clientProfileId, clientProfile, step
111
109
  });
110
+ const {client_id, request_uri_method} = authorizationRequest;
112
111
  const searchParams = new URLSearchParams({
113
112
  client_id,
114
113
  request_uri: `${clientBaseUrl}/authorization/request`
115
114
  });
115
+ if(request_uri_method && version !== 'OID4VP-draft18') {
116
+ searchParams.set('request_uri_method', request_uri_method);
117
+ }
116
118
  protocols[name] = `${scheme}://?${searchParams}`;
117
119
  }
118
120
  return protocols;
@@ -191,8 +193,6 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
191
193
  verifyPresentationOptions.challenge = authorizationRequest.nonce;
192
194
  verifyPresentationOptions.domain = authorizationRequest.response_uri;
193
195
 
194
- // FIXME: OID4VP 1.0+ does not have a presentation submission
195
- // handle mDL submission
196
196
  const {envelope} = parseResponseResult;
197
197
  if(envelope?.mediaType === 'application/mdl-vp-token') {
198
198
  // generate `handover` for mDL verification
@@ -205,8 +205,7 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
205
205
 
206
206
  // `direct_post.jwt` => ISO18013-7 Annex B
207
207
  // FIXME: same response mode is also used for OID4VP 1.0 with
208
- // `OpenID4VPHandover` where `presentationSubmission` will be absent;
209
- // this is not yet supported
208
+ // `OpenID4VPHandover` for non-Annex-B; this is not yet supported
210
209
  // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-invocation-via-redirects
211
210
  const {responseMode} = parseResponseResult;
212
211
  if(responseMode === 'direct_post.jwt') {
@@ -270,8 +269,6 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
270
269
  presentationSubmission
271
270
  }
272
271
  };
273
- // FIXME: do w/o `parseResponseResult.envelope` to eliminate envelope
274
- // parsing (let verifier do it)
275
272
  if(parseResponseResult.envelope) {
276
273
  // include enveloped VP in step result
277
274
  exchange.variables.results[exchange.step]
@@ -296,6 +293,21 @@ export async function supportsOID4VP({workflow, exchange, step}) {
296
293
  return step.openId !== undefined;
297
294
  }
298
295
 
296
+ function _getProtocolUrlParameters({clientProfile}) {
297
+ const protocolUrlParameters = {
298
+ name: 'OID4VP',
299
+ scheme: 'openid4vp',
300
+ version: undefined,
301
+ ...clientProfile.protocolUrlParameters
302
+ };
303
+ if(protocolUrlParameters.version === undefined) {
304
+ protocolUrlParameters.version =
305
+ protocolUrlParameters.scheme === 'mdoc-openid4vp' ?
306
+ 'OID4VP-draft18' : 'OID4VP-1.0';
307
+ }
308
+ return protocolUrlParameters;
309
+ }
310
+
299
311
  async function _getOrCreateStepAuthorizationRequest({
300
312
  workflow, exchange, clientProfileId, clientProfile, step
301
313
  }) {
@@ -304,6 +316,9 @@ async function _getOrCreateStepAuthorizationRequest({
304
316
  // get authorization request
305
317
  authorizationRequest = clientProfile.authorizationRequest;
306
318
  if(authorizationRequest) {
319
+ authorizationRequest = _normalizeAuthorizationRequest({
320
+ authorizationRequest, exchange, clientProfile
321
+ });
307
322
  return {authorizationRequest, exchangeChanged: false};
308
323
  }
309
324
 
@@ -328,6 +343,9 @@ async function _getOrCreateStepAuthorizationRequest({
328
343
  variables: exchange.variables, name: authzReqVarName
329
344
  });
330
345
  if(authorizationRequest) {
346
+ authorizationRequest = _normalizeAuthorizationRequest({
347
+ authorizationRequest, exchange, clientProfile
348
+ });
331
349
  return {authorizationRequest, exchangeChanged: false};
332
350
  }
333
351
 
@@ -338,7 +356,9 @@ async function _getOrCreateStepAuthorizationRequest({
338
356
  clientProfile, clientProfileId,
339
357
  verifiablePresentationRequest
340
358
  });
341
- authorizationRequest = result.authorizationRequest;
359
+ authorizationRequest = _normalizeAuthorizationRequest({
360
+ authorizationRequest: result.authorizationRequest, exchange, clientProfile
361
+ });
342
362
 
343
363
  // merge any newly created exchange secrets
344
364
  exchange.secrets = {
@@ -365,6 +385,55 @@ async function _getOrCreateStepAuthorizationRequest({
365
385
  return {authorizationRequest, exchangeChanged: true};
366
386
  }
367
387
 
388
+ function _normalizeAuthorizationRequest({
389
+ authorizationRequest, exchange, clientProfile
390
+ }) {
391
+ const {
392
+ client_id, client_id_scheme, request_uri_method, state
393
+ } = authorizationRequest;
394
+ authorizationRequest = {...authorizationRequest};
395
+
396
+ // get any explicit version to be used with client profile
397
+ const {version} = _getProtocolUrlParameters({clientProfile});
398
+
399
+ // if `version` is OID4VP draft 18, remove any `client_id_scheme` prefix
400
+ // from the `client_id`; otherwise add it
401
+ if(version === 'OID4VP-draft18') {
402
+ authorizationRequest.client_id = removeClientIdPrefix({
403
+ clientId: client_id
404
+ });
405
+ return authorizationRequest;
406
+ }
407
+
408
+ // version is OID4VP 1.0+ or that + compatibility w/Draft18...
409
+ if(client_id_scheme && client_id && !client_id.startsWith(client_id_scheme)) {
410
+ // note: for `redirect_uri` `client_id_scheme`, it is ok to always include
411
+ // `redirect_uri:` prefix in the client ID, even for versions that support
412
+ // Draft 18 compatibility along with other versions, as the client ID
413
+ // should be treated as opaque when using `response_mode=direct*` and
414
+ // omitting the `redirect_uri` parameter; which is the only supported
415
+ // configuration in this implementation for Draft 18 clients
416
+ authorizationRequest.client_id = `${client_id_scheme}:${client_id}`;
417
+ }
418
+
419
+ // OID4VP 1.0+ requires `state` to be included for authz requests that do
420
+ // not require "holder binding", but always including it does not cause any
421
+ // known issues, so just include `state` using `referenceId` (if set) or
422
+ // `localExchangeId`
423
+ if(!state && oid4vp.authzRequest.usesClientIdScheme({
424
+ authorizationRequest, scheme: 'redirect_uri'
425
+ })) {
426
+ authorizationRequest.state = exchange.referenceId ?? exchange.id;
427
+ }
428
+
429
+ // default to `request_uri_method=post` in OID4VP 1.0+
430
+ if(!request_uri_method) {
431
+ authorizationRequest.request_uri_method = 'post';
432
+ }
433
+
434
+ return authorizationRequest;
435
+ }
436
+
368
437
  function _throwUnsupportedProtocol() {
369
438
  throw new BedrockError('OID4VP is not supported by this exchange.', {
370
439
  name: 'NotSupportedError',
@@ -568,6 +568,7 @@ function _buildUpdate({exchange}) {
568
568
  const update = {
569
569
  $inc: {'exchange.sequence': 1},
570
570
  $set: {
571
+ 'exchange.referenceId': exchange.referenceId ?? exchange.id,
571
572
  'exchange.state': exchange.state,
572
573
  'exchange.secrets': exchange.secrets,
573
574
  'exchange.variables': exchange.variables,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "7.12.0",
3
+ "version": "7.13.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -40,7 +40,7 @@
40
40
  "@digitalbazaar/ed25519-signature-2020": "^5.4.0",
41
41
  "@digitalbazaar/ezcap": "^4.1.0",
42
42
  "@digitalbazaar/http-client": "^4.2.0",
43
- "@digitalbazaar/oid4-client": "^5.8.0",
43
+ "@digitalbazaar/oid4-client": "^5.9.0",
44
44
  "@digitalbazaar/vc": "^7.2.0",
45
45
  "@digitalbazaar/webkms-client": "^14.2.0",
46
46
  "assert-plus": "^1.0.0",
@@ -472,6 +472,10 @@ const oid4vpClientProfile = {
472
472
  presentation_definition: {type: 'object'},
473
473
  response_mode: {type: 'string'},
474
474
  response_uri: {type: 'string'},
475
+ response_uri_method: {
476
+ type: 'string',
477
+ enum: ['get', 'post']
478
+ },
475
479
  // optional parameters for signing authorization requests
476
480
  authorizationRequestSigningParameters: {
477
481
  type: 'object',
@@ -498,6 +502,10 @@ const oid4vpClientProfile = {
498
502
  },
499
503
  scheme: {
500
504
  type: 'string'
505
+ },
506
+ version: {
507
+ type: 'string',
508
+ enum: ['OID4VP-draft18', 'OID4VP-1.0']
501
509
  }
502
510
  }
503
511
  },