@digitalbazaar/oid4-client 4.3.0 → 4.4.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/authorizationRequest.js +344 -0
- package/lib/authorizationResponse.js +312 -0
- package/lib/convert.js +440 -0
- package/lib/index.js +3 -2
- package/lib/oid4vp.js +20 -836
- package/lib/util.js +50 -1
- package/package.json +8 -9
package/lib/oid4vp.js
CHANGED
|
@@ -1,840 +1,24 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2023-
|
|
2
|
+
* Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
export * as authzRequest from './authorizationRequest.js';
|
|
5
|
+
export * as authzResponse from './authorizationResponse.js';
|
|
6
|
+
export * as convert from './convert.js';
|
|
7
|
+
|
|
8
|
+
// backwards compatibility APIs
|
|
9
|
+
export {
|
|
10
|
+
get as getAuthorizationRequest
|
|
11
|
+
} from './authorizationRequest.js';
|
|
12
|
+
export {
|
|
13
|
+
createPresentationSubmission,
|
|
14
|
+
send as sendAuthorizationResponse
|
|
15
|
+
} from './authorizationResponse.js';
|
|
16
|
+
export {
|
|
17
|
+
fromVpr, toVpr,
|
|
18
|
+
// exported for testing purposes only
|
|
19
|
+
_fromQueryByExampleQuery
|
|
20
|
+
} from './convert.js';
|
|
21
|
+
|
|
22
|
+
// Note: for examples of presentation request and responses, see:
|
|
12
23
|
// eslint-disable-next-line max-len
|
|
13
24
|
// https://openid.github.io/OpenID4VP/openid-4-verifiable-presentations-wg-draft.html#appendix-A.1.2.2
|
|
14
|
-
|
|
15
|
-
// get an authorization request from a verifier
|
|
16
|
-
export async function getAuthorizationRequest({
|
|
17
|
-
url, agent, documentLoader
|
|
18
|
-
} = {}) {
|
|
19
|
-
try {
|
|
20
|
-
assert(url, 'url', 'string');
|
|
21
|
-
assertOptional(documentLoader, 'documentLoader', 'function');
|
|
22
|
-
|
|
23
|
-
let requestUrl = url;
|
|
24
|
-
let expectedClientId;
|
|
25
|
-
if(url.startsWith('openid4vp://')) {
|
|
26
|
-
const {authorizationRequest} = _parseOID4VPUrl({url});
|
|
27
|
-
if(authorizationRequest.request) {
|
|
28
|
-
const error = new Error(
|
|
29
|
-
'JWT-Secured Authorization Request (JAR) not implemented.');
|
|
30
|
-
error.name = 'NotSupportedError';
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
if(!authorizationRequest.request_uri) {
|
|
34
|
-
// return direct request
|
|
35
|
-
return {authorizationRequest, fetched: false};
|
|
36
|
-
}
|
|
37
|
-
requestUrl = authorizationRequest.request_uri;
|
|
38
|
-
({client_id: expectedClientId} = authorizationRequest);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// FIXME: every `fetchJSON` call needs to use a block list or other
|
|
42
|
-
// protections to prevent a confused deputy attack where the `requestUrl`
|
|
43
|
-
// accesses a location it should not, e.g., is on localhost and should
|
|
44
|
-
// not be used in this way
|
|
45
|
-
const response = await fetchJSON({url: requestUrl, agent});
|
|
46
|
-
|
|
47
|
-
// parse payload from response data...
|
|
48
|
-
const contentType = response.headers.get('content-type');
|
|
49
|
-
const jwt = await response.text();
|
|
50
|
-
// verify response is a JWT-secured authorization request
|
|
51
|
-
if(!(contentType.includes('application/oauth-authz-req+jwt') &&
|
|
52
|
-
typeof jwt === 'string')) {
|
|
53
|
-
const error = new Error(
|
|
54
|
-
'Authorization request content-type must be ' +
|
|
55
|
-
'"application/oauth-authz-req+jwt".');
|
|
56
|
-
error.name = 'DataError';
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// decode JWT *WITHOUT* verification
|
|
61
|
-
const payload = decodeJwt(jwt);
|
|
62
|
-
|
|
63
|
-
// validate payload (expected authorization request)
|
|
64
|
-
const {
|
|
65
|
-
client_id,
|
|
66
|
-
client_id_scheme,
|
|
67
|
-
client_metadata,
|
|
68
|
-
client_metadata_uri,
|
|
69
|
-
nonce,
|
|
70
|
-
presentation_definition,
|
|
71
|
-
presentation_definition_uri,
|
|
72
|
-
response_mode,
|
|
73
|
-
scope
|
|
74
|
-
} = payload;
|
|
75
|
-
assert(client_id, 'client_id', 'string');
|
|
76
|
-
// ensure `client_id` matches expected client ID
|
|
77
|
-
if(expectedClientId !== undefined && client_id !== expectedClientId) {
|
|
78
|
-
const error = new Error(
|
|
79
|
-
'"client_id" in fetched request does not match authorization ' +
|
|
80
|
-
'request URL parameter.');
|
|
81
|
-
error.name = 'DataError';
|
|
82
|
-
throw error;
|
|
83
|
-
}
|
|
84
|
-
assert(nonce, 'nonce', 'string');
|
|
85
|
-
assertOptional(client_id_scheme, 'client_id_scheme', 'string');
|
|
86
|
-
assertOptional(client_metadata, 'client_metadata', 'object');
|
|
87
|
-
assertOptional(client_metadata_uri, 'client_metadata_uri', 'string');
|
|
88
|
-
assertOptional(
|
|
89
|
-
presentation_definition, 'presentation_definition', 'object');
|
|
90
|
-
assertOptional(
|
|
91
|
-
presentation_definition_uri, 'presentation_definition_uri', 'string');
|
|
92
|
-
assertOptional(response_mode, 'response_mode', 'string');
|
|
93
|
-
assertOptional(scope, 'scope', 'string');
|
|
94
|
-
if(client_metadata && client_metadata_uri) {
|
|
95
|
-
const error = new Error(
|
|
96
|
-
'Only one of "client_metadata" and ' +
|
|
97
|
-
'"client_metadata_uri" must be present.');
|
|
98
|
-
error.name = 'DataError';
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
if(presentation_definition && presentation_definition_uri) {
|
|
102
|
-
const error = new Error(
|
|
103
|
-
'Only one of "presentation_definition" and ' +
|
|
104
|
-
'"presentation_definition_uri" must be present.');
|
|
105
|
-
error.name = 'DataError';
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
108
|
-
// Note: This implementation requires `response_mode` to be `direct_post`,
|
|
109
|
-
// no other modes are supported.
|
|
110
|
-
if(response_mode !== 'direct_post') {
|
|
111
|
-
const error = new Error(
|
|
112
|
-
'Only "direct_post" response mode is supported.');
|
|
113
|
-
error.name = 'NotSupportedError';
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// build merged authorization request
|
|
118
|
-
const authorizationRequest = {...payload};
|
|
119
|
-
|
|
120
|
-
// get client meta data from URL if specified
|
|
121
|
-
if(client_metadata_uri) {
|
|
122
|
-
const response = await fetchJSON({url: client_metadata_uri, agent});
|
|
123
|
-
if(!response.data) {
|
|
124
|
-
const error = new Error('Client meta data format is not JSON.');
|
|
125
|
-
error.name = 'DataError';
|
|
126
|
-
throw error;
|
|
127
|
-
}
|
|
128
|
-
// FIXME: can `data` be a JWT and require verification as well?
|
|
129
|
-
delete authorizationRequest.client_metadata_uri;
|
|
130
|
-
authorizationRequest.client_metadata = response.data;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// get presentation definition from URL if not embedded
|
|
134
|
-
if(presentation_definition_uri) {
|
|
135
|
-
const response = await fetchJSON(
|
|
136
|
-
{url: presentation_definition_uri, agent});
|
|
137
|
-
if(!response.data) {
|
|
138
|
-
const error = new Error('Presentation definition format is not JSON.');
|
|
139
|
-
error.name = 'DataError';
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
// FIXME: can `data` be a JWT and require verification as well?
|
|
143
|
-
delete authorizationRequest.presentation_definition_uri;
|
|
144
|
-
authorizationRequest.presentation_definition = response.data;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// FIXME: validate `authorizationRequest.presentation_definition`
|
|
148
|
-
|
|
149
|
-
// return merged authorization request and original response
|
|
150
|
-
return {authorizationRequest, fetched: true, requestUrl, response, jwt};
|
|
151
|
-
} catch(cause) {
|
|
152
|
-
const error = new Error('Could not get authorization request.', {cause});
|
|
153
|
-
error.name = 'OperationError';
|
|
154
|
-
throw error;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function sendAuthorizationResponse({
|
|
159
|
-
verifiablePresentation,
|
|
160
|
-
presentationSubmission,
|
|
161
|
-
authorizationRequest,
|
|
162
|
-
vpToken,
|
|
163
|
-
agent
|
|
164
|
-
} = {}) {
|
|
165
|
-
try {
|
|
166
|
-
// if no `presentationSubmission` provided, auto-generate one
|
|
167
|
-
let generatedPresentationSubmission = false;
|
|
168
|
-
if(!presentationSubmission) {
|
|
169
|
-
({presentationSubmission} = createPresentationSubmission({
|
|
170
|
-
presentationDefinition: authorizationRequest.presentation_definition,
|
|
171
|
-
verifiablePresentation
|
|
172
|
-
}));
|
|
173
|
-
generatedPresentationSubmission = true;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// send VP and presentation submission to complete exchange
|
|
177
|
-
const body = new URLSearchParams();
|
|
178
|
-
body.set('vp_token', vpToken ?? JSON.stringify(verifiablePresentation));
|
|
179
|
-
body.set('presentation_submission', JSON.stringify(presentationSubmission));
|
|
180
|
-
const response = await httpClient.post(authorizationRequest.response_uri, {
|
|
181
|
-
agent, body, headers: {accept: 'application/json'},
|
|
182
|
-
// FIXME: limit response size
|
|
183
|
-
// timeout in ms for response
|
|
184
|
-
timeout: 5000
|
|
185
|
-
});
|
|
186
|
-
// return response data as `result`
|
|
187
|
-
const result = response.data || {};
|
|
188
|
-
if(generatedPresentationSubmission) {
|
|
189
|
-
// return any generated presentation submission
|
|
190
|
-
return {result, presentationSubmission};
|
|
191
|
-
}
|
|
192
|
-
return {result};
|
|
193
|
-
} catch(cause) {
|
|
194
|
-
const error = new Error(
|
|
195
|
-
'Could not send OID4VP authorization response.', {cause});
|
|
196
|
-
error.name = 'OperationError';
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// converts an OID4VP authorization request (including its
|
|
202
|
-
// "presentation definition") to a VPR
|
|
203
|
-
export async function toVpr({
|
|
204
|
-
authorizationRequest, strict = false, agent
|
|
205
|
-
} = {}) {
|
|
206
|
-
try {
|
|
207
|
-
const {
|
|
208
|
-
client_id,
|
|
209
|
-
client_metadata_uri,
|
|
210
|
-
nonce,
|
|
211
|
-
presentation_definition_uri,
|
|
212
|
-
} = authorizationRequest;
|
|
213
|
-
let {
|
|
214
|
-
client_metadata,
|
|
215
|
-
presentation_definition
|
|
216
|
-
} = authorizationRequest;
|
|
217
|
-
if(client_metadata && client_metadata_uri) {
|
|
218
|
-
const error = new Error(
|
|
219
|
-
'Only one of "client_metadata" and ' +
|
|
220
|
-
'"client_metadata_uri" must be present.');
|
|
221
|
-
error.name = 'DataError';
|
|
222
|
-
throw error;
|
|
223
|
-
}
|
|
224
|
-
if(presentation_definition && presentation_definition_uri) {
|
|
225
|
-
const error = new Error(
|
|
226
|
-
'Only one of "presentation_definition" and ' +
|
|
227
|
-
'"presentation_definition_uri" must be present.');
|
|
228
|
-
error.name = 'DataError';
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// apply constraints for currently supported subset of AR data
|
|
233
|
-
if(client_metadata_uri) {
|
|
234
|
-
const response = await fetchJSON({url: client_metadata_uri, agent});
|
|
235
|
-
if(!response.data) {
|
|
236
|
-
const error = new Error('Client metadata format is not JSON.');
|
|
237
|
-
error.name = 'DataError';
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
client_metadata = response.data;
|
|
241
|
-
}
|
|
242
|
-
assertOptional(client_metadata, 'client_metadata', 'object');
|
|
243
|
-
if(presentation_definition_uri) {
|
|
244
|
-
const response = await fetchJSON(
|
|
245
|
-
{url: presentation_definition_uri, agent});
|
|
246
|
-
if(!response.data) {
|
|
247
|
-
const error = new Error('Presentation definition format is not JSON.');
|
|
248
|
-
error.name = 'DataError';
|
|
249
|
-
throw error;
|
|
250
|
-
}
|
|
251
|
-
presentation_definition = response.data;
|
|
252
|
-
}
|
|
253
|
-
assert(presentation_definition, 'presentation_definition', 'object');
|
|
254
|
-
assert(presentation_definition?.id, 'presentation_definition.id', 'string');
|
|
255
|
-
if(presentation_definition.submission_requirements && strict) {
|
|
256
|
-
const error = new Error('"submission_requirements" is not supported.');
|
|
257
|
-
error.name = 'NotSupportedError';
|
|
258
|
-
throw error;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// generate base VPR from presentation definition
|
|
262
|
-
const verifiablePresentationRequest = {
|
|
263
|
-
// map each `input_descriptors` value to a `QueryByExample` query
|
|
264
|
-
query: [{
|
|
265
|
-
type: 'QueryByExample',
|
|
266
|
-
credentialQuery: presentation_definition.input_descriptors.map(
|
|
267
|
-
inputDescriptor => _toQueryByExampleQuery({inputDescriptor, strict}))
|
|
268
|
-
}]
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// add `DIDAuthentication` query based on client_metadata
|
|
272
|
-
if(client_metadata) {
|
|
273
|
-
const query = _toDIDAuthenticationQuery({client_metadata, strict});
|
|
274
|
-
if(query !== undefined) {
|
|
275
|
-
verifiablePresentationRequest.query.unshift(query);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// map `client_id` to `domain`
|
|
280
|
-
if(client_id !== undefined) {
|
|
281
|
-
verifiablePresentationRequest.domain = client_id;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// map `nonce` to `challenge`
|
|
285
|
-
if(nonce !== undefined) {
|
|
286
|
-
verifiablePresentationRequest.challenge = nonce;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return {verifiablePresentationRequest};
|
|
290
|
-
} catch(cause) {
|
|
291
|
-
const error = new Error(
|
|
292
|
-
'Could not convert OID4VP authorization request to ' +
|
|
293
|
-
'verifiable presentation request.', {cause});
|
|
294
|
-
error.name = 'OperationError';
|
|
295
|
-
throw error;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// converts a VPR to partial "authorization request"
|
|
300
|
-
export function fromVpr({
|
|
301
|
-
verifiablePresentationRequest, strict = false, prefixJwtVcPath
|
|
302
|
-
} = {}) {
|
|
303
|
-
try {
|
|
304
|
-
let {query} = verifiablePresentationRequest;
|
|
305
|
-
if(!Array.isArray(query)) {
|
|
306
|
-
query = [query];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// convert any `QueryByExample` queries
|
|
310
|
-
const queryByExample = query.filter(({type}) => type === 'QueryByExample');
|
|
311
|
-
let credentialQuery = [];
|
|
312
|
-
if(queryByExample.length > 0) {
|
|
313
|
-
if(queryByExample.length > 1 && strict) {
|
|
314
|
-
const error = new Error(
|
|
315
|
-
'Multiple "QueryByExample" VPR queries are not supported.');
|
|
316
|
-
error.name = 'NotSupportedError';
|
|
317
|
-
throw error;
|
|
318
|
-
}
|
|
319
|
-
([{credentialQuery = []}] = queryByExample);
|
|
320
|
-
if(!Array.isArray(credentialQuery)) {
|
|
321
|
-
credentialQuery = [credentialQuery];
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const authorizationRequest = {
|
|
325
|
-
response_type: 'vp_token',
|
|
326
|
-
presentation_definition: {
|
|
327
|
-
id: uuid(),
|
|
328
|
-
input_descriptors: credentialQuery.map(q => _fromQueryByExampleQuery({
|
|
329
|
-
credentialQuery: q,
|
|
330
|
-
prefixJwtVcPath
|
|
331
|
-
}))
|
|
332
|
-
},
|
|
333
|
-
response_mode: 'direct_post'
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
// convert any `DIDAuthentication` queries
|
|
337
|
-
const didAuthnQuery = query.filter(
|
|
338
|
-
({type}) => type === 'DIDAuthentication');
|
|
339
|
-
if(didAuthnQuery.length > 0) {
|
|
340
|
-
if(didAuthnQuery.length > 1 && strict) {
|
|
341
|
-
const error = new Error(
|
|
342
|
-
'Multiple "DIDAuthentication" VPR queries are not supported.');
|
|
343
|
-
error.name = 'NotSupportedError';
|
|
344
|
-
throw error;
|
|
345
|
-
}
|
|
346
|
-
const [query] = didAuthnQuery;
|
|
347
|
-
const client_metadata = _fromDIDAuthenticationQuery({query, strict});
|
|
348
|
-
authorizationRequest.client_metadata = client_metadata;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if(queryByExample.length === 0 && didAuthnQuery.length === 0 && strict) {
|
|
352
|
-
const error = new Error(
|
|
353
|
-
'Only "DIDAuthentication" and "QueryByExample" VPR queries are ' +
|
|
354
|
-
'supported.');
|
|
355
|
-
error.name = 'NotSupportedError';
|
|
356
|
-
throw error;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// include requested authn params
|
|
360
|
-
if(verifiablePresentationRequest.domain) {
|
|
361
|
-
// `authorizationRequest` uses `direct_post` so force client ID to
|
|
362
|
-
// be the exchange response URL per "Note" here:
|
|
363
|
-
// eslint-disable-next-line max-len
|
|
364
|
-
// https://openid.github.io/OpenID4VP/openid-4-verifiable-presentations-wg-draft.html#section-6.2
|
|
365
|
-
authorizationRequest.client_id = verifiablePresentationRequest.domain;
|
|
366
|
-
authorizationRequest.client_id_scheme = 'redirect_uri';
|
|
367
|
-
authorizationRequest.response_uri = authorizationRequest.client_id;
|
|
368
|
-
}
|
|
369
|
-
if(verifiablePresentationRequest.challenge) {
|
|
370
|
-
authorizationRequest.nonce = verifiablePresentationRequest.challenge;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return authorizationRequest;
|
|
374
|
-
} catch(cause) {
|
|
375
|
-
const error = new Error(
|
|
376
|
-
'Could not convert verifiable presentation request to ' +
|
|
377
|
-
'an OID4VP authorization request.', {cause});
|
|
378
|
-
error.name = 'OperationError';
|
|
379
|
-
throw error;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// creates a "presentation submission" from a presentation definition and VP
|
|
384
|
-
export function createPresentationSubmission({
|
|
385
|
-
presentationDefinition, verifiablePresentation
|
|
386
|
-
} = {}) {
|
|
387
|
-
const descriptor_map = [];
|
|
388
|
-
const presentationSubmission = {
|
|
389
|
-
id: uuid(),
|
|
390
|
-
definition_id: presentationDefinition.id,
|
|
391
|
-
descriptor_map
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
// walk through each input descriptor object and match it to a VC
|
|
396
|
-
let {verifiableCredential: vcs} = verifiablePresentation;
|
|
397
|
-
const single = !Array.isArray(vcs);
|
|
398
|
-
if(single) {
|
|
399
|
-
vcs = [vcs];
|
|
400
|
-
}
|
|
401
|
-
/* Note: It is conceivable that the same VC could match multiple input
|
|
402
|
-
descriptors. In this simplistic implementation, the first VC that matches
|
|
403
|
-
is used. This may result in VCs in the VP not being mapped to an input
|
|
404
|
-
descriptor, but every input descriptor having a VC that matches (i.e., at
|
|
405
|
-
least one VC will be shared across multiple input descriptors). If
|
|
406
|
-
some other behavior is more desirable, this can be changed in a future
|
|
407
|
-
version. */
|
|
408
|
-
for(const inputDescriptor of presentationDefinition.input_descriptors) {
|
|
409
|
-
// walk through each VC and try to match it to the input descriptor
|
|
410
|
-
for(let i = 0; i < vcs.length; ++i) {
|
|
411
|
-
const verifiableCredential = vcs[i];
|
|
412
|
-
if(_matchesInputDescriptor({inputDescriptor, verifiableCredential})) {
|
|
413
|
-
descriptor_map.push({
|
|
414
|
-
id: inputDescriptor.id,
|
|
415
|
-
path: '$',
|
|
416
|
-
format: 'ldp_vp',
|
|
417
|
-
path_nested: {
|
|
418
|
-
format: 'ldp_vc',
|
|
419
|
-
path: single ?
|
|
420
|
-
'$.verifiableCredential' :
|
|
421
|
-
'$.verifiableCredential[' + i + ']'
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
} catch(cause) {
|
|
429
|
-
const error = new Error(
|
|
430
|
-
'Could not create presentation submission.', {cause});
|
|
431
|
-
error.name = 'OperationError';
|
|
432
|
-
throw error;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return {presentationSubmission};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function _filterToValue({filter, strict = false}) {
|
|
439
|
-
/* Each `filter` has a JSON Schema object. In recognition of the fact that
|
|
440
|
-
a query must be usable by common database engines (including perhaps
|
|
441
|
-
encrypted cloud databases) and of the fact that each JSON Schema object will
|
|
442
|
-
come from an untrusted source (and could have malicious regexes, etc.), only
|
|
443
|
-
simple JSON Schema types are supported:
|
|
444
|
-
|
|
445
|
-
`string`: with `const` or `enum`, `format` is not supported and `pattern` has
|
|
446
|
-
partial support as it will be treated as a simple string not a regex; regex
|
|
447
|
-
is a DoS attack vector
|
|
448
|
-
|
|
449
|
-
`array`: with `contains` where uses a `string` filter
|
|
450
|
-
|
|
451
|
-
`allOf`: supported only with the above schemas present in it.
|
|
452
|
-
|
|
453
|
-
*/
|
|
454
|
-
let value;
|
|
455
|
-
|
|
456
|
-
const {type} = filter;
|
|
457
|
-
if(type === 'array') {
|
|
458
|
-
if(filter.contains) {
|
|
459
|
-
if(Array.isArray(filter.contains)) {
|
|
460
|
-
return filter.contains.map(filter => _filterToValue({filter, strict}));
|
|
461
|
-
}
|
|
462
|
-
return _filterToValue({filter: filter.contains, strict});
|
|
463
|
-
}
|
|
464
|
-
if(Array.isArray(filter.allOf) && filter.allOf.every(f => f.contains)) {
|
|
465
|
-
return filter.allOf.map(
|
|
466
|
-
f => _filterToValue({filter: f.contains, strict}));
|
|
467
|
-
}
|
|
468
|
-
if(strict) {
|
|
469
|
-
throw new Error(
|
|
470
|
-
'Unsupported filter; array filters must use "allOf" and/or ' +
|
|
471
|
-
'"contains" with a string filter.');
|
|
472
|
-
}
|
|
473
|
-
return value;
|
|
474
|
-
}
|
|
475
|
-
if(type === 'string' || type === undefined) {
|
|
476
|
-
if(filter.const !== undefined) {
|
|
477
|
-
value = filter.const;
|
|
478
|
-
} else if(filter.pattern) {
|
|
479
|
-
value = filter.pattern;
|
|
480
|
-
} else if(filter.enum) {
|
|
481
|
-
value = filter.enum.slice();
|
|
482
|
-
} else if(strict) {
|
|
483
|
-
throw new Error(
|
|
484
|
-
'Unsupported filter; string filters must use "const" or "pattern".');
|
|
485
|
-
}
|
|
486
|
-
return value;
|
|
487
|
-
}
|
|
488
|
-
if(strict) {
|
|
489
|
-
throw new Error(`Unsupported filter type "${type}".`);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function _jsonPathToJsonPointer(jsonPath) {
|
|
494
|
-
return JSONPath.toPointer(JSONPath.toPathArray(jsonPath));
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function _matchesInputDescriptor({
|
|
498
|
-
inputDescriptor, verifiableCredential, strict = false
|
|
499
|
-
}) {
|
|
500
|
-
// walk through each field ensuring there is a matching value
|
|
501
|
-
const fields = inputDescriptor?.constraints?.fields || [];
|
|
502
|
-
for(const field of fields) {
|
|
503
|
-
const {path, filter, optional} = field;
|
|
504
|
-
if(optional) {
|
|
505
|
-
// skip field, it is optional
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
// each field must have a `path` (which can be a string or an array)
|
|
511
|
-
if(!(Array.isArray(path) || typeof path === 'string')) {
|
|
512
|
-
throw new Error(
|
|
513
|
-
'Input descriptor field "path" must be a string or array.');
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// process any filter
|
|
517
|
-
let value = '';
|
|
518
|
-
if(filter !== undefined) {
|
|
519
|
-
value = _filterToValue({filter, strict});
|
|
520
|
-
}
|
|
521
|
-
// no value to match, presume no match
|
|
522
|
-
if(value === undefined) {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
// normalize value to array
|
|
526
|
-
if(!Array.isArray(value)) {
|
|
527
|
-
value = [value];
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// filter out erroneous paths
|
|
531
|
-
let paths = Array.isArray(path) ? path : [path];
|
|
532
|
-
paths = _adjustErroneousPaths(paths);
|
|
533
|
-
// convert each JSON path to a JSON pointer
|
|
534
|
-
const pointers = paths.map(_jsonPathToJsonPointer);
|
|
535
|
-
|
|
536
|
-
// check for a value at at least one path
|
|
537
|
-
for(const pointer of pointers) {
|
|
538
|
-
const existing = jsonpointer.get(verifiableCredential, pointer);
|
|
539
|
-
if(existing === undefined) {
|
|
540
|
-
// VC does not match
|
|
541
|
-
return false;
|
|
542
|
-
}
|
|
543
|
-
// look for at least one matching value in `existing`
|
|
544
|
-
let match = false;
|
|
545
|
-
for(const v of value) {
|
|
546
|
-
if(Array.isArray(existing)) {
|
|
547
|
-
if(existing.includes(v)) {
|
|
548
|
-
match = true;
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
} else if(existing === v) {
|
|
552
|
-
match = true;
|
|
553
|
-
break;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
if(!match) {
|
|
557
|
-
return false;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
} catch(cause) {
|
|
561
|
-
const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
|
|
562
|
-
const error = new Error(
|
|
563
|
-
`Could not process input descriptor field: "${id}".`, {cause});
|
|
564
|
-
error.field = field;
|
|
565
|
-
throw error;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
return true;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// exported for testing purposes only
|
|
573
|
-
export function _fromQueryByExampleQuery({credentialQuery, prefixJwtVcPath}) {
|
|
574
|
-
// determine `prefixJwtVcPath` default:
|
|
575
|
-
// if `credentialQuery` specifies `acceptedEnvelopes: ['application/jwt']`,
|
|
576
|
-
// then default `prefixJwtVcPath` to `true`
|
|
577
|
-
if(prefixJwtVcPath === undefined &&
|
|
578
|
-
(Array.isArray(credentialQuery.acceptedEnvelopes) &&
|
|
579
|
-
credentialQuery.acceptedEnvelopes.includes?.('application/jwt'))) {
|
|
580
|
-
prefixJwtVcPath = true;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const fields = [];
|
|
584
|
-
const inputDescriptor = {
|
|
585
|
-
id: uuid(),
|
|
586
|
-
constraints: {fields}
|
|
587
|
-
};
|
|
588
|
-
if(credentialQuery?.reason) {
|
|
589
|
-
inputDescriptor.purpose = credentialQuery?.reason;
|
|
590
|
-
}
|
|
591
|
-
// FIXME: current implementation only supports top-level string/array
|
|
592
|
-
// properties and presumes strings
|
|
593
|
-
const path = ['$'];
|
|
594
|
-
const {example = {}} = credentialQuery || {};
|
|
595
|
-
for(const key in example) {
|
|
596
|
-
const value = example[key];
|
|
597
|
-
path.push(key);
|
|
598
|
-
|
|
599
|
-
const filter = {};
|
|
600
|
-
if(Array.isArray(value)) {
|
|
601
|
-
filter.type = 'array';
|
|
602
|
-
filter.allOf = value.map(v => ({
|
|
603
|
-
contains: {
|
|
604
|
-
type: 'string',
|
|
605
|
-
const: v
|
|
606
|
-
}
|
|
607
|
-
}));
|
|
608
|
-
} else if(key === 'type') {
|
|
609
|
-
// special provision for array/string for `type`
|
|
610
|
-
filter.type = 'array',
|
|
611
|
-
filter.contains = {
|
|
612
|
-
type: 'string',
|
|
613
|
-
const: value
|
|
614
|
-
};
|
|
615
|
-
} else {
|
|
616
|
-
filter.type = 'string',
|
|
617
|
-
filter.const = value;
|
|
618
|
-
}
|
|
619
|
-
const fieldsPath = [JSONPath.toPathString(path)];
|
|
620
|
-
// include 'vc' path for queries against JWT payloads instead of VCs
|
|
621
|
-
if(prefixJwtVcPath) {
|
|
622
|
-
const vcPath = [...path];
|
|
623
|
-
vcPath.splice(1, 0, 'vc');
|
|
624
|
-
fieldsPath.push(JSONPath.toPathString(vcPath));
|
|
625
|
-
}
|
|
626
|
-
fields.push({
|
|
627
|
-
path: fieldsPath,
|
|
628
|
-
filter
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
path.pop();
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return inputDescriptor;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function _toDIDAuthenticationQuery({client_metadata, strict = false}) {
|
|
638
|
-
const {vp_formats} = client_metadata;
|
|
639
|
-
const proofTypes = vp_formats?.ldp_vp?.proof_type;
|
|
640
|
-
if(!Array.isArray(proofTypes)) {
|
|
641
|
-
if(strict) {
|
|
642
|
-
const error = new Error(
|
|
643
|
-
'"client_metadata.vp_formats.ldp_vp.proof_type" must be an array to ' +
|
|
644
|
-
'convert to DIDAuthentication query.');
|
|
645
|
-
error.name = 'NotSupportedError';
|
|
646
|
-
throw error;
|
|
647
|
-
}
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
return {
|
|
651
|
-
type: 'DIDAuthentication',
|
|
652
|
-
acceptedCryptosuites: proofTypes.map(cryptosuite => ({cryptosuite}))
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function _fromDIDAuthenticationQuery({query, strict = false}) {
|
|
657
|
-
const cryptosuites = query.acceptedCryptosuites?.map(
|
|
658
|
-
({cryptosuite}) => cryptosuite);
|
|
659
|
-
if(!(cryptosuites && cryptosuites.length > 0)) {
|
|
660
|
-
if(strict) {
|
|
661
|
-
const error = new Error(
|
|
662
|
-
'"query.acceptedCryptosuites" must be a non-array with specified ' +
|
|
663
|
-
'cryptosuites to convert from a DIDAuthentication query.');
|
|
664
|
-
error.name = 'NotSupportedError';
|
|
665
|
-
throw error;
|
|
666
|
-
}
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
return {
|
|
670
|
-
require_signed_request_object: false,
|
|
671
|
-
vp_formats: {
|
|
672
|
-
ldp_vp: {
|
|
673
|
-
proof_type: cryptosuites
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function _toQueryByExampleQuery({inputDescriptor, strict = false}) {
|
|
680
|
-
// every input descriptor must have an `id`
|
|
681
|
-
if(typeof inputDescriptor?.id !== 'string') {
|
|
682
|
-
throw new TypeError('Input descriptor "id" must be a string.');
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const example = {};
|
|
686
|
-
const credentialQuery = {example};
|
|
687
|
-
if(inputDescriptor.purpose) {
|
|
688
|
-
credentialQuery.reason = inputDescriptor.purpose;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/* Note: Each input descriptor object is currently mapped to a single example
|
|
692
|
-
query. If multiple possible path values appear for a single field, these will
|
|
693
|
-
be mapped to multiple properties in the example which may or may not be what
|
|
694
|
-
is intended. This behavior could be changed in a future revision if it
|
|
695
|
-
becomes clear there is a better approach. */
|
|
696
|
-
|
|
697
|
-
const fields = inputDescriptor.constraints?.fields || [];
|
|
698
|
-
for(const field of fields) {
|
|
699
|
-
const {path, filter, optional} = field;
|
|
700
|
-
// skip optional fields
|
|
701
|
-
if(optional === true) {
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
// each field must have a `path` (which can be a string or an array)
|
|
707
|
-
if(!(Array.isArray(path) || typeof path === 'string')) {
|
|
708
|
-
throw new TypeError(
|
|
709
|
-
'Input descriptor field "path" must be a string or array.');
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// process any filter
|
|
713
|
-
let value = '';
|
|
714
|
-
if(filter !== undefined) {
|
|
715
|
-
value = _filterToValue({filter, strict});
|
|
716
|
-
}
|
|
717
|
-
// no value understood, skip field
|
|
718
|
-
if(value === undefined) {
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
// normalize value to array
|
|
722
|
-
if(!Array.isArray(value)) {
|
|
723
|
-
value = [value];
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// filter out erroneous paths
|
|
727
|
-
let paths = Array.isArray(path) ? path : [path];
|
|
728
|
-
paths = _adjustErroneousPaths(paths);
|
|
729
|
-
// convert each JSON path to a JSON pointer
|
|
730
|
-
const pointers = paths.map(_jsonPathToJsonPointer);
|
|
731
|
-
|
|
732
|
-
// add values at each path, converting to an array / appending as needed
|
|
733
|
-
for(const pointer of pointers) {
|
|
734
|
-
const existing = jsonpointer.get(example, pointer);
|
|
735
|
-
if(existing === undefined) {
|
|
736
|
-
jsonpointer.set(
|
|
737
|
-
example, pointer, value.length > 1 ? value : value[0]);
|
|
738
|
-
} else if(Array.isArray(existing)) {
|
|
739
|
-
if(!existing.includes(value)) {
|
|
740
|
-
existing.push(...value);
|
|
741
|
-
}
|
|
742
|
-
} else if(existing !== value) {
|
|
743
|
-
jsonpointer.set(example, pointer, [existing, ...value]);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
} catch(cause) {
|
|
747
|
-
const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
|
|
748
|
-
const error = new Error(
|
|
749
|
-
`Could not process input descriptor field: "${id}".`, {cause});
|
|
750
|
-
error.field = field;
|
|
751
|
-
throw error;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
return credentialQuery;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function _adjustErroneousPaths(paths) {
|
|
759
|
-
// remove any paths that start with what would be present in a
|
|
760
|
-
// presentation submission and adjust any paths that would be part of a
|
|
761
|
-
// JWT-secured VC, such that only actual VC paths remain
|
|
762
|
-
const removed = paths.filter(p => !_isPresentationSubmissionPath(p));
|
|
763
|
-
return [...new Set(removed.map(p => {
|
|
764
|
-
if(_isJWTPath(p)) {
|
|
765
|
-
return '$' + p.slice('$.vc'.length);
|
|
766
|
-
}
|
|
767
|
-
if(_isSquareJWTPath(p)) {
|
|
768
|
-
return '$' + p.slice('$[\'vc\']'.length);
|
|
769
|
-
}
|
|
770
|
-
return p;
|
|
771
|
-
}))];
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function _parseOID4VPUrl({url}) {
|
|
775
|
-
const {searchParams} = new URL(url);
|
|
776
|
-
const request = _get(searchParams, 'request');
|
|
777
|
-
const request_uri = _get(searchParams, 'request_uri');
|
|
778
|
-
const response_type = _get(searchParams, 'response_type');
|
|
779
|
-
const response_mode = _get(searchParams, 'response_mode');
|
|
780
|
-
const presentation_definition = _get(
|
|
781
|
-
searchParams, 'presentation_definition');
|
|
782
|
-
const presentation_definition_uri = _get(
|
|
783
|
-
searchParams, 'presentation_definition_uri');
|
|
784
|
-
const client_id = _get(searchParams, 'client_id');
|
|
785
|
-
const client_id_scheme = _get(searchParams, 'client_id_scheme');
|
|
786
|
-
const client_metadata = _get(searchParams, 'client_metadata');
|
|
787
|
-
const nonce = _get(searchParams, 'nonce');
|
|
788
|
-
const response_uri = _get(searchParams, 'response_uri');
|
|
789
|
-
const state = _get(searchParams, 'state');
|
|
790
|
-
if(request && request_uri) {
|
|
791
|
-
const error = new Error(
|
|
792
|
-
'Only one of "request" and "request_uri" may be present.');
|
|
793
|
-
error.name = 'DataError';
|
|
794
|
-
error.url = url;
|
|
795
|
-
throw error;
|
|
796
|
-
}
|
|
797
|
-
if(!(request || request_uri)) {
|
|
798
|
-
if(response_type !== 'vp_token') {
|
|
799
|
-
throw new Error(`Unsupported "response_type", "${response_type}".`);
|
|
800
|
-
}
|
|
801
|
-
if(response_mode !== 'direct_post') {
|
|
802
|
-
throw new Error(`Unsupported "response_type", "${response_type}".`);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
const authorizationRequest = {
|
|
806
|
-
request,
|
|
807
|
-
request_uri,
|
|
808
|
-
response_type,
|
|
809
|
-
response_mode,
|
|
810
|
-
presentation_definition: presentation_definition &&
|
|
811
|
-
JSON.parse(presentation_definition),
|
|
812
|
-
presentation_definition_uri,
|
|
813
|
-
client_id,
|
|
814
|
-
client_id_scheme,
|
|
815
|
-
client_metadata: client_metadata && JSON.parse(client_metadata),
|
|
816
|
-
response_uri,
|
|
817
|
-
nonce,
|
|
818
|
-
state
|
|
819
|
-
};
|
|
820
|
-
return {authorizationRequest};
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function _get(sp, name) {
|
|
824
|
-
const value = sp.get(name);
|
|
825
|
-
return value === null ? undefined : value;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function _isPresentationSubmissionPath(path) {
|
|
829
|
-
return path.startsWith('$.verifiableCredential[') ||
|
|
830
|
-
path.startsWith('$.vp.') ||
|
|
831
|
-
path.startsWith('$[\'verifiableCredential') || path.startsWith('$[\'vp');
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function _isJWTPath(path) {
|
|
835
|
-
return path.startsWith('$.vc.');
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function _isSquareJWTPath(path) {
|
|
839
|
-
return path.startsWith('$[\'vc\']');
|
|
840
|
-
}
|