Skip to content

Commit dbd5665

Browse files
committed
Use credential configuration to determine proof type in OID4VCI.
1 parent 79c8337 commit dbd5665

2 files changed

Lines changed: 95 additions & 53 deletions

File tree

lib/OID4Client.js

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright (c) 2022-2026 Digital Bazaar, Inc.
33
*/
44
import {
5-
createAuthorizationDetailsFromOffer, createCredentialRequestsFromOffer
5+
createAuthorizationDetailsFromOffer, createConfiguredRequestsFromOffer
66
} from './oid4vci/credentialOffer.js';
77
import {generateDIDProofDIVP, generateDIDProofJWT} from './oid4vci/proofs.js';
88
import {createNamedError} from './util.js';
@@ -82,24 +82,16 @@ export class OID4Client {
8282
async requestCredential({
8383
credentialDefinition, did, didProofSigner, nonce, agent, format = 'ldp_vc'
8484
} = {}) {
85-
const {authorizationDetails, issuerConfig, offer, oid4vciVersion} = this;
8685
let requests;
87-
if(credentialDefinition === undefined) {
88-
if(!offer) {
89-
throw new TypeError('"offer" must be an object.');
90-
}
91-
requests = createCredentialRequestsFromOffer({
92-
issuerConfig, offer, format, authorizationDetails, oid4vciVersion
93-
});
94-
} else {
95-
// OID4VCI Draft 13 only
86+
if(credentialDefinition !== undefined) {
87+
// OID4VCI Draft 13 only...
9688
requests = [{
9789
format,
9890
credential_definition: credentialDefinition
9991
}];
10092
}
10193
return this.requestCredentials({
102-
requests, did, didProofSigner, nonce, agent
94+
requests, did, didProofSigner, nonce, agent, format
10395
});
10496
}
10597

@@ -116,24 +108,51 @@ export class OID4Client {
116108
});
117109
}
118110

