Skip to content

Commit

Permalink
Improve OID4* errors.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Sep 5, 2024
1 parent 40e02cf commit 2aac40e
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 74 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
188 changes: 122 additions & 66 deletions lib/oid4/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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());
}
20 changes: 15 additions & 5 deletions lib/oid4/oid4vci.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
});
}
2 changes: 1 addition & 1 deletion test/mocha/30-oid4vci.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
2 changes: 1 addition & 1 deletion test/mocha/36-oid4vci-vc-jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
2 changes: 1 addition & 1 deletion test/mocha/backwards-compatibility/30-oid4vci.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

0 comments on commit 2aac40e

Please sign in to comment.