@digitalbazaar/oid4-client 5.1.0 → 5.2.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/util.js CHANGED
@@ -6,14 +6,15 @@ import {httpClient} from '@digitalbazaar/http-client';
6
6
 
7
7
  const TEXT_ENCODER = new TextEncoder();
8
8
  const ENCODED_PERIOD = TEXT_ENCODER.encode('.');
9
- const WELL_KNOWN_REGEX = /\/\.well-known\/([^\/]+)/;
10
9
 
11
10
  export function assert(x, name, type, optional = false) {
12
11
  const article = type === 'object' ? 'an' : 'a';
13
- if(x !== undefined && typeof x !== type) {
12
+ const xType = typeof type === 'string' ?
13
+ typeof x : (x instanceof type && type);
14
+ if(x !== undefined && xType !== type) {
14
15
  throw new TypeError(
15
16
  `${optional ? 'When present, ' : ''} ` +
16
- `"${name}" must be ${article} ${type}.`);
17
+ `"${name}" must be ${article} ${type?.name ?? type}.`);
17
18
  }
18
19
  }
19
20
 
@@ -46,101 +47,6 @@ export function createNamedError({message, name, details, cause} = {}) {
46
47
  return error;
47
48
  }
48
49
 
49
- export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
50
- try {
51
- assert(issuerConfigUrl, 'issuerConfigUrl', 'string');
52
-
53
- const response = await fetchJSON({url: issuerConfigUrl, agent});
54
- if(!response.data) {
55
- const error = new Error('Issuer configuration format is not JSON.');
56
- error.name = 'DataError';
57
- throw error;
58
- }
59
- const {data: issuerMetaData} = response;
60
- const {issuer, authorization_server} = issuerMetaData;
61
-
62
- if(authorization_server && authorization_server !== issuer) {
63
- // not yet implemented
64
- throw new Error('Separate authorization server not yet implemented.');
65
- }
66
-
67
- // validate `issuer`
68
- if(!(typeof issuer === 'string' && issuer.startsWith('https://'))) {
69
- const error = new Error('"issuer" is not an HTTPS URL.');
70
- error.name = 'DataError';
71
- throw error;
72
- }
73
-
74
- // ensure `credential_issuer` matches `issuer`, if present
75
- const {credential_issuer} = issuerMetaData;
76
- if(credential_issuer !== undefined && credential_issuer !== issuer) {
77
- const error = new Error('"credential_issuer" must match "issuer".');
78
- error.name = 'DataError';
79
- throw error;
80
- }
81
-
82
- /* Validate `issuer` value against `issuerConfigUrl` (per RFC 8414):
83
-
84
- The `origin` and `path` element must be parsed from `issuer` and checked
85
- against `issuerConfigUrl` like so:
86
-
87
- For issuer `<origin>` (no path), `issuerConfigUrl` must match:
88
- `<origin>/.well-known/<any-path-segment>`
89
-
90
- For issuer `<origin><path>`, `issuerConfigUrl` must be:
91
- `<origin>/.well-known/<any-path-segment><path>` */
92
- const {pathname: wellKnownPath} = new URL(issuerConfigUrl);
93
- const anyPathSegment = wellKnownPath.match(WELL_KNOWN_REGEX)[1];
94
- const {origin, pathname} = new URL(issuer);
95
- let expectedConfigUrl = `${origin}/.well-known/${anyPathSegment}`;
96
- if(pathname !== '/') {
97
- expectedConfigUrl += pathname;
98
- }
99
- if(issuerConfigUrl !== expectedConfigUrl) {
100
- // alternatively, against RFC 8414, but according to OID4VCI, make sure
101
- // the issuer config URL matches:
102
- // <origin><path>/.well-known/<any-path-segment>
103
- expectedConfigUrl = origin;
104
- if(pathname !== '/') {
105
- expectedConfigUrl += pathname;
106
- }
107
- expectedConfigUrl += `/.well-known/${anyPathSegment}`;
108
- if(issuerConfigUrl !== expectedConfigUrl) {
109
- const error = new Error('"issuer" does not match configuration URL.');
110
- error.name = 'DataError';
111
- throw error;
112
- }
113
- }
114
-
115
- // fetch AS meta data
116
- const asMetaDataUrl =
117
- `${origin}/.well-known/oauth-authorization-server${pathname}`;
118
- const asMetaDataResponse = await fetchJSON({url: asMetaDataUrl, agent});
119
- if(!asMetaDataResponse.data) {
120
- const error = new Error('Authorization server meta data is not JSON.');
121
- error.name = 'DataError';
122
- throw error;
123
- }
124
-
125
- const {data: asMetaData} = response;
126
- // merge AS meta data into total issuer config
127
- const issuerConfig = {...issuerMetaData, ...asMetaData};
128
-
129
- // ensure `token_endpoint` is valid
130
- const {token_endpoint} = asMetaData;
131
- assert(token_endpoint, 'token_endpoint', 'string');
132
-
133
- // return merged config and separate issuer and AS configs
134
- const metadata = {issuer: issuerMetaData, authorizationServer: asMetaData};
135
- return {issuerConfig, metadata};
136
- } catch(cause) {
137
- const error = new Error('Could not get OpenID issuer configuration.');
138
- error.name = 'OperationError';
139
- error.cause = cause;
140
- throw error;
141
- }
142
- }
143
-
144
50
  export function fetchJSON({url, agent} = {}) {
145
51
  // allow these params to be passed / configured
146
52
  const fetchOptions = {
@@ -154,126 +60,17 @@ export function fetchJSON({url, agent} = {}) {
154
60
  return httpClient.get(url, fetchOptions);
155
61
  }
156
62
 
157
- export async function generateDIDProofJWT({
158
- signer, nonce, iss, aud, exp, nbf
159
- } = {}) {
160
- /* Example:
161
- {
162
- "alg": "ES256",
163
- "kid":"did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1"
164
- }.
165
- {
166
- "iss": "s6BhdRkqt3",
167
- "aud": "https://server.example.com",
168
- "iat": 1659145924,
169
- "nonce": "tZignsnFbp"
170
- }
171
- */
172
-
173
- if(exp === undefined) {
174
- // default to 5 minute expiration time
175
- exp = Math.floor(Date.now() / 1000) + 60 * 5;
176
- }
177
- if(nbf === undefined) {
178
- // default to now
179
- nbf = Math.floor(Date.now() / 1000);
180
- }
181
-
182
- const {id: kid} = signer;
183
- const alg = _curveToAlg(signer.algorithm);
184
- const payload = {nonce, iss, aud, exp, nbf};
185
- const protectedHeader = {alg, kid};
186
-
187
- return signJWT({payload, protectedHeader, signer});
188
- }
189
-
190
- export async function getCredentialOffer({url, agent} = {}) {
191
- const {protocol, searchParams} = new URL(url);
192
- if(protocol !== 'openid-credential-offer:') {
193
- throw new SyntaxError(
194
- '"url" must express a URL with the ' +
195
- '"openid-credential-offer" protocol.');
196
- }
197
- const offer = searchParams.get('credential_offer');
198
- if(offer) {
199
- return JSON.parse(offer);
200
- }
201
-
202
- // try to fetch offer from URL
203
- const offerUrl = searchParams.get('credential_offer_uri');
204
- if(!offerUrl) {
205
- throw new SyntaxError(
206
- 'OID4VCI credential offer must have "credential_offer" or ' +
207
- '"credential_offer_uri".');
208
- }
209
-
210
- if(!offerUrl.startsWith('https://')) {
211
- const error = new Error(
212
- `"credential_offer_uri" (${offerUrl}) must start with "https://".`);
213
- error.name = 'NotSupportedError';
214
- throw error;
215
- }
216
-
217
- const response = await fetchJSON({url: offerUrl, agent});
218
- if(!response.data) {
219
- const error = new Error(
220
- `Credential offer fetched from "${offerUrl}" is not JSON.`);
221
- error.name = 'DataError';
222
- throw error;
223
- }
224
- return response.data;
225
- }
226
-
227
- export function parseCredentialOfferUrl({url} = {}) {
228
- assert(url, 'url', 'string');
229
-
230
- /* Parse URL, e.g.:
231
-
232
- 'openid-credential-offer://?' +
233
- 'credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2F' +
234
- 'localhost%3A18443%2Fexchangers%2Fz19t8xb568tNRD1zVm9R5diXR%2F' +
235
- 'exchanges%2Fz1ADs3ur2s9tm6JUW6CnTiyn3%22%2C%22credentials' +
236
- '%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition' +
237
- '%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
238
- 'credentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
239
- 'credentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22' +
240
- 'VerifiableCredential%22%2C%22UniversityDegreeCredential' +
241
- '%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams' +
242
- '%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22' +
243
- 'pre-authorized_code%22%3A%22z1AEvnk2cqeRM1Mfv75vzHSUo%22%7D%7D%7D';
244
- */
245
- const {protocol, searchParams} = new URL(url);
246
- if(protocol !== 'openid-credential-offer:') {
247
- throw new SyntaxError(
248
- '"url" must express a URL with the ' +
249
- '"openid-credential-offer" protocol.');
250
- }
251
- return JSON.parse(searchParams.get('credential_offer'));
252
- }
253
-
254
- export async function robustDiscoverIssuer({issuer, agent} = {}) {
255
- // try issuer config URLs based on OID4VCI (first) and RFC 8414 (second)
256
- const parsedIssuer = new URL(issuer);
257
- const {origin} = parsedIssuer;
258
- const path = parsedIssuer.pathname === '/' ? '' : parsedIssuer.pathname;
259
-
260
- const issuerConfigUrls = [
261
- // OID4VCI
262
- `${origin}${path}/.well-known/openid-credential-issuer`,
263
- // RFC 8414
264
- `${origin}/.well-known/openid-credential-issuer${path}`
265
- ];
266
-
267
- let error;
268
- for(const issuerConfigUrl of issuerConfigUrls) {
269
- try {
270
- const config = await discoverIssuer({issuerConfigUrl, agent});
271
- return config;
272
- } catch(e) {
273
- error = e;
274
- }
63
+ export function parseJSON(x, name) {
64
+ try {
65
+ return JSON.parse(x);
66
+ } catch(cause) {
67
+ throw createNamedError({
68
+ message: `Could not parse "${name}".`,
69
+ name: 'DataError',
70
+ details: {httpStatusCode: 400, public: true},
71
+ cause
72
+ });
275
73
  }
276
- throw error;
277
74
  }
278
75
 
279
76
  export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
@@ -319,6 +116,14 @@ export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
319
116
  });
320
117
  }
321
118
 
119
+ export async function sha256(data) {
120
+ if(typeof data === 'string') {
121
+ data = new TextEncoder().encode(data);
122
+ }
123
+ const algorithm = {name: 'SHA-256'};
124
+ return new Uint8Array(await crypto.subtle.digest(algorithm, data));
125
+ }
126
+
322
127
  export async function signJWT({payload, protectedHeader, signer} = {}) {
323
128
  // encode payload and protected header
324
129
  const b64Payload = base64url.encode(JSON.stringify(payload));
@@ -346,16 +151,3 @@ export async function signJWT({payload, protectedHeader, signer} = {}) {
346
151
  // create compact JWT
347
152
  return `${jws.protected}.${jws.payload}.${jws.signature}`;
348
153
  }
349
-
350
- function _curveToAlg(crv) {
351
- if(crv === 'Ed25519' || crv === 'Ed448') {
352
- return 'EdDSA';
353
- }
354
- if(crv?.startsWith('P-')) {
355
- return `ES${crv.slice(2)}`;
356
- }
357
- if(crv === 'secp256k1') {
358
- return 'ES256K';
359
- }
360
- return crv;
361
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalbazaar/oid4-client",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "An OID4 (VC + VP) client",
5
5
  "homepage": "https://github.com/digitalbazaar/oid4-client",
6
6
  "author": {
@@ -26,8 +26,8 @@
26
26
  "@digitalbazaar/http-client": "^4.0.0",
27
27
  "base64url-universal": "^2.0.0",
28
28
  "jose": "^6.1.0",
29
+ "json-pointer": "^0.6.2",
29
30
  "jsonpath-plus": "^10.3.0",
30
- "jsonpointer": "^5.0.1",
31
31
  "pkijs": "^3.2.5"
32
32
  },
33
33
  "devDependencies": {