diff --git a/CHANGELOG.md b/CHANGELOG.md index 5093315..e889f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @digitalbazaar/oid4-client Changelog +## 4.3.0 - 2024-11-dd + +### Added +- Add `getNonce` to client API for use with OID4VCI `nonce_endpoint`. +- Add option to pass `nonce` to `requestCredential(s)`. + ## 4.2.0 - 2024-10-15 ### Changed diff --git a/lib/OID4Client.js b/lib/OID4Client.js index 2cc162d..1e4b8a7 100644 --- a/lib/OID4Client.js +++ b/lib/OID4Client.js @@ -18,8 +18,47 @@ export class OID4Client { this.offer = offer; } + async getNonce({agent, headers = HEADERS} = {}) { + let response; + try { + // get nonce endpoint + const {nonce_endpoint: url} = this.issuerConfig; + if(url === undefined) { + const error = new Error('Credential issuer has no "nonce_endpoint".'); + error.name = 'DataError'; + throw error; + } + if(!url?.startsWith('https://')) { + const error = new Error( + `Nonce endpoint "${url}" does not start with "https://".`); + error.name = 'DataError'; + throw error; + } + + // get nonce + response = await httpClient.post(url, {agent, headers}); + if(!response.data) { + const error = new Error('Nonce response format is not JSON.'); + error.name = 'DataError'; + throw error; + } + if(response.data.c_nonce === undefined) { + const error = new Error('Nonce not provided in response.'); + error.name = 'DataError'; + throw error; + } + } catch(cause) { + const error = new Error('Could not get nonce.', {cause}); + error.name = 'DataError'; + throw error; + } + + const {c_nonce: nonce} = response.data; + return {nonce, response}; + } + async requestCredential({ - credentialDefinition, did, didProofSigner, agent, format = 'ldp_vc' + credentialDefinition, did, didProofSigner, nonce, agent, format = 'ldp_vc' } = {}) { const {issuerConfig, offer} = this; let requests; @@ -41,13 +80,21 @@ export class OID4Client { credential_definition: credentialDefinition }]; } - return this.requestCredentials({requests, did, didProofSigner, agent}); + return this.requestCredentials({ + requests, did, didProofSigner, nonce, agent + }); } async requestCredentials({ - requests, did, didProofSigner, agent, format = 'ldp_vc', + requests, did, didProofSigner, agent, nonce, format = 'ldp_vc', alwaysUseBatchEndpoint = false } = {}) { + // if `nonce` is given, then `did` and `didProofSigner` must also be + if(nonce !== undefined && !(did && didProofSigner)) { + throw new Error( + 'If "nonce" is given then "did" and "didProofSigner" are required.'); + } + const {issuerConfig, offer} = this; if(requests === undefined && offer) { requests = _createCredentialRequestsFromOffer({ @@ -61,7 +108,8 @@ export class OID4Client { requests = requests.map(r => ({format, ...r})); try { - /* First send credential request(s) to DS without DID proof JWT, e.g.: + /* First send credential request(s) to DS without DID proof JWT (unless + `nonce` is given) e.g.: POST /credential HTTP/1.1 Host: server.example.com @@ -71,7 +119,7 @@ export class OID4Client { { "format": "ldp_vc", "credential_definition": {...}, - // only present on retry after server requests it + // only present on retry after server requests it or if nonce is given "proof": { "proof_type": "jwt", "jwt": "eyJraW..." @@ -109,6 +157,11 @@ export class OID4Client { json = {...requests[0]}; } + if(nonce !== undefined) { + // add DID proof JWT to json + await _addDIDProofJWT({issuerConfig, json, nonce, did, didProofSigner}); + } + let result; const headers = { ...HEADERS, @@ -149,32 +202,16 @@ export class OID4Client { } // validate that `result` has - const {data: {c_nonce: nonce}} = cause; + let {data: {c_nonce: nonce}} = cause; if(!(nonce && typeof nonce === 'string')) { - const error = new Error('No DID proof challenge specified.'); - error.name = 'DataError'; - throw error; + // try to get a nonce + ({nonce} = await this.getNonce({agent})); } - // generate a DID proof JWT - const {issuer: aud} = this.issuerConfig; - const jwt = await generateDIDProofJWT({ - signer: didProofSigner, - nonce, - // the entity identified by the DID is issuing this JWT - iss: did, - // audience MUST be the target issuer per the OID4VCI spec - aud + // add DID proof JWT to json + await _addDIDProofJWT({ + issuerConfig, json, nonce, did, didProofSigner }); - - // add proof to body to be posted and loop to retry - const proof = {proof_type: 'jwt', jwt}; - if(json.credential_requests) { - json.credential_requests = json.credential_requests.map( - cr => ({...cr, proof})); - } else { - json.proof = proof; - } } } @@ -344,6 +381,30 @@ export class OID4Client { } } +async function _addDIDProofJWT({ + issuerConfig, json, nonce, did, didProofSigner +}) { + // generate a DID proof JWT + const {issuer: aud} = issuerConfig; + const jwt = await generateDIDProofJWT({ + signer: didProofSigner, + nonce, + // the entity identified by the DID is issuing this JWT + iss: did, + // audience MUST be the target issuer per the OID4VCI spec + aud + }); + + // add proof to body to be posted and loop to retry + const proof = {proof_type: 'jwt', jwt}; + if(json.credential_requests) { + json.credential_requests = json.credential_requests.map( + cr => ({...cr, proof})); + } else { + json.proof = proof; + } +} + function _assertRequest(request) { if(!(request && typeof request === 'object')) { throw new TypeError('"request" must be an object.');