@bedrock/vc-delivery 7.11.3 → 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.
- package/lib/ExchangeProcessor.js +53 -32
- package/lib/helpers.js +1 -82
- package/lib/oid4/authorizationRequest.js +41 -11
- package/lib/oid4/authorizationResponse.js +20 -31
- package/lib/oid4/http.js +22 -3
- package/lib/oid4/oid4vp.js +146 -47
- package/lib/storage/exchanges.js +1 -0
- package/package.json +2 -2
- package/schemas/bedrock-vc-workflow.js +16 -0
package/lib/ExchangeProcessor.js
CHANGED
|
@@ -288,10 +288,21 @@ export class ExchangeProcessor {
|
|
|
288
288
|
// 4.2. Call subalgorithm `prepareStep`, passing `workflow`,
|
|
289
289
|
// `exchange`, `step`, `receivedPresentation`, and
|
|
290
290
|
// `receivedPresentationRequest` to perform any protocol-specific
|
|
291
|
-
// custom step preparation.
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
// custom step preparation. If `prepareStep` returns a `prepareResult`
|
|
292
|
+
// with `receivedPresentation` and/or `receivedPresentationRequest` set,
|
|
293
|
+
// then update `receivedPresentation` and/or
|
|
294
|
+
// `receivedPresentationRequest` accordingly.
|
|
295
|
+
const prepareResult = await prepareStep?.({
|
|
296
|
+
workflow, exchange, step,
|
|
297
|
+
receivedPresentation, receivedPresentationRequest
|
|
294
298
|
});
|
|
299
|
+
if(prepareResult?.receivedPresentation !== undefined) {
|
|
300
|
+
receivedPresentation = prepareResult.receivedPresentation;
|
|
301
|
+
}
|
|
302
|
+
if(prepareResult?.receivedPresentationRequest) {
|
|
303
|
+
receivedPresentationRequest =
|
|
304
|
+
prepareResult.receivedPresentationRequest;
|
|
305
|
+
}
|
|
295
306
|
|
|
296
307
|
// 4.3. If `receivedPresentation` is set, then call the
|
|
297
308
|
// `validateReceivedPresentation` sub-algorithm, passing `workflow`,
|
|
@@ -312,20 +323,27 @@ export class ExchangeProcessor {
|
|
|
312
323
|
});
|
|
313
324
|
}
|
|
314
325
|
|
|
315
|
-
// 4.5.
|
|
326
|
+
// 4.5. If the implementation supports blocking callbacks that can
|
|
327
|
+
// return results to be added to exchange variables (or return errors),
|
|
328
|
+
// call the callback and store its results in
|
|
329
|
+
// `exchange.variables.results[exchange.step].callbackResults` or
|
|
330
|
+
// throw any error received.
|
|
331
|
+
// FIXME: to be implemented
|
|
332
|
+
|
|
333
|
+
// 4.6. Set `isInputRequired` to the result of calling
|
|
316
334
|
// `inputRequired({step, receivedPresentation})`.
|
|
317
335
|
const isInputRequired = await inputRequired?.({
|
|
318
336
|
workflow, exchange, step, receivedPresentation
|
|
319
337
|
}) ?? false;
|
|
320
338
|
|
|
321
|
-
// 4.
|
|
339
|
+
// 4.7. If `isInputRequired` is true:
|
|
322
340
|
if(isInputRequired) {
|
|
323
|
-
// 4.
|
|
341
|
+
// 4.7.1. If `response` is `null`, set it to an empty object.
|
|
324
342
|
if(!response) {
|
|
325
343
|
response = {};
|
|
326
344
|
}
|
|
327
345
|
|
|
328
|
-
// 4.
|
|
346
|
+
// 4.7.2. If `step.verifiablePresentationRequest` is set, call
|
|
329
347
|
// the `createVerifiablePresentationRequest` sub-algorithm, passing
|
|
330
348
|
// `workflow`, `exchange`, `step`, and `response`.
|
|
331
349
|
if(step.verifiablePresentationRequest) {
|
|
@@ -334,12 +352,13 @@ export class ExchangeProcessor {
|
|
|
334
352
|
});
|
|
335
353
|
}
|
|
336
354
|
|
|
337
|
-
// 4.
|
|
355
|
+
// 4.7.3. Save the exchange (and call any non-blocking callback
|
|
356
|
+
// in the step) and return `response`.
|
|
338
357
|
await _updateExchange({workflow, exchange, meta, step});
|
|
339
358
|
return response;
|
|
340
359
|
}
|
|
341
360
|
|
|
342
|
-
// 4.
|
|
361
|
+
// 4.8. Set `issueToClient` to `true` if `step.issueRequests` includes
|
|
343
362
|
// any issuer requests for VCs that are to be sent to the client
|
|
344
363
|
// (`issueRequest.result` is NOT set), otherwise set it to `false`.
|
|
345
364
|
const issueRequestsParams = getIssueRequestsParams({
|
|
@@ -347,27 +366,28 @@ export class ExchangeProcessor {
|
|
|
347
366
|
});
|
|
348
367
|
const issueToClient = issueRequestsParams.some(p => !p.result);
|
|
349
368
|
|
|
350
|
-
// 4.
|
|
369
|
+
// 4.9. If `step.verifiablePresentation` is set or `issueToClient` is
|
|
351
370
|
// `true`:
|
|
352
371
|
if(step.verifiablePresentation || issueToClient) {
|
|
353
|
-
// 4.
|
|
372
|
+
// 4.9.1. If `response` is not `null`
|
|
354
373
|
if(response) {
|
|
355
|
-
// 4.
|
|
374
|
+
// 4.9.1.1. If `response.verifiablePresentationRequest` is not set,
|
|
356
375
|
// set it to an empty object (to indicate that the exchange is
|
|
357
376
|
// not yet complete).
|
|
358
377
|
if(!response.verifiablePresentationRequest) {
|
|
359
378
|
response.verifiablePresentationRequest = {};
|
|
360
379
|
}
|
|
361
|
-
// 4.
|
|
380
|
+
// 4.9.1.2. Save the exchange (and call any non-blocking callback
|
|
381
|
+
// in the step).
|
|
362
382
|
await _updateExchange({workflow, exchange, meta, step});
|
|
363
|
-
// 4.
|
|
383
|
+
// 4.9.1.3. Return `response`.
|
|
364
384
|
return response;
|
|
365
385
|
}
|
|
366
386
|
|
|
367
|
-
// 4.
|
|
387
|
+
// 4.9.2. Set `response` to an empty object.
|
|
368
388
|
response = {};
|
|
369
389
|
|
|
370
|
-
// 4.
|
|
390
|
+
// 4.9.3. If `step.verifiablePresentation` is set, set
|
|
371
391
|
// `response.verifiablePresentation` to a copy of it, otherwise
|
|
372
392
|
// set `response.verifiablePresentation` to a new, empty,
|
|
373
393
|
// Verifiable Presentation (using VCDM 2.0 by default, but a custom
|
|
@@ -380,14 +400,14 @@ export class ExchangeProcessor {
|
|
|
380
400
|
// issuance has been triggered
|
|
381
401
|
issuanceTriggered = true;
|
|
382
402
|
|
|
383
|
-
// 4.
|
|
384
|
-
// an error response to the client if any fails (note:
|
|
385
|
-
// can optionally implement failure recovery or retry
|
|
386
|
-
// their own discretion):
|
|
387
|
-
// 4.
|
|
403
|
+
// 4.10. Perform every issue request (optionally in parallel),
|
|
404
|
+
// returning an error response to the client if any fails (note:
|
|
405
|
+
// implementations can optionally implement failure recovery or retry
|
|
406
|
+
// issue requests at their own discretion):
|
|
407
|
+
// 4.10.1. For each issue request where `result` is set to an exchange
|
|
388
408
|
// variable path or name, save the issued credential in the referenced
|
|
389
409
|
// exchange variable.
|
|
390
|
-
// 4.
|
|
410
|
+
// 4.10.2. For each issue request where `result` is not specified, save
|
|
391
411
|
// the issued credential in `response.verifiablePresentation`, i.e.,
|
|
392
412
|
// for a VCDM presentation, append the issued credential to
|
|
393
413
|
// `response.verifiablePresentation.verifiableCredential`.
|
|
@@ -396,44 +416,45 @@ export class ExchangeProcessor {
|
|
|
396
416
|
verifiablePresentation: response?.verifiablePresentation
|
|
397
417
|
});
|
|
398
418
|
|
|
399
|
-
// 4.
|
|
419
|
+
// 4.11. If `response.verifiablePresentation` is set and the step
|
|
400
420
|
// configuration indicates it should be signed, sign the presentation
|
|
401
421
|
// (e.g., by using a VCALM holder instance's `/presentations/create`
|
|
402
422
|
// endpoint).
|
|
403
423
|
// FIXME: implement
|
|
404
424
|
//if(response?.verifiablePresentation) {}
|
|
405
425
|
|
|
406
|
-
// 4.
|
|
426
|
+
// 4.12. If `step.redirectUrl` is set:
|
|
407
427
|
if(step.redirectUrl) {
|
|
408
|
-
// 4.
|
|
428
|
+
// 4.12.1. If `response` is `null` then set it to an empty object.
|
|
409
429
|
if(!response) {
|
|
410
430
|
response = {};
|
|
411
431
|
}
|
|
412
|
-
// 4.
|
|
432
|
+
// 4.12.2. Set `response.redirectUrl` to `step.redirectUrl`.
|
|
413
433
|
response.redirectUrl = step.redirectUrl;
|
|
414
434
|
}
|
|
415
435
|
|
|
416
|
-
// 4.
|
|
436
|
+
// 4.13. If `step.nextStep` is not set then set `exchange.state` to
|
|
417
437
|
// `complete`.
|
|
418
438
|
if(!step.nextStep) {
|
|
419
439
|
exchange.state = 'complete';
|
|
420
440
|
} else {
|
|
421
|
-
// 4.
|
|
441
|
+
// 4.14. Otherwise, delete `exchange.variables.results[step.nextStep]`
|
|
422
442
|
// if it exists, and set `exchange.step` to `step.nextStep`.
|
|
423
443
|
delete exchange.variables.results[step.nextStep];
|
|
424
444
|
exchange.step = step.nextStep;
|
|
425
445
|
}
|
|
426
446
|
|
|
427
|
-
// 4.
|
|
447
|
+
// 4.15. Save the exchange (and call any non-blocking callback in
|
|
448
|
+
// the step).
|
|
428
449
|
await _updateExchange({workflow, exchange, meta, step});
|
|
429
450
|
|
|
430
|
-
// 4.
|
|
451
|
+
// 4.16. If `exchange.state` is `complete`, return `response` if it is
|
|
431
452
|
// not `null`, otherwise return an empty object.
|
|
432
453
|
if(exchange.state === 'complete') {
|
|
433
454
|
return response ?? {};
|
|
434
455
|
}
|
|
435
456
|
|
|
436
|
-
// 4.
|
|
457
|
+
// 4.17. Set `receivedPresentation` to `null`.
|
|
437
458
|
receivedPresentation = null;
|
|
438
459
|
}
|
|
439
460
|
} catch(e) {
|
|
@@ -458,7 +479,6 @@ export class ExchangeProcessor {
|
|
|
458
479
|
|
|
459
480
|
async function _getStep({workflow, exchange}) {
|
|
460
481
|
const currentStep = exchange.step;
|
|
461
|
-
|
|
462
482
|
if(!currentStep) {
|
|
463
483
|
// return default empty step and set dummy stepname for exchange
|
|
464
484
|
exchange.step = 'initial';
|
|
@@ -541,6 +561,7 @@ function _isInitialStep({workflow, exchange}) {
|
|
|
541
561
|
|
|
542
562
|
async function _updateExchange({workflow, exchange, meta, step}) {
|
|
543
563
|
try {
|
|
564
|
+
exchange.referenceId = globalThis.crypto.randomUUID();
|
|
544
565
|
exchange.sequence++;
|
|
545
566
|
if(exchange.state === 'complete') {
|
|
546
567
|
await exchanges.complete({workflowId: workflow.id, exchange});
|
package/lib/helpers.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
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 vcjwt from './vcjwt.js';
|
|
6
5
|
import {decodeId, generateId} from 'bnid';
|
|
7
6
|
import {compile} from '@bedrock/validation';
|
|
8
7
|
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
@@ -22,16 +21,6 @@ const ALLOWED_ERROR_KEYS = [
|
|
|
22
21
|
'status'
|
|
23
22
|
];
|
|
24
23
|
|
|
25
|
-
const JWT_FORMAT_ALIASES = new Set([
|
|
26
|
-
'application/jwt',
|
|
27
|
-
'application/vc+jwt',
|
|
28
|
-
'application/vp+jwt',
|
|
29
|
-
'jwt_vp',
|
|
30
|
-
'jwt_vp_json',
|
|
31
|
-
'jwt_vc_json-ld',
|
|
32
|
-
'jwt_vc_json'
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
24
|
export function buildPresentationFromResults({
|
|
36
25
|
presentation, verifyResult
|
|
37
26
|
}) {
|
|
@@ -126,7 +115,7 @@ export async function evaluateTemplate({
|
|
|
126
115
|
export async function evaluateExchangeStep({
|
|
127
116
|
workflow, exchange, stepName = exchange.step
|
|
128
117
|
}) {
|
|
129
|
-
let step = workflow.steps[stepName];
|
|
118
|
+
let step = workflow.steps[stepName] ?? {};
|
|
130
119
|
if(step.stepTemplate) {
|
|
131
120
|
step = await evaluateTemplate({
|
|
132
121
|
workflow, exchange, typedTemplate: step.stepTemplate
|
|
@@ -358,52 +347,8 @@ export function stripStacktrace(error) {
|
|
|
358
347
|
return error;
|
|
359
348
|
}
|
|
360
349
|
|
|
361
|
-
export async function unenvelopeCredential({
|
|
362
|
-
envelopedCredential, format
|
|
363
|
-
} = {}) {
|
|
364
|
-
const result = _getEnvelope({envelope: envelopedCredential, format});
|
|
365
|
-
|
|
366
|
-
// only supported format is VC-JWT at this time
|
|
367
|
-
const credential = vcjwt.decodeVCJWTCredential({jwt: result.envelope});
|
|
368
|
-
return {credential, ...result};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export async function unenvelopePresentation({
|
|
372
|
-
envelopedPresentation, format
|
|
373
|
-
} = {}) {
|
|
374
|
-
const result = _getEnvelope({envelope: envelopedPresentation, format});
|
|
375
|
-
|
|
376
|
-
// only supported format is VC-JWT at this time
|
|
377
|
-
const presentation = vcjwt.decodeVCJWTPresentation({jwt: result.envelope});
|
|
378
|
-
|
|
379
|
-
// unenvelope any VCs in the presentation
|
|
380
|
-
let {verifiableCredential = []} = presentation;
|
|
381
|
-
if(!Array.isArray(verifiableCredential)) {
|
|
382
|
-
verifiableCredential = [verifiableCredential];
|
|
383
|
-
}
|
|
384
|
-
if(verifiableCredential.length > 0) {
|
|
385
|
-
presentation.verifiableCredential = await Promise.all(
|
|
386
|
-
verifiableCredential.map(async vc => {
|
|
387
|
-
if(vc?.type !== 'EnvelopedVerifiableCredential') {
|
|
388
|
-
return vc;
|
|
389
|
-
}
|
|
390
|
-
const {credential} = await unenvelopeCredential({
|
|
391
|
-
envelopedCredential: vc
|
|
392
|
-
});
|
|
393
|
-
return credential;
|
|
394
|
-
}));
|
|
395
|
-
}
|
|
396
|
-
return {presentation, ...result};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
350
|
export async function validateStep({step} = {}) {
|
|
400
351
|
// FIXME: use `ajv` and do JSON schema check
|
|
401
|
-
if(Object.keys(step).length === 0) {
|
|
402
|
-
throw new BedrockError('Empty exchange step detected.', {
|
|
403
|
-
name: 'DataError',
|
|
404
|
-
details: {httpStatusCode: 500, public: true}
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
352
|
if(step.issueRequests !== undefined && !Array.isArray(step.issueRequests)) {
|
|
408
353
|
throw new BedrockError(
|
|
409
354
|
'Invalid "issueRequests" in step.', {
|
|
@@ -423,32 +368,6 @@ export async function validateStep({step} = {}) {
|
|
|
423
368
|
}
|
|
424
369
|
}
|
|
425
370
|
|
|
426
|
-
function _getEnvelope({envelope, format}) {
|
|
427
|
-
const isString = typeof envelope === 'string';
|
|
428
|
-
if(isString) {
|
|
429
|
-
// supported formats
|
|
430
|
-
if(JWT_FORMAT_ALIASES.has(format)) {
|
|
431
|
-
format = 'application/jwt';
|
|
432
|
-
}
|
|
433
|
-
} else {
|
|
434
|
-
const {id} = envelope;
|
|
435
|
-
if(id?.startsWith('data:application/jwt,')) {
|
|
436
|
-
format = 'application/jwt';
|
|
437
|
-
envelope = id.slice('data:application/jwt,'.length);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if(format === 'application/jwt' && envelope !== undefined) {
|
|
442
|
-
return {envelope, format};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
throw new BedrockError(
|
|
446
|
-
`Unsupported credential or presentation envelope format "${format}".`, {
|
|
447
|
-
name: 'NotSupportedError',
|
|
448
|
-
details: {httpStatusCode: 400, public: true}
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
371
|
export function validateVerifiablePresentation({schema, presentation}) {
|
|
453
372
|
const validate = compile({schema});
|
|
454
373
|
const {valid, error} = validate(presentation);
|
|
@@ -12,7 +12,14 @@ import {randomUUID} from 'node:crypto';
|
|
|
12
12
|
|
|
13
13
|
const {util: {BedrockError}} = bedrock;
|
|
14
14
|
|
|
15
|
+
const ENCRYPTED_RESPONSE_MODES = new Set([
|
|
16
|
+
'direct_post.jwt', 'dc_api.jwt', 'dc_api'
|
|
17
|
+
]);
|
|
15
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
|
+
|
|
16
23
|
const TEXT_ENCODER = new TextEncoder();
|
|
17
24
|
|
|
18
25
|
export async function create({
|
|
@@ -27,16 +34,24 @@ export async function create({
|
|
|
27
34
|
|
|
28
35
|
// get params from step OID4VP client profile to apply to the AR
|
|
29
36
|
const {
|
|
30
|
-
client_id,
|
|
37
|
+
client_id,
|
|
38
|
+
client_id_scheme,
|
|
39
|
+
dcql_query,
|
|
40
|
+
expected_origins,
|
|
31
41
|
nonce,
|
|
32
42
|
presentation_definition,
|
|
33
|
-
response_mode,
|
|
43
|
+
response_mode,
|
|
44
|
+
response_uri
|
|
34
45
|
} = clientProfile;
|
|
35
46
|
const clientBaseUrl = getClientBaseUrl({workflow, exchange, clientProfileId});
|
|
36
47
|
|
|
37
48
|
// client_id_scheme (draft versions of OID4VP use this param)
|
|
38
49
|
authorizationRequest.client_id_scheme = client_id_scheme ?? 'redirect_uri';
|
|
39
50
|
|
|
51
|
+
// dcql_query
|
|
52
|
+
authorizationRequest.dcql_query = dcql_query ??
|
|
53
|
+
authorizationRequest.dcql_query;
|
|
54
|
+
|
|
40
55
|
// presentation_definition
|
|
41
56
|
authorizationRequest.presentation_definition = presentation_definition ??
|
|
42
57
|
authorizationRequest.presentation_definition;
|
|
@@ -49,10 +64,13 @@ export async function create({
|
|
|
49
64
|
`${clientBaseUrl}/authorization/response`;
|
|
50
65
|
|
|
51
66
|
// client_id (defaults to `response_uri`)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
authorizationRequest.
|
|
55
|
-
|
|
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
|
+
}
|
|
56
74
|
|
|
57
75
|
// `x509_san_dns` requires the `direct_post.jwt` response mode when using
|
|
58
76
|
// `direct_post`
|
|
@@ -63,6 +81,10 @@ export async function create({
|
|
|
63
81
|
authorizationRequest.response_mode += '.jwt';
|
|
64
82
|
}
|
|
65
83
|
|
|
84
|
+
// expected origins; safe to always include
|
|
85
|
+
authorizationRequest.expected_origins = expected_origins ??
|
|
86
|
+
[new URL(authorizationRequest.response_uri).origin];
|
|
87
|
+
|
|
66
88
|
// nonce
|
|
67
89
|
if(nonce) {
|
|
68
90
|
authorizationRequest.nonce = nonce;
|
|
@@ -109,6 +131,16 @@ export async function encode({
|
|
|
109
131
|
return jwt;
|
|
110
132
|
}
|
|
111
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
|
+
|
|
112
144
|
async function _createClientMetaData({
|
|
113
145
|
authorizationRequest, clientProfile
|
|
114
146
|
} = {}) {
|
|
@@ -175,8 +207,8 @@ async function _createClientMetaData({
|
|
|
175
207
|
client_metadata.require_signed_request_object = true;
|
|
176
208
|
}
|
|
177
209
|
|
|
178
|
-
//
|
|
179
|
-
if(authorizationRequest.response_mode
|
|
210
|
+
// offer encryption options for encrypted response modes
|
|
211
|
+
if(ENCRYPTED_RESPONSE_MODES.has(authorizationRequest.response_mode)) {
|
|
180
212
|
// generate ECDH-ES P-256 key
|
|
181
213
|
const kp = await generateKeyPair('ECDH-ES', {
|
|
182
214
|
crv: 'P-256', extractable: true
|
|
@@ -239,9 +271,7 @@ async function _createJwt({workflow, clientProfile, authorizationRequest}) {
|
|
|
239
271
|
const kid = keyDescription.id;
|
|
240
272
|
|
|
241
273
|
// create the JWT payload and header to be signed
|
|
242
|
-
const payload = {
|
|
243
|
-
...authorizationRequest
|
|
244
|
-
};
|
|
274
|
+
const payload = {...authorizationRequest};
|
|
245
275
|
const protectedHeader = {typ: OID4VP_JWT_TYP, alg: 'ES256', kid, x5c};
|
|
246
276
|
|
|
247
277
|
// create the JWT
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
} from '../../schemas/bedrock-vc-workflow.js';
|
|
9
9
|
import {compile} from '@bedrock/validation';
|
|
10
10
|
import {oid4vp} from '@digitalbazaar/oid4-client';
|
|
11
|
-
import {unenvelopePresentation} from '../helpers.js';
|
|
12
11
|
|
|
13
12
|
const {util: {BedrockError}} = bedrock;
|
|
14
13
|
|
|
@@ -27,58 +26,46 @@ bedrock.events.on('bedrock.init', () => {
|
|
|
27
26
|
});
|
|
28
27
|
});
|
|
29
28
|
|
|
30
|
-
export async function parse({
|
|
29
|
+
export async function parse({
|
|
30
|
+
req, exchange, clientProfileId, authorizationRequest
|
|
31
|
+
} = {}) {
|
|
31
32
|
try {
|
|
32
33
|
const {body} = req;
|
|
33
34
|
const {
|
|
34
|
-
responseMode, parsed, protectedHeader
|
|
35
|
+
responseMode, parsed, protectedHeader,
|
|
36
|
+
recipientPublicJwk, recipientPublicJwkThumbprint,
|
|
37
|
+
vpTokenMediaType
|
|
35
38
|
} = await oid4vp.verifier.parseAuthorizationResponse({
|
|
36
39
|
body,
|
|
37
40
|
getDecryptParameters() {
|
|
38
41
|
return _getDecryptParameters({exchange, clientProfileId});
|
|
39
|
-
}
|
|
42
|
+
},
|
|
43
|
+
authorizationRequest
|
|
40
44
|
});
|
|
41
45
|
|
|
42
|
-
// validate parsed presentation submission
|
|
46
|
+
// validate parsed presentation submission if given
|
|
43
47
|
const {presentationSubmission} = parsed;
|
|
44
|
-
|
|
48
|
+
if(presentationSubmission) {
|
|
49
|
+
_validate(VALIDATORS.presentationSubmission, presentationSubmission);
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
// obtain `presentation` and optional `envelope` from parsed `vpToken`
|
|
47
53
|
const {vpToken} = parsed;
|
|
48
54
|
let presentation;
|
|
49
55
|
let envelope;
|
|
50
56
|
|
|
51
|
-
if(
|
|
52
|
-
|
|
53
|
-
})) {
|
|
54
|
-
// `vp_token` is declared to be a base64url-encoded mDL device response
|
|
55
|
-
presentation = {
|
|
56
|
-
'@context': VC_CONTEXT_2,
|
|
57
|
-
id: `data:application/mdl-vp-token,${vpToken}`,
|
|
58
|
-
type: 'EnvelopedVerifiablePresentation'
|
|
59
|
-
};
|
|
60
|
-
} else if(typeof vpToken === 'string') {
|
|
61
|
-
// FIXME: remove unenveloping here and delegate it to VC API verifier;
|
|
62
|
-
// FIXME: check if envelope matches submission once verified
|
|
63
|
-
const {
|
|
64
|
-
envelope: raw, presentation: contents, format
|
|
65
|
-
} = await unenvelopePresentation({
|
|
66
|
-
envelopedPresentation: vpToken,
|
|
67
|
-
// FIXME: check `presentationSubmission` for VP format
|
|
68
|
-
format: 'application/jwt'
|
|
69
|
-
});
|
|
70
|
-
_validate(VALIDATORS.presentation, contents);
|
|
57
|
+
if(vpTokenMediaType !== 'application/vp') {
|
|
58
|
+
// `vp_token` contains some enveloped format
|
|
71
59
|
presentation = {
|
|
72
60
|
'@context': VC_CONTEXT_2,
|
|
73
|
-
id: `data:${
|
|
61
|
+
id: `data:${vpTokenMediaType},${vpToken}`,
|
|
74
62
|
type: 'EnvelopedVerifiablePresentation'
|
|
75
63
|
};
|
|
76
|
-
envelope = {
|
|
64
|
+
envelope = {mediaType: vpTokenMediaType};
|
|
77
65
|
} else {
|
|
78
|
-
// simplest case: `vpToken` is a VP; validate it
|
|
66
|
+
// simplest case: `vpToken` is a VP; validate it against basic schema
|
|
79
67
|
presentation = vpToken;
|
|
80
68
|
_validate(VALIDATORS.presentation, presentation);
|
|
81
|
-
// FIXME: validate VP against presentation submission
|
|
82
69
|
}
|
|
83
70
|
|
|
84
71
|
return {
|
|
@@ -86,7 +73,9 @@ export async function parse({req, exchange, clientProfileId} = {}) {
|
|
|
86
73
|
presentationSubmission,
|
|
87
74
|
presentation,
|
|
88
75
|
envelope,
|
|
89
|
-
protectedHeader
|
|
76
|
+
protectedHeader,
|
|
77
|
+
recipientPublicJwk,
|
|
78
|
+
recipientPublicJwkThumbprint
|
|
90
79
|
};
|
|
91
80
|
} catch(cause) {
|
|
92
81
|
throw new BedrockError(
|
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
|
|
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) {
|
|
@@ -405,7 +424,7 @@ function _normalizeCredentials({verifiablePresentation}) {
|
|
|
405
424
|
// use raw format for each credential
|
|
406
425
|
const {verifiableCredential} = verifiablePresentation;
|
|
407
426
|
return verifiableCredential.map(vc => {
|
|
408
|
-
// parse any enveloped VC into its non-VC format
|
|
427
|
+
// parse any JWT-enveloped VC into its non-VC format
|
|
409
428
|
if(vc.type === 'EnvelopedVerifiableCredential' &&
|
|
410
429
|
vc.id?.startsWith('data:application/jwt,')) {
|
|
411
430
|
return vc.id.slice('data:application/jwt,'.length);
|
package/lib/oid4/oid4vp.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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;
|
|
@@ -138,15 +140,8 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
138
140
|
const {config: workflow} = req.serviceObject;
|
|
139
141
|
const exchangeRecord = await req.getExchange();
|
|
140
142
|
|
|
141
|
-
// ensure authz response can be parsed
|
|
142
|
-
const {
|
|
143
|
-
presentation, envelope, presentationSubmission,
|
|
144
|
-
responseMode, protectedHeader
|
|
145
|
-
} = await parseAuthorizationResponse({
|
|
146
|
-
req, exchange: exchangeRecord.exchange, clientProfileId
|
|
147
|
-
});
|
|
148
|
-
|
|
149
143
|
// process exchange and produce result
|
|
144
|
+
let parseResponseResult;
|
|
150
145
|
const result = {};
|
|
151
146
|
const exchangeProcessor = new ExchangeProcessor({
|
|
152
147
|
workflow, exchangeRecord,
|
|
@@ -156,15 +151,11 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
156
151
|
});
|
|
157
152
|
result.authorizationRequest = authorizationRequest;
|
|
158
153
|
|
|
159
|
-
// ensure response
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
name: 'ConstraintError',
|
|
165
|
-
details: {httpStatusCode: 400, public: true}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
154
|
+
// ensure authz response can be parsed
|
|
155
|
+
parseResponseResult = await parseAuthorizationResponse({
|
|
156
|
+
req, exchange: exchangeRecord.exchange, clientProfileId,
|
|
157
|
+
authorizationRequest
|
|
158
|
+
});
|
|
168
159
|
|
|
169
160
|
// only mark exchange complete if there is nothing to be issued; this
|
|
170
161
|
// handles same-step OID4VCI+OID4VP case
|
|
@@ -178,6 +169,9 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
178
169
|
if(redirect_uri) {
|
|
179
170
|
result.redirect_uri = redirect_uri;
|
|
180
171
|
}
|
|
172
|
+
|
|
173
|
+
const {presentation: receivedPresentation} = parseResponseResult;
|
|
174
|
+
return {receivedPresentation};
|
|
181
175
|
},
|
|
182
176
|
inputRequired({step}) {
|
|
183
177
|
// indicate input always required to avoid automatically advancing
|
|
@@ -199,26 +193,58 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
199
193
|
verifyPresentationOptions.challenge = authorizationRequest.nonce;
|
|
200
194
|
verifyPresentationOptions.domain = authorizationRequest.response_uri;
|
|
201
195
|
|
|
202
|
-
|
|
203
|
-
if(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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` for non-Annex-B; this is not yet supported
|
|
209
|
+
// https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-invocation-via-redirects
|
|
210
|
+
const {responseMode} = parseResponseResult;
|
|
211
|
+
if(responseMode === 'direct_post.jwt') {
|
|
212
|
+
handover = {
|
|
213
|
+
type: 'AnnexBHandover',
|
|
214
|
+
// per ISO 18013-7 B the `mdocGeneratedNonce` is base64url-encoded
|
|
215
|
+
// and put into the `apu` protected header parameter, so parse that
|
|
216
|
+
// here and convert it to a UTF-8 string instead
|
|
217
|
+
mdocGeneratedNonce: Buffer
|
|
218
|
+
.from(parseResponseResult.protectedHeader?.apu ?? '', 'base64url')
|
|
219
|
+
.toString('utf8'),
|
|
220
|
+
clientId: authorizationRequest.client_id,
|
|
221
|
+
responseUri: authorizationRequest.response_uri,
|
|
222
|
+
verifierGeneratedNonce: nonce
|
|
223
|
+
};
|
|
224
|
+
} else if(responseMode === 'dc_api') {
|
|
225
|
+
// `dc_api` => ISO18013-7 Annex C
|
|
226
|
+
handover = {
|
|
227
|
+
type: 'dcapi',
|
|
228
|
+
origin,
|
|
229
|
+
nonce,
|
|
230
|
+
recipientPublicJwk: parseResponseResult.recipientPublicJwk
|
|
231
|
+
};
|
|
232
|
+
} else if(responseMode === 'dc_api.jwt') {
|
|
233
|
+
// `dc_api.jwt` => ISO18013-7 Annex D
|
|
234
|
+
handover = {
|
|
235
|
+
type: 'OpenID4VPDCAPIHandover',
|
|
236
|
+
origin,
|
|
237
|
+
nonce,
|
|
238
|
+
jwkThumbprint: parseResponseResult.recipientPublicJwkThumbprint
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
207
242
|
verifyPresentationOptions.mdl = {
|
|
208
243
|
...verifyPresentationOptions.mdl,
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
sessionTranscript: {
|
|
214
|
-
// per ISO 18013-7 the `mdocGeneratedNonce` is base64url-encoded
|
|
215
|
-
// and put into the `apu` protected header parameter -- and the
|
|
216
|
-
// VC API `mdl.sessionTranscript` option expects the
|
|
217
|
-
// `mdocGeneratedNonce` to be base64url-encoded, so we can pass
|
|
218
|
-
// it straight through
|
|
219
|
-
mdocGeneratedNonce: protectedHeader.apu,
|
|
220
|
-
clientId: authorizationRequest.client_id
|
|
221
|
-
}
|
|
244
|
+
// send encoded mDL `sessionTranscript`
|
|
245
|
+
sessionTranscript: Buffer
|
|
246
|
+
.from(await oid4vp.mdl.encodeSessionTranscript({handover}))
|
|
247
|
+
.toString('base64url')
|
|
222
248
|
};
|
|
223
249
|
}
|
|
224
250
|
|
|
@@ -234,6 +260,7 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
234
260
|
});
|
|
235
261
|
|
|
236
262
|
// save OID4VP results in exchange
|
|
263
|
+
const {presentationSubmission} = parseResponseResult;
|
|
237
264
|
exchange.variables.results[exchange.step] = {
|
|
238
265
|
...exchange.variables.results[exchange.step],
|
|
239
266
|
openId: {
|
|
@@ -242,7 +269,7 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
242
269
|
presentationSubmission
|
|
243
270
|
}
|
|
244
271
|
};
|
|
245
|
-
if(envelope) {
|
|
272
|
+
if(parseResponseResult.envelope) {
|
|
246
273
|
// include enveloped VP in step result
|
|
247
274
|
exchange.variables.results[exchange.step]
|
|
248
275
|
.envelopedPresentation = presentation;
|
|
@@ -251,7 +278,7 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
251
278
|
return verifyResult;
|
|
252
279
|
}
|
|
253
280
|
});
|
|
254
|
-
await exchangeProcessor.process(
|
|
281
|
+
await exchangeProcessor.process();
|
|
255
282
|
|
|
256
283
|
return result;
|
|
257
284
|
}
|
|
@@ -266,6 +293,21 @@ export async function supportsOID4VP({workflow, exchange, step}) {
|
|
|
266
293
|
return step.openId !== undefined;
|
|
267
294
|
}
|
|
268
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
|
+
|
|
269
311
|
async function _getOrCreateStepAuthorizationRequest({
|
|
270
312
|
workflow, exchange, clientProfileId, clientProfile, step
|
|
271
313
|
}) {
|
|
@@ -274,6 +316,9 @@ async function _getOrCreateStepAuthorizationRequest({
|
|
|
274
316
|
// get authorization request
|
|
275
317
|
authorizationRequest = clientProfile.authorizationRequest;
|
|
276
318
|
if(authorizationRequest) {
|
|
319
|
+
authorizationRequest = _normalizeAuthorizationRequest({
|
|
320
|
+
authorizationRequest, exchange, clientProfile
|
|
321
|
+
});
|
|
277
322
|
return {authorizationRequest, exchangeChanged: false};
|
|
278
323
|
}
|
|
279
324
|
|
|
@@ -298,6 +343,9 @@ async function _getOrCreateStepAuthorizationRequest({
|
|
|
298
343
|
variables: exchange.variables, name: authzReqVarName
|
|
299
344
|
});
|
|
300
345
|
if(authorizationRequest) {
|
|
346
|
+
authorizationRequest = _normalizeAuthorizationRequest({
|
|
347
|
+
authorizationRequest, exchange, clientProfile
|
|
348
|
+
});
|
|
301
349
|
return {authorizationRequest, exchangeChanged: false};
|
|
302
350
|
}
|
|
303
351
|
|
|
@@ -308,7 +356,9 @@ async function _getOrCreateStepAuthorizationRequest({
|
|
|
308
356
|
clientProfile, clientProfileId,
|
|
309
357
|
verifiablePresentationRequest
|
|
310
358
|
});
|
|
311
|
-
authorizationRequest =
|
|
359
|
+
authorizationRequest = _normalizeAuthorizationRequest({
|
|
360
|
+
authorizationRequest: result.authorizationRequest, exchange, clientProfile
|
|
361
|
+
});
|
|
312
362
|
|
|
313
363
|
// merge any newly created exchange secrets
|
|
314
364
|
exchange.secrets = {
|
|
@@ -335,6 +385,55 @@ async function _getOrCreateStepAuthorizationRequest({
|
|
|
335
385
|
return {authorizationRequest, exchangeChanged: true};
|
|
336
386
|
}
|
|
337
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
|
+
|
|
338
437
|
function _throwUnsupportedProtocol() {
|
|
339
438
|
throw new BedrockError('OID4VP is not supported by this exchange.', {
|
|
340
439
|
name: 'NotSupportedError',
|
package/lib/storage/exchanges.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|
|
@@ -460,10 +460,22 @@ const oid4vpClientProfile = {
|
|
|
460
460
|
client_id: {type: 'string'},
|
|
461
461
|
client_id_scheme: {type: 'string'},
|
|
462
462
|
client_metadata: {type: 'object'},
|
|
463
|
+
dcql_query: {type: 'object'},
|
|
464
|
+
expected_origins: {
|
|
465
|
+
type: 'array',
|
|
466
|
+
minItems: 1,
|
|
467
|
+
items: {
|
|
468
|
+
type: 'string'
|
|
469
|
+
}
|
|
470
|
+
},
|
|
463
471
|
nonce: {type: 'string'},
|
|
464
472
|
presentation_definition: {type: 'object'},
|
|
465
473
|
response_mode: {type: 'string'},
|
|
466
474
|
response_uri: {type: 'string'},
|
|
475
|
+
response_uri_method: {
|
|
476
|
+
type: 'string',
|
|
477
|
+
enum: ['get', 'post']
|
|
478
|
+
},
|
|
467
479
|
// optional parameters for signing authorization requests
|
|
468
480
|
authorizationRequestSigningParameters: {
|
|
469
481
|
type: 'object',
|
|
@@ -490,6 +502,10 @@ const oid4vpClientProfile = {
|
|
|
490
502
|
},
|
|
491
503
|
scheme: {
|
|
492
504
|
type: 'string'
|
|
505
|
+
},
|
|
506
|
+
version: {
|
|
507
|
+
type: 'string',
|
|
508
|
+
enum: ['OID4VP-draft18', 'OID4VP-1.0']
|
|
493
509
|
}
|
|
494
510
|
}
|
|
495
511
|
},
|