@digitalbazaar/oid4-client 5.0.0 → 5.1.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.
@@ -9,60 +9,101 @@ import {pathsToVerifiableCredentialPointers} from './convert.js';
9
9
 
10
10
  const TEXT_ENCODER = new TextEncoder();
11
11
 
12
- export async function send({
12
+ // creates an authorization response without sending it; use `send()` to create
13
+ // and send one at once
14
+ export async function create({
13
15
  verifiablePresentation,
14
16
  presentationSubmission,
15
17
  authorizationRequest,
16
18
  vpToken,
17
- encryptionOptions = {},
18
- agent
19
+ encryptionOptions = {}
19
20
  } = {}) {
20
- try {
21
- if(!(verifiablePresentation || vpToken)) {
21
+ if(!(verifiablePresentation || vpToken)) {
22
+ throw createNamedError({
23
+ message: 'One of "verifiablePresentation" or "vpToken" must be given.',
24
+ name: 'DataError'
25
+ });
26
+ }
27
+ // if no `vpToken` given, use VP
28
+ vpToken = vpToken ?? JSON.stringify(verifiablePresentation);
29
+
30
+ // if no `presentationSubmission` provided, auto-generate one
31
+ let generatedPresentationSubmission = false;
32
+ if(!presentationSubmission) {
33
+ ({presentationSubmission} = createPresentationSubmission({
34
+ presentationDefinition: authorizationRequest.presentation_definition,
35
+ verifiablePresentation
36
+ }));
37
+ generatedPresentationSubmission = true;
38
+ }
39
+
40
+ // prepare response body
41
+ const body = {};
42
+
43
+ // if `authorizationRequest.response_mode` is `direct.jwt` generate a JWT
44
+ if(authorizationRequest.response_mode === 'direct_post.jwt') {
45
+ if(submitsFormat({presentationSubmission, format: 'mso_mdoc'}) &&
46
+ !encryptionOptions?.mdl?.sessionTranscript) {
22
47
  throw createNamedError({
23
- message: 'One of "verifiablePresentation" or "vpToken" must be given.',
48
+ message: '"encryptionOptions.mdl.sessionTranscript" is required ' +
49
+ 'when submitting an mDL presentation.',
24
50
  name: 'DataError'
25
51
  });
26
52
  }
27
- // if no `vpToken` given, use VP
28
- vpToken = vpToken ?? JSON.stringify(verifiablePresentation);
29
-
30
- // if no `presentationSubmission` provided, auto-generate one
31
- let generatedPresentationSubmission = false;
32
- if(!presentationSubmission) {
33
- ({presentationSubmission} = createPresentationSubmission({
34
- presentationDefinition: authorizationRequest.presentation_definition,
35
- verifiablePresentation
36
- }));
37
- generatedPresentationSubmission = true;
38
- }
39
53
 
40
- // prepare response body
41
- const body = new URLSearchParams();
54
+ const jwt = await _encrypt({
55
+ vpToken, presentationSubmission, authorizationRequest,
56
+ encryptionOptions
57
+ });
58
+ body.response = jwt;
59
+ } else {
60
+ // include vp token and presentation submittion directly in body
61
+ body.vp_token = vpToken;
62
+ body.presentation_submission = JSON.stringify(presentationSubmission);
63
+ }
42
64
 
43
- // if `authorizationRequest.response_mode` is `direct.jwt` generate a JWT
44
- if(authorizationRequest.response_mode === 'direct_post.jwt') {
45
- if(submitsFormat({presentationSubmission, format: 'mso_mdoc'}) &&
46
- !encryptionOptions?.mdl?.sessionTranscript) {
47
- throw createNamedError({
48
- message: '"encryptionOptions.mdl.sessionTranscript" is required ' +
49
- 'when submitting an mDL presentation.',
50
- name: 'DataError'
51
- });
52
- }
65
+ const authorizationResponse = body;
66
+ if(generatedPresentationSubmission) {
67
+ // return any generated presentation submission
68
+ return {authorizationResponse, presentationSubmission};
69
+ }
70
+ return {authorizationResponse};
71
+ }
53
72
 
54
- const jwt = await _encrypt({
55
- vpToken, presentationSubmission, authorizationRequest,
73
+ export async function send({
74
+ verifiablePresentation,
75
+ presentationSubmission,
76
+ authorizationRequest,
77
+ vpToken,
78
+ encryptionOptions = {},
79
+ authorizationResponse,
80
+ agent
81
+ } = {}) {
82
+ try {
83
+ // create `authorizationResponse` if not passed
84
+ let generatedPresentationSubmission;
85
+ if(!authorizationResponse) {
86
+ ({
87
+ authorizationResponse,
88
+ presentationSubmission: generatedPresentationSubmission
89
+ } = await create({
90
+ verifiablePresentation,
91
+ presentationSubmission,
92
+ authorizationRequest,
93
+ vpToken,
56
94
  encryptionOptions
57
- });
58
- body.set('response', jwt);
59
- } else {
60
- // include vp token and presentation submittion directly in body
61
- body.set('vp_token', vpToken);
62
- body.set(
63
- 'presentation_submission', JSON.stringify(presentationSubmission));
95
+ }));
96
+ } else if(verifiablePresentation || presentationSubmission || vpToken ||
97
+ encryptionOptions) {
98
+ throw new TypeError(
99
+ 'Only "authorizationResponse" or its components ( ' +
100
+ '"verifiablePresentation", "presentationSubmission", "vpToken", ' +
101
+ '"encryptionOptions") can be passed, but not both.');
64
102
  }
65
103
 
104
+ // prepare response body
105
+ const body = new URLSearchParams(authorizationResponse);
106
+
66
107
  // send response
67
108
  const response = await httpClient.post(authorizationRequest.response_uri, {
68
109
  agent, body, headers: {accept: 'application/json'},
@@ -75,7 +116,7 @@ export async function send({
75
116
  const result = response.data || {};
76
117
  if(generatedPresentationSubmission) {
77
118
  // return any generated presentation submission
78
- return {result, presentationSubmission};
119
+ return {result, presentationSubmission: generatedPresentationSubmission};
79
120
  }
80
121
  return {result};
81
122
  } catch(cause) {
package/lib/oid4vp.js CHANGED
@@ -4,6 +4,7 @@
4
4
  export * as authzRequest from './authorizationRequest.js';
5
5
  export * as authzResponse from './authorizationResponse.js';
6
6
  export * as convert from './convert.js';
7
+ export * as verifier from './verifier.js';
7
8
 
8
9
  // backwards compatibility APIs
9
10
  export {
package/lib/util.js CHANGED
@@ -34,12 +34,15 @@ export function base64Encode(data) {
34
34
  return data.toBase64();
35
35
  }
36
36
  // note: this is base64-no-pad; will only work with specific data lengths
37
- base64url.encode(data).replace(/-/g, '+').replace(/_/g, '/');
37
+ return base64url.encode(data).replace(/-/g, '+').replace(/_/g, '/');
38
38
  }
39
39
 
40
- export function createNamedError({message, name, cause} = {}) {
40
+ export function createNamedError({message, name, details, cause} = {}) {
41
41
  const error = new Error(message, {cause});
42
42
  error.name = name;
43
+ if(details) {
44
+ error.details = details;
45
+ }
43
46
  return error;
44
47
  }
45
48
 
@@ -0,0 +1,115 @@
1
+ /*!
2
+ * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {createNamedError, selectJwk} from './util.js';
5
+ import {importJWK, jwtDecrypt} from 'jose';
6
+
7
+ // parses (and decrypts) an authz response from a response body object
8
+ export async function parseAuthorizationResponse({
9
+ body = {},
10
+ supportedResponseModes = ['direct_post.jwt', 'direct_post'],
11
+ getDecryptParameters
12
+ }) {
13
+ let responseMode;
14
+ const parsed = {};
15
+ let payload;
16
+ let protectedHeader;
17
+
18
+ supportedResponseModes = new Set(supportedResponseModes);
19
+
20
+ if(body.response) {
21
+ // `body.response` is present which must contain an encrypted JWT
22
+ responseMode = 'direct_post.jwt';
23
+ _assertSupportedResponseMode({responseMode, supportedResponseModes});
24
+ const jwt = body.response;
25
+ ({
26
+ payload,
27
+ protectedHeader
28
+ } = await _decrypt({jwt, getDecryptParameters}));
29
+ parsed.presentationSubmission = payload.presentation_submission;
30
+ } else {
31
+ responseMode = 'direct_post';
32
+ _assertSupportedResponseMode({responseMode, supportedResponseModes});
33
+ payload = body;
34
+ parsed.presentationSubmission = _jsonParse(
35
+ payload.presentation_submission, 'presentation_submission');
36
+ }
37
+
38
+ // `vp_token` is either:
39
+ // 1. a JSON object (a VP)
40
+ // 2. a JSON array (of something)
41
+ // 3. a JSON string (a quoted JWT: "<JWT>")
42
+ // 4. a JWT
43
+ // 5. a base64url-encoded mDL device response
44
+ // 6. unknown
45
+ const {vp_token} = payload;
46
+ if(typeof vp_token === 'string' &&
47
+ (vp_token.startsWith('{') || vp_token.startsWith('[') ||
48
+ vp_token.startsWith('"'))) {
49
+ parsed.vpToken = _jsonParse(vp_token, 'vp_token');
50
+ } else {
51
+ parsed.vpToken = vp_token;
52
+ }
53
+
54
+ return {responseMode, parsed, payload, protectedHeader};
55
+ }
56
+
57
+ function _assertSupportedResponseMode({
58
+ responseMode, supportedResponseModes
59
+ }) {
60
+ if(!supportedResponseModes.has(responseMode)) {
61
+ throw createNamedError({
62
+ message: `Unsupported response mode "${responseMode}".`,
63
+ name: 'NotSupportedError'
64
+ });
65
+ }
66
+ }
67
+
68
+ async function _decrypt({jwt, getDecryptParameters}) {
69
+ if(typeof getDecryptParameters !== 'function') {
70
+ throw new TypeError(
71
+ '"getDecryptParameters" is required for "direct_post.jwt" ' +
72
+ 'response mode.');
73
+ }
74
+
75
+ const params = await getDecryptParameters({jwt});
76
+ const {keys} = params;
77
+ let {getKey} = params;
78
+ if(!getKey) {
79
+ // note: `jose` lib's JWK key set feature cannot be used and passed to
80
+ // `jwtDecrypt()` as the second parameter because the expected `alg`
81
+ // "ECDH-ES" is not a unsupported algorithm for selecting a key from a set
82
+ getKey = protectedHeader => {
83
+ if(protectedHeader.alg !== 'ECDH-ES') {
84
+ const error = createNamedError({
85
+ message: `Unsupported algorithm "${protectedHeader.alg}"; ` +
86
+ 'algorithm must be "ECDH-ES".',
87
+ name: 'NotSupportedError',
88
+ details: {httpStatusCode: 400, public: true}
89
+ });
90
+ throw error;
91
+ }
92
+ const jwk = selectJwk({keys, kid: protectedHeader.kid});
93
+ return importJWK(jwk, 'ECDH-ES');
94
+ };
95
+ }
96
+
97
+ return jwtDecrypt(jwt, getKey, {
98
+ // only supported algorithms at this time:
99
+ contentEncryptionAlgorithms: ['A256GCM'],
100
+ keyManagementAlgorithms: ['ECDH-ES']
101
+ });
102
+ }
103
+
104
+ function _jsonParse(x, name) {
105
+ try {
106
+ return JSON.parse(x);
107
+ } catch(cause) {
108
+ throw createNamedError({
109
+ message: `Could not parse "${name}".`,
110
+ name: 'DataError',
111
+ details: {httpStatusCode: 400, public: true},
112
+ cause
113
+ });
114
+ }
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalbazaar/oid4-client",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "An OID4 (VC + VP) client",
5
5
  "homepage": "https://github.com/digitalbazaar/oid4-client",
6
6
  "author": {
@@ -25,12 +25,14 @@
25
25
  "dependencies": {
26
26
  "@digitalbazaar/http-client": "^4.0.0",
27
27
  "base64url-universal": "^2.0.0",
28
- "jose": "^6.0.13",
28
+ "jose": "^6.1.0",
29
29
  "jsonpath-plus": "^10.3.0",
30
30
  "jsonpointer": "^5.0.1",
31
31
  "pkijs": "^3.2.5"
32
32
  },
33
33
  "devDependencies": {
34
+ "@auth0/mdl": "^3.0.0",
35
+ "asn1js": "^3.0.6",
34
36
  "c8": "^10.1.3",
35
37
  "chai": "^4.3.6",
36
38
  "cross-env": "^10.0.0",