@bedrock/vc-delivery 7.11.2 → 7.12.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,22 +2,17 @@
2
2
  * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
- import * as exchanges from '../storage/exchanges.js';
6
5
  import {
7
- buildPresentationFromResults,
8
- buildVerifyPresentationResults,
9
- emitExchangeUpdated,
10
6
  evaluateExchangeStep,
11
7
  resolveVariableName,
12
8
  setVariable
13
9
  } from '../helpers.js';
14
10
  import {getClientBaseUrl, getClientProfile} from './clientProfiles.js';
15
- import {compile} from '@bedrock/validation';
16
11
  import {create as createAuthorizationRequest} from './authorizationRequest.js';
17
- import {logger} from '../logger.js';
12
+ import {verify as defaultVerify} from '../verify.js';
13
+ import {ExchangeProcessor} from '../ExchangeProcessor.js';
18
14
  import {oid4vp} from '@digitalbazaar/oid4-client';
19
15
  import {parse as parseAuthorizationResponse} from './authorizationResponse.js';
20
- import {verify} from '../verify.js';
21
16
 
22
17
  const {util: {BedrockError}} = bedrock;
23
18
 
@@ -26,73 +21,59 @@ export {encode as encodeAuthorizationRequest} from './authorizationRequest.js';
26
21
  export async function getAuthorizationRequest({req, clientProfileId}) {
27
22
  const {config: workflow} = req.serviceObject;
28
23
  const exchangeRecord = await req.getExchange();
29
- let {exchange} = exchangeRecord;
30
- let step;
31
- let clientProfile;
32
-
33
- while(true) {
34
- // exchange step required for OID4VP
35
- const currentStep = exchange.step;
36
- if(!currentStep) {
37
- _throwUnsupportedProtocol();
38
- }
39
-
40
- step = await evaluateExchangeStep({workflow, exchange});
41
24
 
42
- // step must have `openId` to perform OID4VP
43
- if(!step.openId) {
44
- _throwUnsupportedProtocol();
45
- }
25
+ // process exchange and capture values to return
26
+ const result = {};
27
+ const exchangeProcessor = new ExchangeProcessor({
28
+ workflow, exchangeRecord,
29
+ async prepareStep({exchange, step}) {
30
+ const {
31
+ clientProfile, authorizationRequest
32
+ } = await getStepAuthorizationRequest({
33
+ workflow, exchange, step, clientProfileId
34
+ });
46
35
 
47
- // deny retrieval of authorization request if an authorization response
48
- // has already been accepted for this step
49
- if(exchange.variables?.results?.[exchange.step]) {
50
- throw new BedrockError(
51
- 'This OID4VP exchange is already in progress.', {
52
- name: 'NotAllowedError',
53
- details: {httpStatusCode: 403, public: true}
54
- });
36
+ // save values to return
37
+ result.clientProfile = clientProfile;
38
+ result.authorizationRequest = authorizationRequest;
39
+ result.exchange = exchange;
40
+ result.step = step;
41
+ },
42
+ inputRequired() {
43
+ // input always required (authz response required)
44
+ return true;
55
45
  }
46
+ });
47
+ await exchangeProcessor.process();
56
48
 
57
- // get OID4VP client profile
58
- clientProfile = getClientProfile({step, clientProfileId});
49
+ return result;
50
+ }
59
51
 
60
- const {
61
- authorizationRequest,
62
- exchangeChanged
63
- } = await _getStepAuthorizationRequest({
64
- workflow, exchange, clientProfileId, clientProfile, step
52
+ export async function getStepAuthorizationRequest({
53
+ workflow, exchange, step, clientProfileId
54
+ }) {
55
+ // step must have `openId` to perform OID4VP
56
+ if(!step.openId) {
57
+ _throwUnsupportedProtocol();
58
+ }
59
+ // deny retrieval of authorization request if an authorization response
60
+ // has already been accepted for this step
61
+ if(exchange.variables.results[exchange.step]?.verifiablePresentation) {
62
+ throw new BedrockError('This OID4VP exchange is already in progress.', {
63
+ name: 'NotAllowedError',
64
+ details: {httpStatusCode: 403, public: true}
65
65
  });
66
+ }
66
67
 
67
- const prevState = exchange.state;
68
- let updateExchange = exchangeChanged;
69
- if(exchange.state === 'pending') {
70
- exchange.state = 'active';
71
- updateExchange = true;
72
- }
68
+ // get OID4VP client profile
69
+ const clientProfile = getClientProfile({step, clientProfileId});
73
70
 
74
- if(updateExchange) {
75
- try {
76
- exchange.sequence++;
77
- await exchanges.update({workflowId: workflow.id, exchange});
78
- await emitExchangeUpdated({workflow, exchange, step});
79
- } catch(e) {
80
- exchange.state = prevState;
81
- exchange.sequence--;
82
- if(e.name !== 'InvalidStateError') {
83
- // unrecoverable error
84
- throw e;
85
- }
86
- // get exchange and loop to try again on `InvalidStateError`
87
- const record = await exchanges.get(
88
- {workflowId: workflow.id, id: exchange.id});
89
- ({exchange} = record);
90
- continue;
91
- }
92
- }
71
+ // generate authorization request
72
+ const {authorizationRequest} = await _getOrCreateStepAuthorizationRequest({
73
+ workflow, exchange, clientProfileId, clientProfile, step
74
+ });
93
75
 
94
- return {authorizationRequest, exchange, step, clientProfile};
95
- }
76
+ return {clientProfile, authorizationRequest};
96
77
  }
97
78
 
98
79
  export async function getOID4VPProtocols({workflow, exchange, step}) {
@@ -125,7 +106,7 @@ export async function getOID4VPProtocols({workflow, exchange, step}) {
125
106
  });
126
107
  const {
127
108
  authorizationRequest: {client_id}
128
- } = await _getStepAuthorizationRequest({
109
+ } = await _getOrCreateStepAuthorizationRequest({
129
110
  workflow, exchange, clientProfileId, clientProfile, step
130
111
  });
131
112
  const searchParams = new URLSearchParams({
@@ -148,7 +129,7 @@ export async function initExchange({workflow, exchange, initialStep} = {}) {
148
129
  const clientProfiles = openId.clientProfiles ?
149
130
  Object.entries(openId.clientProfiles) : [[undefined, openId]];
150
131
  await Promise.all(clientProfiles.map(([clientProfileId, clientProfile]) =>
151
- _getStepAuthorizationRequest({
132
+ _getOrCreateStepAuthorizationRequest({
152
133
  workflow, exchange, clientProfileId, clientProfile, step: initialStep
153
134
  })));
154
135
  }
@@ -156,184 +137,153 @@ export async function initExchange({workflow, exchange, initialStep} = {}) {
156
137
  export async function processAuthorizationResponse({req, clientProfileId}) {
157
138
  const {config: workflow} = req.serviceObject;
158
139
  const exchangeRecord = await req.getExchange();
159
- let {exchange} = exchangeRecord;
160
-
161
- // ensure authz response can be parsed
162
- const {
163
- presentation, envelope, presentationSubmission,
164
- responseMode, protectedHeader
165
- } = await parseAuthorizationResponse({req, exchange, clientProfileId});
166
-
167
- let {meta: {updated: lastUpdated}} = exchangeRecord;
168
- let step;
169
- try {
170
- // get authorization request and updated exchange associated with exchange
171
- const arResult = await getAuthorizationRequest({req, clientProfileId});
172
- const {authorizationRequest} = arResult;
173
- ({exchange, step} = arResult);
174
-
175
- // ensure a result for this step has not already been stored
176
- const currentStep = exchange.step;
177
- if(exchange.variables?.results?.[currentStep]) {
178
- throw new BedrockError(
179
- 'This OID4VP exchange is already in progress.', {
180
- name: 'NotAllowedError',
181
- details: {httpStatusCode: 403, public: true}
182
- });
183
- }
184
140
 
185
- // ensure response mode matches
186
- if(responseMode !== authorizationRequest.response_mode) {
187
- throw new BedrockError(
188
- `The used response mode ("${responseMode}") does not match the ` +
189
- `expected response mode ("${authorizationRequest.response_mode}".`, {
190
- name: 'ConstraintError',
191
- details: {httpStatusCode: 400, public: true}
192
- });
193
- }
141
+ // process exchange and produce result
142
+ let parseResponseResult;
143
+ const result = {};
144
+ const exchangeProcessor = new ExchangeProcessor({
145
+ workflow, exchangeRecord,
146
+ async prepareStep({exchange, step}) {
147
+ const {authorizationRequest} = await getStepAuthorizationRequest({
148
+ workflow, exchange, step, clientProfileId
149
+ });
150
+ result.authorizationRequest = authorizationRequest;
151
+
152
+ // ensure authz response can be parsed
153
+ parseResponseResult = await parseAuthorizationResponse({
154
+ req, exchange: exchangeRecord.exchange, clientProfileId,
155
+ authorizationRequest
156
+ });
194
157
 
195
- // FIXME: check the VP against the presentation submission if requested
196
- // FIXME: check the VP against "trustedIssuer" in VPR, if provided
197
- const {presentationSchema} = step;
198
- if(presentationSchema) {
199
- // if the VP is enveloped, validate the contents of the envelope
200
- const toValidate = envelope ? envelope.contents : presentation;
201
-
202
- // validate the received VP / envelope contents
203
- const {jsonSchema: schema} = presentationSchema;
204
- const validate = compile({schema});
205
- const {valid, error} = validate(toValidate);
206
- if(!valid) {
207
- throw error;
158
+ // only mark exchange complete if there is nothing to be issued; this
159
+ // handles same-step OID4VCI+OID4VP case
160
+ const {credentialTemplates = []} = workflow;
161
+ if(credentialTemplates.length === 0) {
162
+ exchange.state = 'complete';
208
163
  }
209
- }
210
164
 
211
- // verify the received VP
212
- const {verifiablePresentationRequest} = await oid4vp.toVpr(
213
- {authorizationRequest});
214
- const {
215
- allowUnprotectedPresentation = false,
216
- verifyPresentationResultSchema
217
- } = step;
218
- const verifyPresentationOptions = {
219
- ...step.verifyPresentationOptions
220
- };
221
-
222
- // if `direct_post.jwt` used w/ mDL presentation, include `mDL` options
223
- if(responseMode === 'direct_post.jwt' &&
224
- oid4vp.authzResponse.submitsFormat({
225
- presentationSubmission, format: 'mso_mdoc'
226
- })) {
227
- verifyPresentationOptions.challenge = authorizationRequest.nonce;
228
- verifyPresentationOptions.domain = authorizationRequest.response_uri;
229
- verifyPresentationOptions.mdl = {
230
- ...verifyPresentationOptions.mdl,
231
- // note: in session transcript:
232
- // `domain` option above will be used for `responseUri`
233
- // `challenge` option above will be used for `verifierGeneratedNonce`
234
- // so do not send here to avoid redundancy
235
- sessionTranscript: {
236
- // per ISO 18013-7 the `mdocGeneratedNonce` is base64url-encoded
237
- // and put into the `apu` protected header parameter -- and the
238
- // VC API `mdl.sessionTranscript` option expects the
239
- // `mdocGeneratedNonce` to be base64url-encoded, so we can pass
240
- // it straight through
241
- mdocGeneratedNonce: protectedHeader.apu,
242
- clientId: authorizationRequest.client_id
243
- }
244
- };
245
- }
165
+ // include `redirect_uri` if specified in step
166
+ const {redirect_uri} = step.openId;
167
+ if(redirect_uri) {
168
+ result.redirect_uri = redirect_uri;
169
+ }
246
170
 
247
- const verifyResult = await verify({
248
- workflow,
171
+ const {presentation: receivedPresentation} = parseResponseResult;
172
+ return {receivedPresentation};
173
+ },
174
+ inputRequired({step}) {
175
+ // indicate input always required to avoid automatically advancing
176
+ // to the next step, but clear `step.verifiablePresentationRequest`
177
+ // to avoid overwriting previous value
178
+ delete step.verifiablePresentationRequest;
179
+ return true;
180
+ },
181
+ async verify({
182
+ workflow, exchange,
249
183
  verifyPresentationOptions,
250
184
  verifyPresentationResultSchema,
251
- verifiablePresentationRequest,
252
185
  presentation,
253
186
  allowUnprotectedPresentation,
254
- expectedChallenge: authorizationRequest.nonce
255
- });
256
- const {verificationMethod} = verifyResult;
187
+ expectedChallenge,
188
+ expectedDomain
189
+ }) {
190
+ const {authorizationRequest} = result;
191
+ verifyPresentationOptions.challenge = authorizationRequest.nonce;
192
+ verifyPresentationOptions.domain = authorizationRequest.response_uri;
257
193
 
258
- // store VP results in variables associated with current step
259
- if(!exchange.variables.results) {
260
- exchange.variables.results = {};
261
- }
262
- const stepResult = {
263
- // common use case of DID Authentication; provide `did` for ease
264
- // of use in template
265
- did: verificationMethod?.controller || null,
266
- verificationMethod,
267
- verifiablePresentation: buildPresentationFromResults({
268
- presentation,
269
- verifyResult
270
- }),
271
- verifyPresentationResults: buildVerifyPresentationResults({
272
- verifyResult
273
- }),
274
- openId: {
275
- clientProfileId,
276
- authorizationRequest,
277
- presentationSubmission
278
- }
279
- };
280
- if(envelope) {
281
- // include enveloped VP in step result
282
- stepResult.envelopedPresentation = presentation;
283
- }
284
- const prevState = exchange.state;
285
- exchange.variables.results[currentStep] = stepResult;
286
- try {
287
- exchange.sequence++;
194
+ // FIXME: OID4VP 1.0+ does not have a presentation submission
195
+ // handle mDL submission
196
+ const {envelope} = parseResponseResult;
197
+ if(envelope?.mediaType === 'application/mdl-vp-token') {
198
+ // generate `handover` for mDL verification
199
+ let handover;
200
+
201
+ // common `handover` parameters:
202
+ const origin = authorizationRequest?.expected_origins?.[0] ??
203
+ new URL(authorizationRequest.response_uri).origin;
204
+ const nonce = authorizationRequest.nonce;
205
+
206
+ // `direct_post.jwt` => ISO18013-7 Annex B
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
210
+ // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-invocation-via-redirects
211
+ const {responseMode} = parseResponseResult;
212
+ if(responseMode === 'direct_post.jwt') {
213
+ handover = {
214
+ type: 'AnnexBHandover',
215
+ // per ISO 18013-7 B the `mdocGeneratedNonce` is base64url-encoded
216
+ // and put into the `apu` protected header parameter, so parse that
217
+ // here and convert it to a UTF-8 string instead
218
+ mdocGeneratedNonce: Buffer
219
+ .from(parseResponseResult.protectedHeader?.apu ?? '', 'base64url')
220
+ .toString('utf8'),
221
+ clientId: authorizationRequest.client_id,
222
+ responseUri: authorizationRequest.response_uri,
223
+ verifierGeneratedNonce: nonce
224
+ };
225
+ } else if(responseMode === 'dc_api') {
226
+ // `dc_api` => ISO18013-7 Annex C
227
+ handover = {
228
+ type: 'dcapi',
229
+ origin,
230
+ nonce,
231
+ recipientPublicJwk: parseResponseResult.recipientPublicJwk
232
+ };
233
+ } else if(responseMode === 'dc_api.jwt') {
234
+ // `dc_api.jwt` => ISO18013-7 Annex D
235
+ handover = {
236
+ type: 'OpenID4VPDCAPIHandover',
237
+ origin,
238
+ nonce,
239
+ jwkThumbprint: parseResponseResult.recipientPublicJwkThumbprint
240
+ };
241
+ }
288
242
 
289
- // if there is something to issue, update exchange, do not complete it
290
- const {credentialTemplates = []} = workflow;
291
- if(credentialTemplates?.length > 0 &&
292
- (exchange.state === 'pending' || exchange.state === 'active')) {
293
- // ensure exchange state is set to `active` (will be rejected as a
294
- // conflict if the state in database at update time isn't `pending` or
295
- // `active`)
296
- exchange.state = 'active';
297
- await exchanges.update({workflowId: workflow.id, exchange});
298
- } else {
299
- // mark exchange complete
300
- exchange.state = 'complete';
301
- await exchanges.complete({workflowId: workflow.id, exchange});
243
+ verifyPresentationOptions.mdl = {
244
+ ...verifyPresentationOptions.mdl,
245
+ // send encoded mDL `sessionTranscript`
246
+ sessionTranscript: Buffer
247
+ .from(await oid4vp.mdl.encodeSessionTranscript({handover}))
248
+ .toString('base64url')
249
+ };
302
250
  }
303
- await emitExchangeUpdated({workflow, exchange, step});
304
- lastUpdated = Date.now();
305
- } catch(e) {
306
- // revert exchange changes as it couldn't be written
307
- exchange.sequence--;
308
- exchange.state = prevState;
309
- delete exchange.variables.results[currentStep];
310
- throw e;
311
- }
312
251
 
313
- const result = {};
252
+ // verify presentation
253
+ const verifyResult = await defaultVerify({
254
+ workflow,
255
+ verifyPresentationOptions,
256
+ verifyPresentationResultSchema,
257
+ presentation,
258
+ allowUnprotectedPresentation,
259
+ expectedChallenge,
260
+ expectedDomain
261
+ });
314
262
 
315
- // include `redirect_uri` if specified in step
316
- const {redirect_uri} = step.openId;
317
- if(redirect_uri) {
318
- result.redirect_uri = redirect_uri;
319
- }
263
+ // save OID4VP results in exchange
264
+ const {presentationSubmission} = parseResponseResult;
265
+ exchange.variables.results[exchange.step] = {
266
+ ...exchange.variables.results[exchange.step],
267
+ openId: {
268
+ clientProfileId,
269
+ authorizationRequest,
270
+ presentationSubmission
271
+ }
272
+ };
273
+ // FIXME: do w/o `parseResponseResult.envelope` to eliminate envelope
274
+ // parsing (let verifier do it)
275
+ if(parseResponseResult.envelope) {
276
+ // include enveloped VP in step result
277
+ exchange.variables.results[exchange.step]
278
+ .envelopedPresentation = presentation;
279
+ }
320
280
 
321
- return result;
322
- } catch(e) {
323
- if(e.name === 'InvalidStateError') {
324
- throw e;
281
+ return verifyResult;
325
282
  }
326
- // write last error if exchange hasn't been frequently updated
327
- const {id: workflowId} = workflow;
328
- const copy = {...exchange};
329
- copy.sequence++;
330
- copy.lastError = e;
331
- await exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
332
- .catch(error => logger.error(
333
- 'Could not set last exchange error: ' + error.message, {error}));
334
- await emitExchangeUpdated({workflow, exchange, step});
335
- throw e;
336
- }
283
+ });
284
+ await exchangeProcessor.process();
285
+
286
+ return result;
337
287
  }
338
288
 
339
289
  export async function supportsOID4VP({workflow, exchange, step}) {
@@ -346,7 +296,7 @@ export async function supportsOID4VP({workflow, exchange, step}) {
346
296
  return step.openId !== undefined;
347
297
  }
348
298
 
349
- async function _getStepAuthorizationRequest({
299
+ async function _getOrCreateStepAuthorizationRequest({
350
300
  workflow, exchange, clientProfileId, clientProfile, step
351
301
  }) {
352
302
  let authorizationRequest;