@bedrock/vc-delivery 7.3.0 → 7.5.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.
package/lib/oid4/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as oid4vci from './oid4vci.js';
5
5
  import * as oid4vp from './oid4vp.js';
@@ -13,13 +13,8 @@ import {asyncHandler} from '@bedrock/express';
13
13
  import bodyParser from 'body-parser';
14
14
  import cors from 'cors';
15
15
  import {logger} from '../logger.js';
16
- import {UnsecuredJWT} from 'jose';
17
16
  import {createValidateMiddleware as validate} from '@bedrock/validation';
18
17
 
19
- // re-export support detection helpers
20
- export {supportsOID4VCI} from './oid4vci.js';
21
- export {supportsOID4VP} from './oid4vp.js';
22
-
23
18
  /* NOTE: Parts of the OID4VCI design imply tight integration between the
24
19
  authorization server and the credential issuance / delivery server. This
25
20
  file provides the routes for both and treats them as integrated; supporting
@@ -51,6 +46,7 @@ export async function createRoutes({
51
46
  app, exchangeRoute, getConfigMiddleware, getExchange
52
47
  } = {}) {
53
48
  const openIdRoute = `${exchangeRoute}/openid`;
49
+ const oid4vpClientUrl = `${openIdRoute}/clients/:clientProfileId`;
54
50
  const routes = {
55
51
  // OID4VCI routes
56
52
  asMetadata1: `/.well-known/oauth-authorization-server${exchangeRoute}`,
@@ -63,9 +59,13 @@ export async function createRoutes({
63
59
  nonce: `${openIdRoute}/nonce`,
64
60
  token: `${openIdRoute}/token`,
65
61
  jwks: `${openIdRoute}/jwks`,
66
- // OID4VP routes
62
+ // OID4VP routes:
63
+ // legacy routes do not include a client profile ID
67
64
  authorizationRequest: `${openIdRoute}/client/authorization/request`,
68
- authorizationResponse: `${openIdRoute}/client/authorization/response`
65
+ authorizationResponse: `${openIdRoute}/client/authorization/response`,
66
+ // modern routes include a "clientProfileId" in the URL
67
+ profiledAuthorizationRequest: `${oid4vpClientUrl}/authorization/request`,
68
+ profiledAuthorizationResponse: `${oid4vpClientUrl}/authorization/response`
69
69
  };
70
70
 
71
71
  // urlencoded body parser
@@ -216,23 +216,11 @@ export async function createRoutes({
216
216
  return;
217
217
  }
218
218
 
219
- // send VC(s)
219
+ // format VC(s)
220
220
  const {
221
- response: {verifiablePresentation: {verifiableCredential}},
222
- format
221
+ response: {verifiablePresentation}, format
223
222
  } = result;
224
- // FIXME: "format" doesn't seem to be in the spec anymore (draft 14+)...
225
- const credentials = verifiableCredential.map(vc => {
226
- // parse any enveloped VC
227
- let credential;
228
- if(vc.type === 'EnvelopedVerifiableCredential' &&
229
- vc.id?.startsWith('data:application/jwt,')) {
230
- credential = vc.id.slice('data:application/jwt,'.length);
231
- } else {
232
- credential = vc;
233
- }
234
- return credential;
235
- });
223
+ const credentials = _normalizeCredentials({verifiablePresentation});
236
224
 
237
225
  /* Note: The `/credential` route only supports sending VCs of the same
238
226
  type, but there can be more than one of them. The above `isBatchRequest`
@@ -326,23 +314,13 @@ export async function createRoutes({
326
314
  return;
327
315
  }
328
316
 
329
- // send VCs
317
+ // format VC(s)
330
318
  const {
331
- response: {verifiablePresentation: {verifiableCredential}},
319
+ response: {verifiablePresentation},
332
320
  format
333
321
  } = result;
334
- // FIXME: "format" doesn't seem to be in the spec anymore (draft 14+)...
335
- result = verifiableCredential.map(vc => {
336
- // parse any enveloped VC
337
- let credential;
338
- if(vc.type === 'EnvelopedVerifiableCredential' &&
339
- vc.id?.startsWith('data:application/jwt,')) {
340
- credential = vc.id.slice('data:application/jwt,'.length);
341
- } else {
342
- credential = vc;
343
- }
344
- return {format, credential};
345
- });
322
+ result = _normalizeCredentials({verifiablePresentation})
323
+ .map(credential => ({format, credential}));
346
324
  } catch(error) {
347
325
  return _sendOID4Error({res, error});
348
326
  }
@@ -357,20 +335,14 @@ export async function createRoutes({
357
335
  cors(),
358
336
  getConfigMiddleware,
359
337
  getExchange,
360
- asyncHandler(async (req, res) => {
361
- let result;
362
- try {
363
- const {
364
- authorizationRequest
365
- } = await oid4vp.getAuthorizationRequest({req});
366
- // construct and send authz request as unsecured JWT
367
- result = new UnsecuredJWT(authorizationRequest).encode();
368
- res.set('content-type', 'application/oauth-authz-req+jwt');
369
- } catch(error) {
370
- return _sendOID4Error({res, error});
371
- }
372
- res.send(result);
373
- }));
338
+ asyncHandler(_handleOid4vpAuthzRequest));
339
+ // same as above but handling is based on specific client profile
340
+ app.get(
341
+ routes.profiledAuthorizationRequest,
342
+ cors(),
343
+ getConfigMiddleware,
344
+ getExchange,
345
+ asyncHandler(_handleOid4vpAuthzRequest));
374
346
 
375
347
  // an OID4VP verifier endpoint
376
348
  // receives an authorization response with vp_token
@@ -382,15 +354,64 @@ export async function createRoutes({
382
354
  validate({bodySchema: openIdAuthorizationResponseBody()}),
383
355
  getConfigMiddleware,
384
356
  getExchange,
385
- asyncHandler(async (req, res) => {
386
- let result;
387
- try {
388
- result = await oid4vp.processAuthorizationResponse({req});
389
- } catch(error) {
390
- return _sendOID4Error({res, error});
391
- }
392
- res.json(result);
393
- }));
357
+ asyncHandler(_handleOid4vpAuthzResponse));
358
+ // same as above but handling is based on specific client profile
359
+ app.options(routes.profiledAuthorizationResponse, cors());
360
+ app.post(
361
+ routes.profiledAuthorizationResponse,
362
+ cors(),
363
+ urlencodedLarge,
364
+ validate({bodySchema: openIdAuthorizationResponseBody()}),
365
+ getConfigMiddleware,
366
+ getExchange,
367
+ asyncHandler(_handleOid4vpAuthzResponse));
368
+ }
369
+
370
+ function _camelToSnakeCase(s) {
371
+ return s.replace(/[A-Z]/g, (c, i) => (i === 0 ? '' : '_') + c.toLowerCase());
372
+ }
373
+
374
+ async function _handleOid4vpAuthzRequest(req, res) {
375
+ const {clientProfileId} = req.params;
376
+ let result;
377
+ try {
378
+ const {
379
+ authorizationRequest,
380
+ clientProfile
381
+ } = await oid4vp.getAuthorizationRequest({req, clientProfileId});
382
+ const {config: workflow} = req.serviceObject;
383
+ result = await oid4vp.encodeAuthorizationRequest({
384
+ workflow, clientProfile, authorizationRequest
385
+ });
386
+ res.set('content-type', 'application/oauth-authz-req+jwt');
387
+ } catch(error) {
388
+ return _sendOID4Error({res, error});
389
+ }
390
+ res.send(result);
391
+ }
392
+
393
+ async function _handleOid4vpAuthzResponse(req, res) {
394
+ const {clientProfileId} = req.params;
395
+ let result;
396
+ try {
397
+ result = await oid4vp.processAuthorizationResponse({req, clientProfileId});
398
+ } catch(error) {
399
+ return _sendOID4Error({res, error});
400
+ }
401
+ res.json(result);
402
+ }
403
+
404
+ function _normalizeCredentials({verifiablePresentation}) {
405
+ // use raw format for each credential
406
+ const {verifiableCredential} = verifiablePresentation;
407
+ return verifiableCredential.map(vc => {
408
+ // parse any enveloped VC into its non-VC format
409
+ if(vc.type === 'EnvelopedVerifiableCredential' &&
410
+ vc.id?.startsWith('data:application/jwt,')) {
411
+ return vc.id.slice('data:application/jwt,'.length);
412
+ }
413
+ return vc;
414
+ });
394
415
  }
395
416
 
396
417
  function _sendOID4Error({res, error}) {
@@ -412,7 +433,3 @@ function _sendOID4Error({res, error}) {
412
433
  }
413
434
  res.status(status).json(oid4Error);
414
435
  }
415
-
416
- function _camelToSnakeCase(s) {
417
- return s.replace(/[A-Z]/g, (c, i) => (i === 0 ? '' : '_') + c.toLowerCase());
418
- }
@@ -5,7 +5,7 @@ import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from '../exchanges.js';
6
6
  import {
7
7
  deepEqual, emitExchangeUpdated,
8
- evaluateTemplate, getWorkflowIssuerInstances, validateStep
8
+ evaluateExchangeStep, getWorkflowIssuerInstances
9
9
  } from '../helpers.js';
10
10
  import {importJWK, SignJWT} from 'jose';
11
11
  import {checkAccessToken} from '@bedrock/oauth2-verifier';
@@ -84,6 +84,36 @@ export async function getJwks({req}) {
84
84
  return [exchange.openId.oauth2.keyPair.publicKeyJwk];
85
85
  }
86
86
 
87
+ export function getOID4VCIProtocols({workflow, exchange}) {
88
+ if(!supportsOID4VCI({exchange})) {
89
+ return {};
90
+ }
91
+ // OID4VCI supported; add credential offer URL
92
+ const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
93
+ const searchParams = new URLSearchParams();
94
+ const uri = `${exchangeId}/openid/credential-offer`;
95
+ searchParams.set('credential_offer_uri', uri);
96
+ return {OID4VCI: `openid-credential-offer://?${searchParams}`};
97
+ }
98
+
99
+ export async function initExchange({workflow, exchange} = {}) {
100
+ if(!supportsOID4VCI({exchange})) {
101
+ return;
102
+ }
103
+
104
+ // either issuer instances or a single issuer zcap be given if
105
+ // any expected credential requests are given
106
+ const {expectedCredentialRequests} = exchange.openId;
107
+ if(expectedCredentialRequests &&
108
+ !(workflow.issuerInstances || workflow.zcaps.issue)) {
109
+ throw new BedrockError(
110
+ 'Credential requests are not supported by this workflow.', {
111
+ name: 'DataError',
112
+ details: {httpStatusCode: 400, public: true}
113
+ });
114
+ }
115
+ }
116
+
87
117
  export async function processAccessTokenRequest({req}) {
88
118
  const exchangeRecord = await req.getExchange();
89
119
  const {exchange} = exchangeRecord;
@@ -432,14 +462,7 @@ async function _processExchange({
432
462
  // process exchange step if present
433
463
  const currentStep = exchange.step;
434
464
  if(currentStep) {
435
- step = workflow.steps[exchange.step];
436
- if(step.stepTemplate) {
437
- // generate step from the template; assume the template type is
438
- // `jsonata` per the JSON schema
439
- step = await evaluateTemplate(
440
- {workflow, exchange, typedTemplate: step.stepTemplate});
441
- }
442
- await validateStep({step});
465
+ step = await evaluateExchangeStep({workflow, exchange});
443
466
  const {jwtDidProofRequest} = step;
444
467
 
445
468
  // check to see if step supports OID4VP during OID4VCI
@@ -449,9 +472,14 @@ async function _processExchange({
449
472
  if(!results?.[exchange.step]?.openId?.presentationSubmission) {
450
473
  // FIXME: optimize away double step-template processing that
451
474
  // currently occurs when calling `_getAuthorizationRequest`
475
+ // note: only the "default" `clientProfileId` is supported at this
476
+ // time because there isn't presently a defined way to specify
477
+ // alternatives
478
+ const clientProfileId = step.openId.clientProfiles ?
479
+ 'default' : undefined;
452
480
  const {
453
481
  authorizationRequest
454
- } = await getAuthorizationRequest({req});
482
+ } = await getAuthorizationRequest({req, clientProfileId});
455
483
  return _requestOID4VP({authorizationRequest, res});
456
484
  }
457
485
  // otherwise drop down below to complete exchange...