@digitalbazaar/oid4-client 3.5.0 → 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 +73 -27
- package/lib/index.js +2 -1
- package/lib/util.js +47 -4
- package/package.json +1 -1
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 {
|
|
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
|
|
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 {
|
|
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(
|
|
220
|
-
|
|
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
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
|
380
|
-
|
|
381
|
-
const
|
|
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-
|
|
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/util.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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));
|