@digitalbazaar/oid4-client 3.4.1 → 3.6.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/OID4Client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
- import {discoverIssuer, generateDIDProofJWT} from './util.js';
4
+ import {generateDIDProofJWT, robustDiscoverIssuer} from './util.js';
5
5
  import {httpClient} from '@digitalbazaar/http-client';
6
6
 
7
7
  const GRANT_TYPES = new Map([
@@ -159,7 +159,7 @@ export class OID4Client {
159
159
  nonce,
160
160
  // the entity identified by the DID is issuing this JWT
161
161
  iss: did,
162
- // audience MUST be the target issuer per the OID4VC spec
162
+ // audience MUST be the target issuer per the OID4VCI spec
163
163
  aud
164
164
  });
165
165
 
@@ -206,7 +206,12 @@ export class OID4Client {
206
206
  // create a client from a credential offer
207
207
  static async fromCredentialOffer({offer, agent} = {}) {
208
208
  // validate offer
209
- const {credential_issuer, credentials, grants = {}} = offer;
209
+ const {
210
+ credential_issuer,
211
+ credentials,
212
+ credential_configuration_ids,
213
+ grants = {}
214
+ } = offer;
210
215
  let parsedIssuer;
211
216
  try {
212
217
  parsedIssuer = new URL(credential_issuer);
@@ -216,8 +221,20 @@ export class OID4Client {
216
221
  } catch(cause) {
217
222
  throw new Error('"offer.credential_issuer" is not valid.', {cause});
218
223
  }
219
- if(!(Array.isArray(credentials) && credentials.length > 0 &&
220
- credentials.every(c => c && typeof c === 'object'))) {
224
+ if(credentials === undefined &&
225
+ credential_configuration_ids === undefined) {
226
+ throw new Error(
227
+ 'Either "offer.credential_configuration_ids" or ' +
228
+ '"offer.credentials" is required.');
229
+ }
230
+ if(credential_configuration_ids !== undefined &&
231
+ !Array.isArray(credential_configuration_ids)) {
232
+ throw new Error('"offer.credential_configuration_ids" is not valid.');
233
+ }
234
+ if(credentials !== undefined &&
235
+ !(Array.isArray(credentials) && credentials.length > 0 &&
236
+ credentials.every(c => c && (
237
+ typeof c === 'object' || typeof c === 'string')))) {
221
238
  throw new Error('"offer.credentials" is not valid.');
222
239
  }
223
240
  const grant = grants[GRANT_TYPES.get('preAuthorizedCode')];
@@ -227,6 +244,7 @@ export class OID4Client {
227
244
  }
228
245
  const {
229
246
  'pre-authorized_code': preAuthorizedCode,
247
+ // FIXME: update to `tx_code` terminology
230
248
  user_pin_required: userPinRequired
231
249
  } = grant;
232
250
  if(!preAuthorizedCode) {
@@ -238,11 +256,9 @@ export class OID4Client {
238
256
 
239
257
  try {
240
258
  // discover issuer info
241
- const issuerConfigUrl =
242
- `${parsedIssuer.origin}/.well-known/openid-credential-issuer` +
243
- parsedIssuer.pathname;
244
- const {issuerConfig, metadata} = await discoverIssuer(
245
- {issuerConfigUrl, agent});
259
+ const {issuerConfig, metadata} = await robustDiscoverIssuer({
260
+ issuer: credential_issuer, agent
261
+ });
246
262
 
247
263
  /* First get access token from AS (Authorization Server), e.g.:
248
264
 
@@ -306,8 +322,9 @@ export class OID4Client {
306
322
  }
307
323
 
308
324
  // create client w/access token
309
- return new OID4Client(
310
- {accessToken, agent, issuerConfig, metadata, offer});
325
+ return new OID4Client({
326
+ accessToken, agent, issuerConfig, metadata, offer
327
+ });
311
328
  } catch(cause) {
312
329
  const error = new Error('Could not create OID4 client.', {cause});
313
330
  error.name = 'OperationError';
@@ -376,9 +393,48 @@ function _isPresentationRequired(error) {
376
393
  return error.status === 400 && errorType === 'presentation_required';
377
394
  }
378
395
 
379
- function _createCredentialRequestFromId({id, issuerConfig}) {
380
- const {credentials_supported: supported = []} = issuerConfig;
381
- const meta = supported.find(d => d.id === id);
396
+ function _createCredentialRequestsFromOffer({issuerConfig, offer}) {
397
+ // get any supported credential configurations from issuer config
398
+ const supported = _createSupportedCredentialsMap({issuerConfig});
399
+
400
+ // build requests from credentials identified in `offer`
401
+ const credentials = offer.credential_configuration_ids ?? offer.credentials;
402
+ return credentials.map(c => {
403
+ if(typeof c === 'string') {
404
+ // use supported credential config
405
+ return _getSupportedCredentialById({id: c, supported});
406
+ }
407
+ return c;
408
+ });
409
+ }
410
+
411
+ function _createSupportedCredentialsMap({issuerConfig}) {
412
+ const {
413
+ credential_configurations_supported,
414
+ credentials_supported
415
+ } = issuerConfig;
416
+
417
+ let supported;
418
+ if(credential_configurations_supported &&
419
+ typeof credential_configurations_supported === 'object') {
420
+ supported = new Map(Object.entries(
421
+ issuerConfig.credential_configurations_supported));
422
+ } else if(Array.isArray(credentials_supported)) {
423
+ // handle legacy `credentials_supported` array
424
+ supported = new Map();
425
+ for(const entry of issuerConfig.credentials_supported) {
426
+ supported.set(entry.id, entry);
427
+ }
428
+ } else {
429
+ // no supported credentials from issuer config
430
+ supported = new Map();
431
+ }
432
+
433
+ return supported;
434
+ }
435
+
436
+ function _getSupportedCredentialById({id, supported}) {
437
+ const meta = supported.get(id);
382
438
  if(!meta) {
383
439
  throw new Error(`No supported credential "${id}" found.`);
384
440
  }
@@ -388,21 +444,11 @@ function _createCredentialRequestFromId({id, issuerConfig}) {
388
444
  `Invalid supported credential "${id}"; "format" not specified.`);
389
445
  }
390
446
  if(!(Array.isArray(credential_definition?.['@context']) &&
391
- Array.isArray(credential_definition?.types))) {
447
+ (Array.isArray(credential_definition?.types) ||
448
+ Array.isArray(credential_definition?.type)))) {
392
449
  throw new Error(
393
450
  `Invalid supported credential "${id}"; "credential_definition" not ` +
394
451
  'fully specified.');
395
452
  }
396
453
  return {format, credential_definition};
397
454
  }
398
-
399
- function _createCredentialRequestsFromOffer({issuerConfig, offer}) {
400
- // build requests from `offer`
401
- return offer.credentials.map(c => {
402
- if(typeof c === 'string') {
403
- // use issuer config metadata to dereference string
404
- return _createCredentialRequestFromId({id: c, issuerConfig});
405
- }
406
- return c;
407
- });
408
- }
package/lib/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  /*!
2
- * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  export * as oid4vp from './oid4vp.js';
5
5
  export {
6
6
  discoverIssuer,
7
7
  generateDIDProofJWT,
8
8
  parseCredentialOfferUrl,
9
+ robustDiscoverIssuer,
9
10
  signJWT
10
11
  } from './util.js';
11
12
  export {OID4Client} from './OID4Client.js';
package/lib/oid4vp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2023-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import {assert, assertOptional, fetchJSON} from './util.js';
5
5
  import {decodeJwt} from 'jose';
@@ -159,6 +159,7 @@ export async function sendAuthorizationResponse({
159
159
  verifiablePresentation,
160
160
  presentationSubmission,
161
161
  authorizationRequest,
162
+ vpToken,
162
163
  agent
163
164
  } = {}) {
164
165
  try {
@@ -174,7 +175,7 @@ export async function sendAuthorizationResponse({
174
175
 
175
176
  // send VP and presentation submission to complete exchange
176
177
  const body = new URLSearchParams();
177
- body.set('vp_token', JSON.stringify(verifiablePresentation));
178
+ body.set('vp_token', vpToken ?? JSON.stringify(verifiablePresentation));
178
179
  body.set('presentation_submission', JSON.stringify(presentationSubmission));
179
180
  const response = await httpClient.post(authorizationRequest.response_uri, {
180
181
  agent, body, headers: {accept: 'application/json'},
package/lib/util.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as base64url from 'base64url-universal';
5
5
  import {httpClient} from '@digitalbazaar/http-client';
@@ -46,6 +46,14 @@ export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
46
46
  throw error;
47
47
  }
48
48
 
49
+ // ensure `credential_issuer` matches `issuer`, if present
50
+ const {credential_issuer} = issuerMetaData;
51
+ if(credential_issuer !== undefined && credential_issuer !== issuer) {
52
+ const error = new Error('"credential_issuer" must match "issuer".');
53
+ error.name = 'DataError';
54
+ throw error;
55
+ }
56
+
49
57
  /* Validate `issuer` value against `issuerConfigUrl` (per RFC 8414):
50
58
 
51
59
  The `origin` and `path` element must be parsed from `issuer` and checked
@@ -64,9 +72,19 @@ export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
64
72
  expectedConfigUrl += pathname;
65
73
  }
66
74
  if(issuerConfigUrl !== expectedConfigUrl) {
67
- const error = new Error('"issuer" does not match configuration URL.');
68
- error.name = 'DataError';
69
- throw error;
75
+ // alternatively, against RFC 8414, but according to OID4VCI, make sure
76
+ // the issuer config URL matches:
77
+ // <origin><path>/.well-known/<any-path-segment>
78
+ expectedConfigUrl = origin;
79
+ if(pathname !== '/') {
80
+ expectedConfigUrl += pathname;
81
+ }
82
+ expectedConfigUrl += `/.well-known/${anyPathSegment}`;
83
+ if(issuerConfigUrl !== expectedConfigUrl) {
84
+ const error = new Error('"issuer" does not match configuration URL.');
85
+ error.name = 'DataError';
86
+ throw error;
87
+ }
70
88
  }
71
89
 
72
90
  // fetch AS meta data
@@ -171,6 +189,31 @@ export function parseCredentialOfferUrl({url} = {}) {
171
189
  return JSON.parse(searchParams.get('credential_offer'));
172
190
  }
173
191
 
192
+ export async function robustDiscoverIssuer({issuer, agent} = {}) {
193
+ // try issuer config URLs based on OID4VCI (first) and RFC 8414 (second)
194
+ const parsedIssuer = new URL(issuer);
195
+ const {origin} = parsedIssuer;
196
+ const path = parsedIssuer.pathname === '/' ? '' : parsedIssuer.pathname;
197
+
198
+ const issuerConfigUrls = [
199
+ // OID4VCI
200
+ `${origin}${path}/.well-known/openid-credential-issuer`,
201
+ // RFC 8414
202
+ `${origin}/.well-known/openid-credential-issuer${path}`
203
+ ];
204
+
205
+ let error;
206
+ for(const issuerConfigUrl of issuerConfigUrls) {
207
+ try {
208
+ const config = await discoverIssuer({issuerConfigUrl, agent});
209
+ return config;
210
+ } catch(e) {
211
+ error = e;
212
+ }
213
+ }
214
+ throw error;
215
+ }
216
+
174
217
  export async function signJWT({payload, protectedHeader, signer} = {}) {
175
218
  // encode payload and protected header
176
219
  const b64Payload = base64url.encode(JSON.stringify(payload));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalbazaar/oid4-client",
3
- "version": "3.4.1",
3
+ "version": "3.6.0",
4
4
  "description": "An OID4 (VC + VP) client",
5
5
  "homepage": "https://github.com/digitalbazaar/oid4-client",
6
6
  "author": {