@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.
- package/lib/ExchangeProcessor.js +72 -46
- package/lib/helpers.js +39 -0
- package/lib/issue.js +54 -18
- package/lib/oid4/authorizationResponse.js +0 -1
- package/lib/oid4/http.js +13 -3
- package/lib/oid4/oid4vci.js +478 -273
- package/lib/oid4/oid4vciDraft13.js +197 -0
- package/lib/oid4/oid4vp.js +6 -1
- package/lib/vcapi.js +27 -2
- package/package.json +2 -2
- package/schemas/bedrock-vc-workflow.js +220 -180
package/lib/oid4/oid4vci.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
70
|
-
const configurationIds = Object.keys(
|
|
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
|
-
&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
|
394
|
-
// get all
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
405
|
-
const
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
425
|
-
|
|
719
|
+
async function _getSupportedCredentialRequests({
|
|
720
|
+
exchangeProcessor, workflow, exchange, step
|
|
426
721
|
}) {
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
let
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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
|
+
}
|