@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/constants.js +4 -2
- package/lib/exchanges.js +14 -10
- package/lib/helpers.js +29 -4
- package/lib/http.js +15 -31
- package/lib/index.js +9 -3
- package/lib/oid4/authorizationRequest.js +238 -0
- package/lib/oid4/authorizationResponse.js +117 -0
- package/lib/oid4/clientProfiles.js +36 -0
- package/lib/oid4/http.js +81 -64
- package/lib/oid4/oid4vci.js +38 -10
- package/lib/oid4/oid4vp.js +206 -197
- package/lib/vcapi.js +158 -78
- package/lib/verify.js +2 -2
- package/package.json +5 -4
- package/schemas/bedrock-vc-workflow.js +176 -54
package/lib/oid4/http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
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
|
-
//
|
|
219
|
+
// format VC(s)
|
|
220
220
|
const {
|
|
221
|
-
response: {verifiablePresentation
|
|
222
|
-
format
|
|
221
|
+
response: {verifiablePresentation}, format
|
|
223
222
|
} = result;
|
|
224
|
-
|
|
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
|
-
//
|
|
317
|
+
// format VC(s)
|
|
330
318
|
const {
|
|
331
|
-
response: {verifiablePresentation
|
|
319
|
+
response: {verifiablePresentation},
|
|
332
320
|
format
|
|
333
321
|
} = result;
|
|
334
|
-
|
|
335
|
-
|
|
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(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
}
|
package/lib/oid4/oid4vci.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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...
|