@digitalbazaar/oid4-client 3.0.0 → 3.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/README.md +141 -2
- package/lib/OID4Client.js +16 -15
- package/lib/index.js +7 -1
- package/lib/oid4vp.js +797 -0
- package/lib/util.js +53 -23
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,2 +1,141 @@
|
|
|
1
|
-
# oid4-client
|
|
2
|
-
|
|
1
|
+
# OID4Client Library _(@digitalbazaar/oid4-client)_
|
|
2
|
+
|
|
3
|
+
[](https://npm.im/@digitalbazaar/oid4-client)
|
|
4
|
+
|
|
5
|
+
A JavaScript library for working with the OpenID 4 Verifiable Credential
|
|
6
|
+
Issuance (OID4VCI) protocol, offering functionality for requesting Verifiable
|
|
7
|
+
Credentials.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Background](#background)
|
|
12
|
+
- [Install](#install)
|
|
13
|
+
- [Usage](#usage)
|
|
14
|
+
- [Testing](#testing)
|
|
15
|
+
- [Contribute](#contribute)
|
|
16
|
+
- [Commercial Support](#commercial-support)
|
|
17
|
+
- [License](#license)
|
|
18
|
+
|
|
19
|
+
## Background
|
|
20
|
+
|
|
21
|
+
This library is a JavaScript (Node.js and browser) implementation of the
|
|
22
|
+
[OID4VCI v11](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
|
|
23
|
+
Protocol.
|
|
24
|
+
|
|
25
|
+
It allows you to perform the following operations:
|
|
26
|
+
|
|
27
|
+
1. Request a credential to be issued given a OID4VCI credential offer.
|
|
28
|
+
2. Request multiple credentials to be issued given a OID4VCI credential offer.
|
|
29
|
+
3. Authenticate using a DID if the offer requires it.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @digitalbazaar/oid4-client
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Importing the Library
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import { OID4Client } from "@digitalbazaar/oid4-client";
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Creating a Client from a Credential Offer
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
const clientFromOffer = await OID4Client.fromCredentialOffer({
|
|
50
|
+
offer: "YOUR_CREDENTIAL_OFFER",
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Constructor
|
|
55
|
+
|
|
56
|
+
You can also instantiate the `OID4Client` directly using the following parameters:
|
|
57
|
+
|
|
58
|
+
- `accessToken` (Optional)
|
|
59
|
+
- `issuerConfig`
|
|
60
|
+
- `metadata`
|
|
61
|
+
- `offer`
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
const client = new OID4Client({
|
|
67
|
+
accessToken: "YOUR_ACCESS_TOKEN",
|
|
68
|
+
issuerConfig: "YOUR_ISSUER_CONFIG",
|
|
69
|
+
metadata: "YOUR_METADATA",
|
|
70
|
+
offer: "YOUR_OFFER",
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Requesting a Credential
|
|
75
|
+
|
|
76
|
+
To request a single credential using the credential offer:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
const credential = await client.requestCredential({
|
|
80
|
+
did: "YOUR_DID",
|
|
81
|
+
didProofSigner: "YOUR_DID_PROOF_SIGNER",
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
To request multiple credentials using the credential offer:
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
const credentials = await client.requestCredentials({
|
|
89
|
+
did: "YOUR_DID",
|
|
90
|
+
didProofSigner: "YOUR_DID_PROOF_SIGNER",
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Requesting a Credential By Definition
|
|
95
|
+
|
|
96
|
+
To request a single credential using a specific credential definition:
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
const credential = await client.requestCredential({
|
|
100
|
+
credentialDefinition: "YOUR_CREDENTIAL_DEFINITION",
|
|
101
|
+
did: "YOUR_DID",
|
|
102
|
+
didProofSigner: "YOUR_DID_PROOF_SIGNER",
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
To request multiple credentials using credential definition requests:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
const credentials = await client.requestCredentials({
|
|
110
|
+
requests: "YOUR_REQUESTS",
|
|
111
|
+
did: "YOUR_DID",
|
|
112
|
+
didProofSigner: "YOUR_DID_PROOF_SIGNER",
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Testing
|
|
117
|
+
|
|
118
|
+
To run tests:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
npm run test
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Contribute
|
|
125
|
+
|
|
126
|
+
See
|
|
127
|
+
[the contribute file](https://github.com/digitalbazaar/bedrock/blob/master/CONTRIBUTING.md)!
|
|
128
|
+
|
|
129
|
+
PRs accepted.
|
|
130
|
+
|
|
131
|
+
Note: If editing the Readme, please conform to the
|
|
132
|
+
[standard-readme](https://github.com/RichardLitt/standard-readme) specification.
|
|
133
|
+
|
|
134
|
+
## Commercial Support
|
|
135
|
+
|
|
136
|
+
Commercial support for this library is available upon request from Digital
|
|
137
|
+
Bazaar: support@digitalbazaar.com
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
[New BSD License (3-clause)](LICENSE) © Digital Bazaar
|
package/lib/OID4Client.js
CHANGED
|
@@ -10,9 +10,10 @@ const GRANT_TYPES = new Map([
|
|
|
10
10
|
const HEADERS = {accept: 'application/json'};
|
|
11
11
|
|
|
12
12
|
export class OID4Client {
|
|
13
|
-
constructor({accessToken = null, agent, issuerConfig, offer} = {}) {
|
|
13
|
+
constructor({accessToken = null, agent, issuerConfig, metadata, offer} = {}) {
|
|
14
14
|
this.accessToken = accessToken;
|
|
15
15
|
this.agent = agent;
|
|
16
|
+
this.metadata = metadata;
|
|
16
17
|
this.issuerConfig = issuerConfig;
|
|
17
18
|
this.offer = offer;
|
|
18
19
|
}
|
|
@@ -127,9 +128,8 @@ export class OID4Client {
|
|
|
127
128
|
// if `didProofSigner` is not provided, throw error
|
|
128
129
|
if(!(did && didProofSigner)) {
|
|
129
130
|
const {data: details} = cause;
|
|
130
|
-
const error = new Error('DID authentication is required.');
|
|
131
|
+
const error = new Error('DID authentication is required.', {cause});
|
|
131
132
|
error.name = 'NotAllowedError';
|
|
132
|
-
error.cause = cause;
|
|
133
133
|
error.details = details;
|
|
134
134
|
throw error;
|
|
135
135
|
}
|
|
@@ -187,9 +187,8 @@ export class OID4Client {
|
|
|
187
187
|
*/
|
|
188
188
|
return result;
|
|
189
189
|
} catch(cause) {
|
|
190
|
-
const error = new Error('Could not receive credentials.');
|
|
190
|
+
const error = new Error('Could not receive credentials.', {cause});
|
|
191
191
|
error.name = 'OperationError';
|
|
192
|
-
error.cause = cause;
|
|
193
192
|
throw error;
|
|
194
193
|
}
|
|
195
194
|
}
|
|
@@ -204,10 +203,8 @@ export class OID4Client {
|
|
|
204
203
|
if(parsedIssuer.protocol !== 'https:') {
|
|
205
204
|
throw new Error('Only "https" credential issuer URLs are supported.');
|
|
206
205
|
}
|
|
207
|
-
} catch(
|
|
208
|
-
|
|
209
|
-
err.cause = e;
|
|
210
|
-
throw err;
|
|
206
|
+
} catch(cause) {
|
|
207
|
+
throw new Error('"offer.credential_issuer" is not valid.', {cause});
|
|
211
208
|
}
|
|
212
209
|
if(!(Array.isArray(credentials) && credentials.length > 0 &&
|
|
213
210
|
credentials.every(c => c && typeof c === 'object'))) {
|
|
@@ -234,7 +231,8 @@ export class OID4Client {
|
|
|
234
231
|
const issuerConfigUrl =
|
|
235
232
|
`${parsedIssuer.origin}/.well-known/openid-credential-issuer` +
|
|
236
233
|
parsedIssuer.pathname;
|
|
237
|
-
const issuerConfig = await discoverIssuer(
|
|
234
|
+
const {issuerConfig, metadata} = await discoverIssuer(
|
|
235
|
+
{issuerConfigUrl, agent});
|
|
238
236
|
|
|
239
237
|
/* First get access token from AS (Authorization Server), e.g.:
|
|
240
238
|
|
|
@@ -298,11 +296,11 @@ export class OID4Client {
|
|
|
298
296
|
}
|
|
299
297
|
|
|
300
298
|
// create client w/access token
|
|
301
|
-
return new OID4Client(
|
|
299
|
+
return new OID4Client(
|
|
300
|
+
{accessToken, agent, issuerConfig, metadata, offer});
|
|
302
301
|
} catch(cause) {
|
|
303
|
-
const error = new Error('Could not create OID4 client.');
|
|
302
|
+
const error = new Error('Could not create OID4 client.', {cause});
|
|
304
303
|
error.name = 'OperationError';
|
|
305
|
-
error.cause = cause;
|
|
306
304
|
throw error;
|
|
307
305
|
}
|
|
308
306
|
}
|
|
@@ -339,15 +337,18 @@ function _isMissingProofError(error) {
|
|
|
339
337
|
Cache-Control: no-store
|
|
340
338
|
|
|
341
339
|
{
|
|
342
|
-
"error": "invalid_or_missing_proof"
|
|
340
|
+
"error": "invalid_or_missing_proof" // or "invalid_proof"
|
|
343
341
|
"error_description":
|
|
344
342
|
"Credential issuer requires proof element in Credential Request"
|
|
345
343
|
"c_nonce": "8YE9hCnyV2",
|
|
346
344
|
"c_nonce_expires_in": 86400
|
|
347
345
|
}
|
|
348
346
|
*/
|
|
347
|
+
// `invalid_proof` OID4VCI draft 13+, `invalid_or_missing_proof` earlier
|
|
348
|
+
const errorType = error.data?.error;
|
|
349
349
|
return error.status === 400 &&
|
|
350
|
-
|
|
350
|
+
(errorType === 'invalid_proof' ||
|
|
351
|
+
errorType === 'invalid_or_missing_proof');
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
function _createCredentialRequestFromId({id, issuerConfig}) {
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
export * from './
|
|
4
|
+
export * as oid4vp from './oid4vp.js';
|
|
5
|
+
export {
|
|
6
|
+
discoverIssuer,
|
|
7
|
+
generateDIDProofJWT,
|
|
8
|
+
parseCredentialOfferUrl,
|
|
9
|
+
signJWT
|
|
10
|
+
} from './util.js';
|
|
5
11
|
export {OID4Client} from './OID4Client.js';
|
package/lib/oid4vp.js
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {assert, assertOptional, fetchJSON} from './util.js';
|
|
5
|
+
import {decodeJwt} from 'jose';
|
|
6
|
+
import {httpClient} from '@digitalbazaar/http-client';
|
|
7
|
+
import {JSONPath} from 'jsonpath-plus';
|
|
8
|
+
import jsonpointer from 'jsonpointer';
|
|
9
|
+
import {v4 as uuid} from 'uuid';
|
|
10
|
+
|
|
11
|
+
// For examples of presentation request and responses, see:
|
|
12
|
+
// eslint-disable-next-line max-len
|
|
13
|
+
// 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
|
+
agent
|
|
163
|
+
} = {}) {
|
|
164
|
+
try {
|
|
165
|
+
// if no `presentationSubmission` provided, auto-generate one
|
|
166
|
+
let generatedPresentationSubmission = false;
|
|
167
|
+
if(!presentationSubmission) {
|
|
168
|
+
({presentationSubmission} = createPresentationSubmission({
|
|
169
|
+
presentationDefinition: authorizationRequest.presentation_definition,
|
|
170
|
+
verifiablePresentation
|
|
171
|
+
}));
|
|
172
|
+
generatedPresentationSubmission = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// send VP and presentation submission to complete exchange
|
|
176
|
+
const body = new URLSearchParams();
|
|
177
|
+
body.set('vp_token', JSON.stringify(verifiablePresentation));
|
|
178
|
+
body.set('presentation_submission', JSON.stringify(presentationSubmission));
|
|
179
|
+
const response = await httpClient.post(authorizationRequest.response_uri, {
|
|
180
|
+
agent, body, headers: {accept: 'application/json'},
|
|
181
|
+
// FIXME: limit response size
|
|
182
|
+
// timeout in ms for response
|
|
183
|
+
timeout: 5000
|
|
184
|
+
});
|
|
185
|
+
// return response data as `result`
|
|
186
|
+
const result = response.data || {};
|
|
187
|
+
if(generatedPresentationSubmission) {
|
|
188
|
+
// return any generated presentation submission
|
|
189
|
+
return {result, presentationSubmission};
|
|
190
|
+
}
|
|
191
|
+
return {result};
|
|
192
|
+
} catch(cause) {
|
|
193
|
+
const error = new Error(
|
|
194
|
+
'Could not send OID4VP authorization response.', {cause});
|
|
195
|
+
error.name = 'OperationError';
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// converts an OID4VP authorization request (including its
|
|
201
|
+
// "presentation definition") to a VPR
|
|
202
|
+
export async function toVpr({
|
|
203
|
+
authorizationRequest, strict = false, agent
|
|
204
|
+
} = {}) {
|
|
205
|
+
try {
|
|
206
|
+
const {
|
|
207
|
+
client_id,
|
|
208
|
+
client_metadata_uri,
|
|
209
|
+
nonce,
|
|
210
|
+
presentation_definition_uri,
|
|
211
|
+
} = authorizationRequest;
|
|
212
|
+
let {
|
|
213
|
+
client_metadata,
|
|
214
|
+
presentation_definition
|
|
215
|
+
} = authorizationRequest;
|
|
216
|
+
if(client_metadata && client_metadata_uri) {
|
|
217
|
+
const error = new Error(
|
|
218
|
+
'Only one of "client_metadata" and ' +
|
|
219
|
+
'"client_metadata_uri" must be present.');
|
|
220
|
+
error.name = 'DataError';
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
if(presentation_definition && presentation_definition_uri) {
|
|
224
|
+
const error = new Error(
|
|
225
|
+
'Only one of "presentation_definition" and ' +
|
|
226
|
+
'"presentation_definition_uri" must be present.');
|
|
227
|
+
error.name = 'DataError';
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// apply constraints for currently supported subset of AR data
|
|
232
|
+
if(client_metadata_uri) {
|
|
233
|
+
const response = await fetchJSON({url: client_metadata_uri, agent});
|
|
234
|
+
if(!response.data) {
|
|
235
|
+
const error = new Error('Client metadata format is not JSON.');
|
|
236
|
+
error.name = 'DataError';
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
client_metadata = response.data;
|
|
240
|
+
}
|
|
241
|
+
assertOptional(client_metadata, 'client_metadata', 'object');
|
|
242
|
+
if(presentation_definition_uri) {
|
|
243
|
+
const response = await fetchJSON(
|
|
244
|
+
{url: presentation_definition_uri, agent});
|
|
245
|
+
if(!response.data) {
|
|
246
|
+
const error = new Error('Presentation definition format is not JSON.');
|
|
247
|
+
error.name = 'DataError';
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
presentation_definition = response.data;
|
|
251
|
+
}
|
|
252
|
+
assert(presentation_definition, 'presentation_definition', 'object');
|
|
253
|
+
assert(presentation_definition?.id, 'presentation_definition.id', 'string');
|
|
254
|
+
if(presentation_definition.submission_requirements && strict) {
|
|
255
|
+
const error = new Error('"submission_requirements" is not supported.');
|
|
256
|
+
error.name = 'NotSupportedError';
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// generate base VPR from presentation definition
|
|
261
|
+
const verifiablePresentationRequest = {
|
|
262
|
+
// map each `input_descriptors` value to a `QueryByExample` query
|
|
263
|
+
query: [{
|
|
264
|
+
type: 'QueryByExample',
|
|
265
|
+
credentialQuery: presentation_definition.input_descriptors.map(
|
|
266
|
+
inputDescriptor => _toQueryByExampleQuery({inputDescriptor, strict}))
|
|
267
|
+
}]
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// add `DIDAuthentication` query based on client_metadata
|
|
271
|
+
if(client_metadata) {
|
|
272
|
+
const query = _toDIDAuthenticationQuery({client_metadata, strict});
|
|
273
|
+
if(query !== undefined) {
|
|
274
|
+
verifiablePresentationRequest.query.unshift(query);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// map `client_id` to `domain`
|
|
279
|
+
if(client_id !== undefined) {
|
|
280
|
+
verifiablePresentationRequest.domain = client_id;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// map `nonce` to `challenge`
|
|
284
|
+
if(nonce !== undefined) {
|
|
285
|
+
verifiablePresentationRequest.challenge = nonce;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {verifiablePresentationRequest};
|
|
289
|
+
} catch(cause) {
|
|
290
|
+
const error = new Error(
|
|
291
|
+
'Could not convert OID4VP authorization request to ' +
|
|
292
|
+
'verifiable presentation request.', {cause});
|
|
293
|
+
error.name = 'OperationError';
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// converts a VPR to partial "authorization request"
|
|
299
|
+
export function fromVpr({verifiablePresentationRequest, strict = false} = {}) {
|
|
300
|
+
try {
|
|
301
|
+
let {query} = verifiablePresentationRequest;
|
|
302
|
+
if(!Array.isArray(query)) {
|
|
303
|
+
query = [query];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// convert any `QueryByExample` queries
|
|
307
|
+
const queryByExample = query.filter(({type}) => type === 'QueryByExample');
|
|
308
|
+
let credentialQuery = [];
|
|
309
|
+
if(queryByExample.length > 0) {
|
|
310
|
+
if(queryByExample.length > 1 && strict) {
|
|
311
|
+
const error = new Error(
|
|
312
|
+
'Multiple "QueryByExample" VPR queries are not supported.');
|
|
313
|
+
error.name = 'NotSupportedError';
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
([{credentialQuery = []}] = queryByExample);
|
|
317
|
+
if(!Array.isArray(credentialQuery)) {
|
|
318
|
+
credentialQuery = [credentialQuery];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const authorizationRequest = {
|
|
322
|
+
response_type: 'vp_token',
|
|
323
|
+
presentation_definition: {
|
|
324
|
+
id: uuid(),
|
|
325
|
+
input_descriptors: credentialQuery.map(_fromQueryByExampleQuery)
|
|
326
|
+
},
|
|
327
|
+
response_mode: 'direct_post'
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// convert any `DIDAuthentication` queries
|
|
331
|
+
const didAuthnQuery = query.filter(
|
|
332
|
+
({type}) => type === 'DIDAuthentication');
|
|
333
|
+
if(didAuthnQuery.length > 0) {
|
|
334
|
+
if(didAuthnQuery.length > 1 && strict) {
|
|
335
|
+
const error = new Error(
|
|
336
|
+
'Multiple "DIDAuthentication" VPR queries are not supported.');
|
|
337
|
+
error.name = 'NotSupportedError';
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
const [query] = didAuthnQuery;
|
|
341
|
+
const client_metadata = _fromDIDAuthenticationQuery({query, strict});
|
|
342
|
+
authorizationRequest.client_metadata = client_metadata;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if(queryByExample.length === 0 && didAuthnQuery.length === 0 && strict) {
|
|
346
|
+
const error = new Error(
|
|
347
|
+
'Only "DIDAuthentication" and "QueryByExample" VPR queries are ' +
|
|
348
|
+
'supported.');
|
|
349
|
+
error.name = 'NotSupportedError';
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// include requested authn params
|
|
354
|
+
if(verifiablePresentationRequest.domain) {
|
|
355
|
+
// `authorizationRequest` uses `direct_post` so force client ID to
|
|
356
|
+
// be the exchange response URL per "Note" here:
|
|
357
|
+
// eslint-disable-next-line max-len
|
|
358
|
+
// https://openid.github.io/OpenID4VP/openid-4-verifiable-presentations-wg-draft.html#section-6.2
|
|
359
|
+
authorizationRequest.client_id = verifiablePresentationRequest.domain;
|
|
360
|
+
authorizationRequest.client_id_scheme = 'redirect_uri';
|
|
361
|
+
authorizationRequest.response_uri = authorizationRequest.client_id;
|
|
362
|
+
}
|
|
363
|
+
if(verifiablePresentationRequest.challenge) {
|
|
364
|
+
authorizationRequest.nonce = verifiablePresentationRequest.challenge;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return authorizationRequest;
|
|
368
|
+
} catch(cause) {
|
|
369
|
+
const error = new Error(
|
|
370
|
+
'Could not convert verifiable presentation request to ' +
|
|
371
|
+
'an OID4VP authorization request.', {cause});
|
|
372
|
+
error.name = 'OperationError';
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// creates a "presentation submission" from a presentation definition and VP
|
|
378
|
+
export function createPresentationSubmission({
|
|
379
|
+
presentationDefinition, verifiablePresentation
|
|
380
|
+
} = {}) {
|
|
381
|
+
const descriptor_map = [];
|
|
382
|
+
const presentationSubmission = {
|
|
383
|
+
id: uuid(),
|
|
384
|
+
definition_id: presentationDefinition.id,
|
|
385
|
+
descriptor_map
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
// walk through each input descriptor object and match it to a VC
|
|
390
|
+
let {verifiableCredential: vcs} = verifiablePresentation;
|
|
391
|
+
const single = !Array.isArray(vcs);
|
|
392
|
+
if(single) {
|
|
393
|
+
vcs = [vcs];
|
|
394
|
+
}
|
|
395
|
+
/* Note: It is conceivable that the same VC could match multiple input
|
|
396
|
+
descriptors. In this simplistic implementation, the first VC that matches
|
|
397
|
+
is used. This may result in VCs in the VP not being mapped to an input
|
|
398
|
+
descriptor, but every input descriptor having a VC that matches (i.e., at
|
|
399
|
+
least one VC will be shared across multiple input descriptors). If
|
|
400
|
+
some other behavior is more desirable, this can be changed in a future
|
|
401
|
+
version. */
|
|
402
|
+
for(const inputDescriptor of presentationDefinition.input_descriptors) {
|
|
403
|
+
// walk through each VC and try to match it to the input descriptor
|
|
404
|
+
for(let i = 0; i < vcs.length; ++i) {
|
|
405
|
+
const verifiableCredential = vcs[i];
|
|
406
|
+
if(_matchesInputDescriptor({inputDescriptor, verifiableCredential})) {
|
|
407
|
+
descriptor_map.push({
|
|
408
|
+
id: inputDescriptor.id,
|
|
409
|
+
path: '$',
|
|
410
|
+
format: 'ldp_vp',
|
|
411
|
+
path_nested: {
|
|
412
|
+
format: 'ldp_vc',
|
|
413
|
+
path: single ?
|
|
414
|
+
'$.verifiableCredential' :
|
|
415
|
+
'$.verifiableCredential[' + i + ']'
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch(cause) {
|
|
423
|
+
const error = new Error(
|
|
424
|
+
'Could not create presentation submission.', {cause});
|
|
425
|
+
error.name = 'OperationError';
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {presentationSubmission};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function _filterToValue({filter, strict = false}) {
|
|
433
|
+
/* Each `filter` has a JSON Schema object. In recognition of the fact that
|
|
434
|
+
a query must be usable by common database engines (including perhaps
|
|
435
|
+
encrypted cloud databases) and of the fact that each JSON Schema object will
|
|
436
|
+
come from an untrusted source (and could have malicious regexes, etc.), only
|
|
437
|
+
simple JSON Schema types are supported:
|
|
438
|
+
|
|
439
|
+
`string`: with `const` or `enum`, `format` is not supported and `pattern` has
|
|
440
|
+
partial support as it will be treated as a simple string not a regex; regex
|
|
441
|
+
is a DoS attack vector
|
|
442
|
+
|
|
443
|
+
`array`: with `items` or `contains` where uses a `string` filter
|
|
444
|
+
|
|
445
|
+
*/
|
|
446
|
+
let value;
|
|
447
|
+
|
|
448
|
+
const {type} = filter;
|
|
449
|
+
if(type === 'array') {
|
|
450
|
+
if(filter.contains) {
|
|
451
|
+
if(Array.isArray(filter.contains)) {
|
|
452
|
+
value = filter.contains.map(filter => _filterToValue({filter, strict}));
|
|
453
|
+
} else {
|
|
454
|
+
value = _filterToValue({filter: filter.contains, strict});
|
|
455
|
+
}
|
|
456
|
+
} else if(strict) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
'Unsupported filter; array filters must use "enum" or "contains" ' +
|
|
459
|
+
'with a string filter.');
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
if(type === 'string' || type === undefined) {
|
|
464
|
+
if(filter.const !== undefined) {
|
|
465
|
+
value = filter.const;
|
|
466
|
+
} else if(filter.pattern) {
|
|
467
|
+
value = filter.pattern;
|
|
468
|
+
} else if(filter.enum) {
|
|
469
|
+
value = filter.enum.slice();
|
|
470
|
+
} else if(strict) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
'Unsupported filter; string filters must use "const" or "pattern".');
|
|
473
|
+
}
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
476
|
+
if(strict) {
|
|
477
|
+
throw new Error(`Unsupported filter type "${type}".`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function _jsonPathToJsonPointer(jsonPath) {
|
|
482
|
+
return JSONPath.toPointer(JSONPath.toPathArray(jsonPath));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function _matchesInputDescriptor({
|
|
486
|
+
inputDescriptor, verifiableCredential, strict = false
|
|
487
|
+
}) {
|
|
488
|
+
// walk through each field ensuring there is a matching value
|
|
489
|
+
const fields = inputDescriptor?.constraints?.fields || [];
|
|
490
|
+
for(const field of fields) {
|
|
491
|
+
const {path, filter, optional} = field;
|
|
492
|
+
if(optional) {
|
|
493
|
+
// skip field, it is optional
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
// each field must have a `path` (which can be a string or an array)
|
|
499
|
+
if(!(Array.isArray(path) || typeof path === 'string')) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
'Input descriptor field "path" must be a string or array.');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// process any filter
|
|
505
|
+
let value = '';
|
|
506
|
+
if(filter !== undefined) {
|
|
507
|
+
value = _filterToValue({filter, strict});
|
|
508
|
+
}
|
|
509
|
+
// no value to match, presume no match
|
|
510
|
+
if(value === undefined) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
// normalize value to array
|
|
514
|
+
if(!Array.isArray(value)) {
|
|
515
|
+
value = [value];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// filter out erroneous paths
|
|
519
|
+
let paths = Array.isArray(path) ? path : [path];
|
|
520
|
+
paths = _adjustErroneousPaths(paths);
|
|
521
|
+
// convert each JSON path to a JSON pointer
|
|
522
|
+
const pointers = paths.map(_jsonPathToJsonPointer);
|
|
523
|
+
|
|
524
|
+
// check for a value at at least one path
|
|
525
|
+
for(const pointer of pointers) {
|
|
526
|
+
const existing = jsonpointer.get(verifiableCredential, pointer);
|
|
527
|
+
if(existing === undefined) {
|
|
528
|
+
// VC does not match
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
// look for at least one matching value in `existing`
|
|
532
|
+
let match = false;
|
|
533
|
+
for(const v of value) {
|
|
534
|
+
if(Array.isArray(existing)) {
|
|
535
|
+
if(existing.includes(v)) {
|
|
536
|
+
match = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
} else if(existing === v) {
|
|
540
|
+
match = true;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if(!match) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} catch(cause) {
|
|
549
|
+
const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
|
|
550
|
+
const error = new Error(
|
|
551
|
+
`Could not process input descriptor field: "${id}".`, {cause});
|
|
552
|
+
error.field = field;
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function _fromQueryByExampleQuery(credentialQuery) {
|
|
561
|
+
const fields = [];
|
|
562
|
+
const inputDescriptor = {
|
|
563
|
+
id: uuid(),
|
|
564
|
+
constraints: {fields}
|
|
565
|
+
};
|
|
566
|
+
if(credentialQuery?.reason) {
|
|
567
|
+
inputDescriptor.purpose = credentialQuery?.reason;
|
|
568
|
+
}
|
|
569
|
+
// FIXME: current implementation only supports top-level string/array
|
|
570
|
+
// properties and presumes strings
|
|
571
|
+
const path = ['$'];
|
|
572
|
+
const {example = {}} = credentialQuery || {};
|
|
573
|
+
for(const key in example) {
|
|
574
|
+
const value = example[key];
|
|
575
|
+
path.push(key);
|
|
576
|
+
|
|
577
|
+
const filter = {};
|
|
578
|
+
if(Array.isArray(value)) {
|
|
579
|
+
filter.type = 'array';
|
|
580
|
+
filter.contains = value.map(v => ({
|
|
581
|
+
type: 'string',
|
|
582
|
+
const: v
|
|
583
|
+
}));
|
|
584
|
+
} else if(key === 'type') {
|
|
585
|
+
// special provision for array/string for `type`
|
|
586
|
+
filter.type = 'array',
|
|
587
|
+
filter.contains = {
|
|
588
|
+
type: 'string',
|
|
589
|
+
const: value
|
|
590
|
+
};
|
|
591
|
+
} else {
|
|
592
|
+
filter.type = 'string',
|
|
593
|
+
filter.const = value;
|
|
594
|
+
}
|
|
595
|
+
fields.push({
|
|
596
|
+
path: JSONPath.toPathString(path),
|
|
597
|
+
filter
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
path.pop();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return inputDescriptor;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function _toDIDAuthenticationQuery({client_metadata, strict = false}) {
|
|
607
|
+
const {vp_formats} = client_metadata;
|
|
608
|
+
const proofTypes = vp_formats?.ldp_vp?.proof_type;
|
|
609
|
+
if(!Array.isArray(proofTypes)) {
|
|
610
|
+
if(strict) {
|
|
611
|
+
const error = new Error(
|
|
612
|
+
'"client_metadata.vp_formats.ldp_vp.proof_type" must be an array to ' +
|
|
613
|
+
'convert to DIDAuthentication query.');
|
|
614
|
+
error.name = 'NotSupportedError';
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
type: 'DIDAuthentication',
|
|
621
|
+
acceptedCryptosuites: proofTypes.map(cryptosuite => ({cryptosuite}))
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function _fromDIDAuthenticationQuery({query, strict = false}) {
|
|
626
|
+
const cryptosuites = query.acceptedCryptosuites?.map(
|
|
627
|
+
({cryptosuite}) => cryptosuite);
|
|
628
|
+
if(!(cryptosuites && cryptosuites.length > 0)) {
|
|
629
|
+
if(strict) {
|
|
630
|
+
const error = new Error(
|
|
631
|
+
'"query.acceptedCryptosuites" must be a non-array with specified ' +
|
|
632
|
+
'cryptosuites to convert from a DIDAuthentication query.');
|
|
633
|
+
error.name = 'NotSupportedError';
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
require_signed_request_object: false,
|
|
640
|
+
vp_formats: {
|
|
641
|
+
ldp_vp: {
|
|
642
|
+
proof_type: cryptosuites
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function _toQueryByExampleQuery({inputDescriptor, strict = false}) {
|
|
649
|
+
// every input descriptor must have an `id`
|
|
650
|
+
if(typeof inputDescriptor?.id !== 'string') {
|
|
651
|
+
throw new TypeError('Input descriptor "id" must be a string.');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const example = {};
|
|
655
|
+
const credentialQuery = {example};
|
|
656
|
+
if(inputDescriptor.purpose) {
|
|
657
|
+
credentialQuery.reason = inputDescriptor.purpose;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/* Note: Each input descriptor object is currently mapped to a single example
|
|
661
|
+
query. If multiple possible path values appear for a single field, these will
|
|
662
|
+
be mapped to multiple properties in the example which may or may not be what
|
|
663
|
+
is intended. This behavior could be changed in a future revision if it
|
|
664
|
+
becomes clear there is a better approach. */
|
|
665
|
+
|
|
666
|
+
const fields = inputDescriptor.constraints?.fields || [];
|
|
667
|
+
for(const field of fields) {
|
|
668
|
+
const {path, filter, optional} = field;
|
|
669
|
+
// skip optional fields
|
|
670
|
+
if(optional === true) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
// each field must have a `path` (which can be a string or an array)
|
|
676
|
+
if(!(Array.isArray(path) || typeof path === 'string')) {
|
|
677
|
+
throw new TypeError(
|
|
678
|
+
'Input descriptor field "path" must be a string or array.');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// process any filter
|
|
682
|
+
let value = '';
|
|
683
|
+
if(filter !== undefined) {
|
|
684
|
+
value = _filterToValue({filter, strict});
|
|
685
|
+
}
|
|
686
|
+
// no value understood, skip field
|
|
687
|
+
if(value === undefined) {
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
// normalize value to array
|
|
691
|
+
if(!Array.isArray(value)) {
|
|
692
|
+
value = [value];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// filter out erroneous paths
|
|
696
|
+
let paths = Array.isArray(path) ? path : [path];
|
|
697
|
+
paths = _adjustErroneousPaths(paths);
|
|
698
|
+
// convert each JSON path to a JSON pointer
|
|
699
|
+
const pointers = paths.map(_jsonPathToJsonPointer);
|
|
700
|
+
|
|
701
|
+
// add values at each path, converting to an array / appending as needed
|
|
702
|
+
for(const pointer of pointers) {
|
|
703
|
+
const existing = jsonpointer.get(example, pointer);
|
|
704
|
+
if(existing === undefined) {
|
|
705
|
+
jsonpointer.set(
|
|
706
|
+
example, pointer, value.length > 1 ? value : value[0]);
|
|
707
|
+
} else if(Array.isArray(existing)) {
|
|
708
|
+
if(!existing.includes(value)) {
|
|
709
|
+
existing.push(...value);
|
|
710
|
+
}
|
|
711
|
+
} else if(existing !== value) {
|
|
712
|
+
jsonpointer.set(example, pointer, [existing, ...value]);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} catch(cause) {
|
|
716
|
+
const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
|
|
717
|
+
const error = new Error(
|
|
718
|
+
`Could not process input descriptor field: "${id}".`, {cause});
|
|
719
|
+
error.field = field;
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return credentialQuery;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function _adjustErroneousPaths(paths) {
|
|
728
|
+
// remove any paths that start with what would be present in a
|
|
729
|
+
// presentation submission and adjust any paths that would be part of a
|
|
730
|
+
// JWT-secured VC, such that only actual VC paths remain
|
|
731
|
+
const removed = paths.filter(p => !_isPresentationSubmissionPath(p));
|
|
732
|
+
return removed.map(p => {
|
|
733
|
+
return !_isJWTPath(p) ? p : '$' + p.slice('$.vc'.length);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function _parseOID4VPUrl({url}) {
|
|
738
|
+
const {searchParams} = new URL(url);
|
|
739
|
+
const request = _get(searchParams, 'request');
|
|
740
|
+
const request_uri = _get(searchParams, 'request_uri');
|
|
741
|
+
const response_type = _get(searchParams, 'response_type');
|
|
742
|
+
const response_mode = _get(searchParams, 'response_mode');
|
|
743
|
+
const presentation_definition = _get(
|
|
744
|
+
searchParams, 'presentation_definition');
|
|
745
|
+
const presentation_definition_uri = _get(
|
|
746
|
+
searchParams, 'presentation_definition_uri');
|
|
747
|
+
const client_id = _get(searchParams, 'client_id');
|
|
748
|
+
const client_id_scheme = _get(searchParams, 'client_id_scheme');
|
|
749
|
+
const client_metadata = _get(searchParams, 'client_metadata');
|
|
750
|
+
const nonce = _get(searchParams, 'nonce');
|
|
751
|
+
const response_uri = _get(searchParams, 'response_uri');
|
|
752
|
+
const state = _get(searchParams, 'state');
|
|
753
|
+
if(request && request_uri) {
|
|
754
|
+
const error = new Error(
|
|
755
|
+
'Only one of "request" and "request_uri" may be present.');
|
|
756
|
+
error.name = 'DataError';
|
|
757
|
+
error.url = url;
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
if(!(request || request_uri)) {
|
|
761
|
+
if(response_type !== 'vp_token') {
|
|
762
|
+
throw new Error(`Unsupported "response_type", "${response_type}".`);
|
|
763
|
+
}
|
|
764
|
+
if(response_mode !== 'direct_post') {
|
|
765
|
+
throw new Error(`Unsupported "response_type", "${response_type}".`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const authorizationRequest = {
|
|
769
|
+
request,
|
|
770
|
+
request_uri,
|
|
771
|
+
response_type,
|
|
772
|
+
response_mode,
|
|
773
|
+
presentation_definition: presentation_definition &&
|
|
774
|
+
JSON.parse(presentation_definition),
|
|
775
|
+
presentation_definition_uri,
|
|
776
|
+
client_id,
|
|
777
|
+
client_id_scheme,
|
|
778
|
+
client_metadata: client_metadata && JSON.parse(client_metadata),
|
|
779
|
+
response_uri,
|
|
780
|
+
nonce,
|
|
781
|
+
state
|
|
782
|
+
};
|
|
783
|
+
return {authorizationRequest};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function _get(sp, name) {
|
|
787
|
+
const value = sp.get(name);
|
|
788
|
+
return value === null ? undefined : value;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function _isPresentationSubmissionPath(path) {
|
|
792
|
+
return path.startsWith('$.verifiableCredential[') || path.startsWith('$.vp.');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function _isJWTPath(path) {
|
|
796
|
+
return path.startsWith('$.vc.');
|
|
797
|
+
}
|
package/lib/util.js
CHANGED
|
@@ -8,30 +8,36 @@ const TEXT_ENCODER = new TextEncoder();
|
|
|
8
8
|
const ENCODED_PERIOD = TEXT_ENCODER.encode('.');
|
|
9
9
|
const WELL_KNOWN_REGEX = /\/\.well-known\/([^\/]+)/;
|
|
10
10
|
|
|
11
|
+
export function assert(x, name, type, optional = false) {
|
|
12
|
+
const article = type === 'object' ? 'an' : 'a';
|
|
13
|
+
if(x !== undefined && typeof x !== type) {
|
|
14
|
+
throw new TypeError(
|
|
15
|
+
`${optional ? 'When present, ' : ''} ` +
|
|
16
|
+
`"${name}" must be ${article} ${type}.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function assertOptional(x, name, type) {
|
|
21
|
+
return assert(x, name, type, true);
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
|
|
12
25
|
try {
|
|
13
|
-
|
|
14
|
-
throw new TypeError('"issuerConfigUrl" must be a string.');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// allow these params to be passed / configured
|
|
18
|
-
const fetchOptions = {
|
|
19
|
-
// max size for issuer config related responses (in bytes, ~4 KiB)
|
|
20
|
-
size: 4096,
|
|
21
|
-
// timeout in ms for fetching an issuer config
|
|
22
|
-
timeout: 5000,
|
|
23
|
-
agent
|
|
24
|
-
};
|
|
26
|
+
assert(issuerConfigUrl, 'issuerConfigUrl', 'string');
|
|
25
27
|
|
|
26
|
-
const response = await
|
|
28
|
+
const response = await fetchJSON({url: issuerConfigUrl, agent});
|
|
27
29
|
if(!response.data) {
|
|
28
30
|
const error = new Error('Issuer configuration format is not JSON.');
|
|
29
31
|
error.name = 'DataError';
|
|
30
32
|
throw error;
|
|
31
33
|
}
|
|
34
|
+
const {data: issuerMetaData} = response;
|
|
35
|
+
const {issuer, authorization_server} = issuerMetaData;
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
if(authorization_server && authorization_server !== issuer) {
|
|
38
|
+
// not yet implemented
|
|
39
|
+
throw new Error('Separate authorization server not yet implemented.');
|
|
40
|
+
}
|
|
35
41
|
|
|
36
42
|
// validate `issuer`
|
|
37
43
|
if(!(typeof issuer === 'string' && issuer.startsWith('https://'))) {
|
|
@@ -63,22 +69,48 @@ export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
|
|
|
63
69
|
throw error;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
// fetch AS meta data
|
|
73
|
+
const asMetaDataUrl =
|
|
74
|
+
`${origin}/.well-known/oauth-authorization-server${pathname}`;
|
|
75
|
+
const asMetaDataResponse = await fetchJSON({url: asMetaDataUrl, agent});
|
|
76
|
+
if(!asMetaDataResponse.data) {
|
|
77
|
+
const error = new Error('Authorization server meta data is not JSON.');
|
|
69
78
|
error.name = 'DataError';
|
|
70
79
|
throw error;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
const {data: asMetaData} = response;
|
|
83
|
+
// merge AS meta data into total issuer config
|
|
84
|
+
const issuerConfig = {...issuerMetaData, ...asMetaData};
|
|
85
|
+
|
|
86
|
+
// ensure `token_endpoint` is valid
|
|
87
|
+
const {token_endpoint} = asMetaData;
|
|
88
|
+
assert(token_endpoint, 'token_endpoint', 'string');
|
|
89
|
+
|
|
90
|
+
// return merged config and separate issuer and AS configs
|
|
91
|
+
const metadata = {issuer: issuerMetaData, authorizationServer: asMetaData};
|
|
92
|
+
return {issuerConfig, metadata};
|
|
74
93
|
} catch(cause) {
|
|
75
|
-
const error = new Error('Could not get
|
|
94
|
+
const error = new Error('Could not get OpenID issuer configuration.');
|
|
76
95
|
error.name = 'OperationError';
|
|
77
96
|
error.cause = cause;
|
|
78
97
|
throw error;
|
|
79
98
|
}
|
|
80
99
|
}
|
|
81
100
|
|
|
101
|
+
export function fetchJSON({url, agent} = {}) {
|
|
102
|
+
// allow these params to be passed / configured
|
|
103
|
+
const fetchOptions = {
|
|
104
|
+
// max size for issuer config related responses (in bytes, ~4 KiB)
|
|
105
|
+
size: 4096,
|
|
106
|
+
// timeout in ms for fetching an issuer config
|
|
107
|
+
timeout: 5000,
|
|
108
|
+
agent
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return httpClient.get(url, fetchOptions);
|
|
112
|
+
}
|
|
113
|
+
|
|
82
114
|
export async function generateDIDProofJWT({
|
|
83
115
|
signer, nonce, iss, aud, exp, nbf
|
|
84
116
|
} = {}) {
|
|
@@ -113,9 +145,7 @@ export async function generateDIDProofJWT({
|
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
export function parseCredentialOfferUrl({url} = {}) {
|
|
116
|
-
|
|
117
|
-
throw new TypeError('"url" must be a string.');
|
|
118
|
-
}
|
|
148
|
+
assert(url, 'url', 'string');
|
|
119
149
|
|
|
120
150
|
/* Parse URL, e.g.:
|
|
121
151
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalbazaar/oid4-client",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "An OID4 (VC + VP) client",
|
|
5
5
|
"homepage": "https://github.com/digitalbazaar/oid4-client",
|
|
6
6
|
"author": {
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@digitalbazaar/http-client": "^3.2.0",
|
|
27
|
-
"base64url-universal": "^2.0.0"
|
|
27
|
+
"base64url-universal": "^2.0.0",
|
|
28
|
+
"jose": "^4.15.4",
|
|
29
|
+
"jsonpath-plus": "^7.2.0",
|
|
30
|
+
"jsonpointer": "^5.0.1",
|
|
31
|
+
"uuid": "^9.0.1"
|
|
28
32
|
},
|
|
29
33
|
"devDependencies": {
|
|
30
34
|
"c8": "^7.11.3",
|