Skip to content

Commit

Permalink
Add nonce_endpoint support.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Nov 11, 2024
1 parent 0883672 commit 6a15d52
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 27 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
115 changes: 88 additions & 27 deletions lib/OID4Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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({
Expand All @@ -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
Expand All @@ -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..."
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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.');
Expand Down

0 comments on commit 6a15d52

Please sign in to comment.