diff --git a/CHANGELOG.md b/CHANGELOG.md index c1adf2c..177801f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # bedrock-vc-delivery ChangeLog +## 5.5.0 - 2024-09-dd + +### Changed +- Improve OID4* errors and use OID4* error style with `error` and + `error_description`. + ## 5.4.0 - 2024-09-03 ### Added diff --git a/lib/oid4/http.js b/lib/oid4/http.js index 00875b6..9042c14 100644 --- a/lib/oid4/http.js +++ b/lib/oid4/http.js @@ -156,8 +156,13 @@ export async function createRoutes({ getConfigMiddleware, getExchange, asyncHandler(async (req, res) => { - const response = await oid4vci.processAccessTokenRequest({req, res}); - res.json(response); + let result; + try { + result = await oid4vci.processAccessTokenRequest({req, res}); + } catch(error) { + return _sendOID4Error({res, error}); + } + res.json(result); })); // a credential delivery server endpoint @@ -195,41 +200,46 @@ export async function createRoutes({ } } */ - const result = await oid4vci.processCredentialRequests({ - req, res, isBatchRequest: false - }); - if(!result) { - // DID proof request response sent - return; - } - - // send VC(s) - const { - response: {verifiablePresentation: {verifiableCredential}}, - format - } = result; - // FIXME: "format" doesn't seem to be in the spec anymore (draft 14+)... - const credentials = verifiableCredential.map(vc => { - // parse any enveloped VC - let credential; - if(vc.type === 'EnvelopedVerifiableCredential' && - vc.id?.startsWith('data:application/jwt,')) { - credential = vc.id.slice('data:application/jwt,'.length); - } else { - credential = vc; + let result; + try { + result = await oid4vci.processCredentialRequests({ + req, res, isBatchRequest: false + }); + if(!result) { + // DID proof request response sent + return; } - return credential; - }); - /* Note: The `/credential` route only supports sending VCs of the same - type, but there can be more than one of them. The above `isBatchRequest` - check will ensure that the workflow used here only allows a single - credential request, indicating a single type. */ + // send VC(s) + const { + response: {verifiablePresentation: {verifiableCredential}}, + format + } = result; + // FIXME: "format" doesn't seem to be in the spec anymore (draft 14+)... + const credentials = verifiableCredential.map(vc => { + // parse any enveloped VC + let credential; + if(vc.type === 'EnvelopedVerifiableCredential' && + vc.id?.startsWith('data:application/jwt,')) { + credential = vc.id.slice('data:application/jwt,'.length); + } else { + credential = vc; + } + return credential; + }); + + /* Note: The `/credential` route only supports sending VCs of the same + type, but there can be more than one of them. The above `isBatchRequest` + check will ensure that the workflow used here only allows a single + credential request, indicating a single type. */ - // send OID4VCI response - const response = credentials.length === 1 ? - {format, credential: credentials[0]} : {format, credentials}; - res.json(response); + // send OID4VCI response + result = credentials.length === 1 ? + {format, credential: credentials[0]} : {format, credentials}; + } catch(error) { + return _sendOID4Error({res, error}); + } + res.json(result); })); // a credential delivery server endpoint @@ -240,8 +250,13 @@ export async function createRoutes({ getConfigMiddleware, getExchange, asyncHandler(async (req, res) => { - const offer = await oid4vci.getCredentialOffer({req}); - res.json(offer); + let result; + try { + result = await oid4vci.getCredentialOffer({req}); + } catch(error) { + return _sendOID4Error({res, error}); + } + res.json(result); })); // a batch credential delivery server endpoint @@ -281,29 +296,37 @@ export async function createRoutes({ }] } */ - const result = await oid4vci.processCredentialRequests({ - req, res, isBatchRequest: true - }); - if(!result) { - // DID proof request response sent - return; - } - - // send VCs - const {response: {verifiablePresentation}, format} = result; - // FIXME: "format" doesn't seem to be in the spec anymore (draft 14+)... - const responses = verifiablePresentation.verifiableCredential.map(vc => { - // parse any enveloped VC - let credential; - if(vc.type === 'EnvelopedVerifiableCredential' && - vc.id?.startsWith('data:application/jwt,')) { - credential = vc.id.slice('data:application/jwt,'.length); - } else { - credential = vc; + let result; + try { + result = await oid4vci.processCredentialRequests({ + req, res, isBatchRequest: true + }); + if(!result) { + // DID proof request response sent + return; } - return {format, credential}; - }); - res.json({credential_responses: responses}); + + // send VCs + const { + response: {verifiablePresentation: {verifiableCredential}}, + format + } = result; + // FIXME: "format" doesn't seem to be in the spec anymore (draft 14+)... + result = verifiableCredential.map(vc => { + // parse any enveloped VC + let credential; + if(vc.type === 'EnvelopedVerifiableCredential' && + vc.id?.startsWith('data:application/jwt,')) { + credential = vc.id.slice('data:application/jwt,'.length); + } else { + credential = vc; + } + return {format, credential}; + }); + } catch(error) { + return _sendOID4Error({res, error}); + } + res.json({credential_responses: result}); })); // an OID4VP verifier endpoint @@ -315,13 +338,18 @@ export async function createRoutes({ getConfigMiddleware, getExchange, asyncHandler(async (req, res) => { - const { - authorizationRequest - } = await oid4vp.getAuthorizationRequest({req}); - // construct and send authz request as unsecured JWT - const jwt = new UnsecuredJWT(authorizationRequest).encode(); - res.set('content-type', 'application/oauth-authz-req+jwt'); - res.send(jwt); + let result; + try { + const { + authorizationRequest + } = await oid4vp.getAuthorizationRequest({req}); + // construct and send authz request as unsecured JWT + result = new UnsecuredJWT(authorizationRequest).encode(); + res.set('content-type', 'application/oauth-authz-req+jwt'); + } catch(error) { + return _sendOID4Error({res, error}); + } + res.send(result); })); // an OID4VP verifier endpoint @@ -335,7 +363,35 @@ export async function createRoutes({ getConfigMiddleware, getExchange, asyncHandler(async (req, res) => { - const result = await oid4vp.processAuthorizationResponse({req}); + let result; + try { + result = await oid4vp.processAuthorizationResponse({req}); + } catch(error) { + return _sendOID4Error({res, error}); + } res.json(result); })); } + +function _sendOID4Error({res, error}) { + const status = error.details?.httpStatusCode ?? 500; + const oid4Error = { + error: _camelToSnakeCase(error.name ?? 'OperationError'), + error_description: error.message + }; + if(error?.details?.public) { + oid4Error.details = error.details; + // expose first level cause only + if(oid4Error.cause?.details?.public) { + oid4Error.cause = { + name: error.cause.name, + message: error.cause.message + }; + } + } + res.status(status).json(oid4Error); +} + +function _camelToSnakeCase(s) { + return s.replace(/[A-Z]/g, (c, i) => (i === 0 ? '' : '_') + c.toLowerCase()); +} diff --git a/lib/oid4/oid4vci.js b/lib/oid4/oid4vci.js index 1d588b6..c4c4c42 100644 --- a/lib/oid4/oid4vci.js +++ b/lib/oid4/oid4vci.js @@ -552,9 +552,10 @@ async function _requestDidProof({res, exchangeRecord}) { const {exchange, meta: {expires}} = exchangeRecord; const ttl = Math.floor((expires.getTime() - Date.now()) / 1000); - res.status(400).json({ - error: 'invalid_or_missing_proof', - error_description: + _sendOID4Error({ + res, + error: 'invalid_proof', + description: 'Credential issuer requires proof element in Credential Request', // use exchange ID c_nonce: exchange.id, @@ -588,10 +589,19 @@ async function _requestOID4VP({authorizationRequest, res}) { challenge to be signed is just the exchange ID itself. An exchange cannot be reused and neither can a challenge. */ - res.status(400).json({ + _sendOID4Error({ + res, error: 'presentation_required', - error_description: + description: 'Credential issuer requires presentation before Credential Request', authorization_request: authorizationRequest }); } + +function _sendOID4Error({res, error, description, status = 400, ...rest}) { + res.status(status).json({ + error, + error_description: description, + ...rest + }); +} diff --git a/test/mocha/30-oid4vci.js b/test/mocha/30-oid4vci.js index e317748..0a79c9d 100644 --- a/test/mocha/30-oid4vci.js +++ b/test/mocha/30-oid4vci.js @@ -496,6 +496,6 @@ describe('exchange w/OID4VCI delivery', () => { err = error; } should.exist(err); - should.equal(err?.cause?.data?.name, 'DuplicateError'); + should.equal(err?.cause?.data?.error, 'duplicate_error'); }); }); diff --git a/test/mocha/36-oid4vci-vc-jwt.js b/test/mocha/36-oid4vci-vc-jwt.js index 8dc1e39..9982a03 100644 --- a/test/mocha/36-oid4vci-vc-jwt.js +++ b/test/mocha/36-oid4vci-vc-jwt.js @@ -341,6 +341,6 @@ describe('exchange w/OID4VCI delivery of VC-JWT', () => { err = error; } should.exist(err); - should.equal(err?.cause?.data?.name, 'DuplicateError'); + should.equal(err?.cause?.data?.error, 'duplicate_error'); }); }); diff --git a/test/mocha/backwards-compatibility/30-oid4vci.js b/test/mocha/backwards-compatibility/30-oid4vci.js index 62f99fd..460cc5e 100644 --- a/test/mocha/backwards-compatibility/30-oid4vci.js +++ b/test/mocha/backwards-compatibility/30-oid4vci.js @@ -296,6 +296,6 @@ describe('exchanger backwards-compatibility: ' + err = error; } should.exist(err); - should.equal(err?.cause?.data?.name, 'DuplicateError'); + should.equal(err?.cause?.data?.error, 'duplicate_error'); }); });