111+
// generate configured requests...
112+
let configuredRequests;
113+
114+
// if `requests` given (deprecated style), validate them
115+
// note: `offer` is preferred instead of `requests`
119116
const {authorizationDetails, issuerConfig, offer, oid4vciVersion} = this;
120-
if(requests === undefined && offer) {
121-
requests = createCredentialRequestsFromOffer({
117+
if(requests) {
118+
// OID4VCI Draft 13 only...
119+
if(!(Array.isArray(requests) && requests.length > 0)) {
120+
throw new TypeError('"requests" must be an array of length >= 1.');
121+
}
122+
requests.forEach(_assertRequest);
123+
// map requests to `configuredRequests`; use `undefined` credential
124+
// configuration unless only a single configuration is mentioned; a
125+
// future revision might try to match `credential_definition`, but this
126+
// is probably not worth the effort since this is for draft 13 only
127+
let configuration;
128+
const supportedConfigIds = Object.keys(
129+
issuerConfig.credential_configurations_supported ?? {});
130+
if(supportedConfigIds.length > 0) {
131+
configuration = issuerConfig
132+
.credential_configurations_supported[supportedConfigIds[0]];
133+
}
134+
configuredRequests = requests.map(
135+
request => ({configuration, request}));
136+
} else if(offer) {
137+
configuredRequests = createConfiguredRequestsFromOffer({
122138
issuerConfig, offer, format, authorizationDetails, oid4vciVersion
123139
});
124-
} else if(!(Array.isArray(requests) && requests.length > 0)) {
125-
throw new TypeError('"requests" must be an array of length >= 1.');
140+
} else {
141+
throw new TypeError('"offer" must be an object.');
126142
}
127-
requests.forEach(_assertRequest);
128143

129144
// determine if OID4VCI 1.0+ is to be used
130-
const version1Plus = requests.some(r => r.credential_identifier);
131-
145+
const version1Plus = configuredRequests.some(
146+
({request}) => request.credential_identifier);
132147
if(!version1Plus) {
133148
// set default `format` for requests with `credential_definition`
134149
// (OID4VCI Draft 13 only)
135-
requests = requests.map(
136-
r => r.credential_definition ? {format, ...r} : r);
150+
configuredRequests = configuredRequests.map(
151+
({configuration, request}) => ({
152+
configuration,
153+
request: request.credential_definition ?
154+
{format, ...request} : request
155+
}));
137156
}
138157

139158
try {
@@ -144,13 +163,15 @@ export class OID4Client {
144163
// adding a DID proof, if requested and N-many nonces might be required
145164
// FIXME: use p-queue to manage work
146165
const {credential_endpoint: url} = issuerConfig;
147-
const results = await Promise.all(requests.map(async request => {
148-
const json = {...request};
149-
return _requestCredential({
150-
accessToken, issuerConfig,
151-
url, json, nonce, did, didProofSigner, agent
152-
});
153-
}));
166+
const results = await Promise.all(configuredRequests.map(
167+
async ({configuration, request}) => {
168+
const json = {...request};
169+
return _requestCredential({
170+
accessToken, issuerConfig,
171+
credentialConfiguration: configuration,
172+
url, json, nonce, did, didProofSigner, agent
173+
});
174+
}));
154175
// for backwards compatibility, return all results independently,
155176
// combined, and singular (if applicable)
156177
const credentials = results
@@ -168,6 +189,9 @@ export class OID4Client {
168189
// draft 13...
169190
let url;
170191
let json;
192+
requests = configuredRequests.map(({request}) => request);
193+
// same configuration has to be used for all requested credentials
194+
const configuration = configuredRequests[0]?.configuration;
171195
if(requests.length > 1 || alwaysUseBatchEndpoint) {
172196
({batch_credential_endpoint: url} = issuerConfig);
173197
json = {credential_requests: requests};
@@ -177,6 +201,7 @@ export class OID4Client {
177201
}
178202
result = await _requestCredential({
179203
accessToken, issuerConfig,
204+
credentialConfiguration: configuration,
180205
url, json, nonce, did, didProofSigner, agent
181206
});
182207
}
@@ -232,26 +257,27 @@ export class OID4Client {
232257
}
233258

234259
async function _addDIDProof({
235-
issuerConfig, json, nonce, did, didProofSigner
260+
issuerConfig, credentialConfiguration, json, nonce, did, didProofSigner
236261
}) {
237-
// FIXME: allow these to combine; and choose just one based on
238-
// `proof_types_supported`, defaulting to `jwt` if nothing is specified
239-
await _addDIDProofDIVP({issuerConfig, json, nonce, did, didProofSigner});
240-
// add DID proof `jwt` to json
241-
await _addDIDProofJWT({issuerConfig, json, nonce, did, didProofSigner});
262+
if(credentialConfiguration?.proof_types_supported?.di_vp) {
263+
return _addDIDProofDIVP({issuerConfig, json, nonce, did, didProofSigner});
264+
}
265+
266+
// default to JWT
267+
return _addDIDProofJWT({issuerConfig, json, nonce, did, didProofSigner});
242268
}
243269

244270
async function _addDIDProofDIVP({
245271
issuerConfig, json, nonce, did, didProofSigner
246272
}) {
247273
// generate a DID proof DI VP
248274
const {issuer: domain} = issuerConfig;
249-
const di_vp = [await generateDIDProofDIVP({
275+
const di_vp = await generateDIDProofDIVP({
250276
did,
251277
signer: didProofSigner,
252278
domain,
253279
challenge: nonce
254-
})];
280+
});
255281

256282
// add proof to body to be posted and loop to retry
257283
const proof = {proof_type: 'di_vp', di_vp};
@@ -304,7 +330,8 @@ async function _addDIDProofJWT({
304330
}
305331

306332
async function _requestCredential({
307-
accessToken, issuerConfig, url, json, nonce, did, didProofSigner, agent
333+
accessToken, issuerConfig, credentialConfiguration,
334+
url, json, nonce, did, didProofSigner, agent
308335
}) {
309336
/* First send credential request(s) to DS without DID proof JWT (unless
310337
`nonce` is given) e.g.:
@@ -373,7 +400,9 @@ async function _requestCredential({
373400
*/
374401
if(nonce !== undefined) {
375402
// add DID proof to json
376-
await _addDIDProof({issuerConfig, json, nonce, did, didProofSigner});
403+
await _addDIDProof({
404+
issuerConfig, credentialConfiguration, json, nonce, did, didProofSigner
405+
});
377406
}
378407

379408
let result;
@@ -426,7 +455,9 @@ async function _requestCredential({
426455
}
427456

428457
// add DID proof to json
429-
await _addDIDProof({issuerConfig, json, nonce, did, didProofSigner});
458+
await _addDIDProof({
459+
issuerConfig, credentialConfiguration, json, nonce, did, didProofSigner
460+
});
430461
}
431462
}
432463

lib/oid4vci/credentialOffer.js

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,48 +27,59 @@ export function createAuthorizationDetailsFromOffer({
2727
return authorizationDetails.length > 0 ? authorizationDetails : undefined;
2828
}
2929

30-
export function createCredentialRequestsFromOffer({
30+
export function createConfiguredRequestsFromOffer({
3131
issuerConfig, offer, format, authorizationDetails, oid4vciVersion
3232
} = {}) {
3333
// get credential configs that match `offer` and `format`
3434
const matchingConfigurations = getCredentialConfigurations({
3535
issuerConfig, offer, supportedFormats: [format]
3636
});
3737

38-
// build requests...
39-
let requests;
38+
// build requests and matching meta data...
39+
let configuredRequests;
4040

4141
// the presence of `authorizationDetails` triggers OID4VCI 1.0+ format,
4242
// which uses `credential_identifier` instead of
4343
// Draft 13 `format` + `credential_definition`
4444
if(authorizationDetails && oid4vciVersion !== 'draft13') {
45-
// add a request for each `credential_identifier` mentioned in each
46-
// matching configuration
47-
requests = [];
48-
const matchingIds = new Set(matchingConfigurations.map(({id}) => id));
45+
// add a configured request for each `credential_identifier` mentioned in
46+
// each matching configuration
47+
configuredRequests = [];
48+
const matchMap = new Map(matchingConfigurations.map(
49+
configuration => [configuration.id, configuration]));
4950
for(const element of authorizationDetails) {
5051
const {
5152
type, credential_configuration_id, credential_identifiers = []
5253
} = element;
5354
if(type !== 'openid_credential') {
5455
continue;
5556
}
56-
if(matchingIds.has(credential_configuration_id)) {
57-
requests.push(
58-
...credential_identifiers.map(id => ({credential_identifier: id})));
57+
const configuration = matchMap.get(credential_configuration_id);
58+
if(configuration) {
59+
for(const credential_identifier of credential_identifiers) {
60+
configuredRequests.push({
61+
configuration,
62+
request: {credential_identifier}
63+
});
64+
}
5965
}
6066
}
6167
} else {
6268
// OID4VCI Draft 13 request format
63-
requests = matchingConfigurations.map(
64-
({format, credential_definition}) => ({format, credential_definition}));
69+
configuredRequests = matchingConfigurations.map(configuration => {
70+
const {format, credential_definition} = configuration;
71+
return {
72+
configuration,
73+
request: {format, credential_definition}
74+
};
75+
});
6576
}
66-
if(!(requests?.length > 0)) {
77+
if(!(configuredRequests?.length > 0)) {
6778
throw new Error(
6879
`No supported credential(s) with format "${format}" found.`);
6980
}
7081

71-
return requests;
82+
return configuredRequests;
7283
}
7384

7485
export async function getCredentialOffer({url, agent} = {}) {

0 commit comments

Comments
 (0)