@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.
- package/lib/authorizationResponse.js +81 -40
- package/lib/oid4vp.js +1 -0
- package/lib/util.js +5 -2
- package/lib/verifier.js +115 -0
- package/package.json +4 -2
|
@@ -9,60 +9,101 @@ import {pathsToVerifiableCredentialPointers} from './convert.js';
|
|
|
9
9
|
|
|
10
10
|
const TEXT_ENCODER = new TextEncoder();
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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: '
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'
|
|
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
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
|
|
package/lib/verifier.js
ADDED
|
@@ -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.
|
|
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
|
|
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",
|