Skip to content

Commit

Permalink
Add <issuer-instance>/credentials/<credentialId> endpoint.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Jul 9, 2024
1 parent b262e07 commit 305b061
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
instead of a single proof on a credential). Each cryptosuite can also have
`options` passed and doing so will additionally prevent clients from overriding
them (e.g., `options.mandatoryPointers`).
- Add `<issuer-instance>/credentials/<credentialId>` endpoint for retrieving
previously issued VCs, provided that those VCs included `credentialStatus`.

### Changed
- **BREAKING**: Management of status list index allocation has been rewritten
Expand Down
1 change: 1 addition & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cfg.documentLoader = {
};

cfg.routes = {
credentials: '/credentials',
credentialsIssue: '/credentials/issue'
};

Expand Down
43 changes: 42 additions & 1 deletion lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import {metering, middleware} from '@bedrock/service-core';
import {asyncHandler} from '@bedrock/express';
import bodyParser from 'body-parser';
import cors from 'cors';
import {getDocumentStore} from './helpers.js';
import {issue} from './issuer.js';
import {issueCredentialBody} from '../schemas/bedrock-vc-issuer.js';
import {logger} from './logger.js';
import {createValidateMiddleware as validate} from '@bedrock/validation';

const {util: {BedrockError}} = bedrock;

// FIXME: remove and apply at top-level application
bedrock.events.on('bedrock-express.configure.bodyParser', app => {
app.use(bodyParser.json({
Expand All @@ -27,8 +30,8 @@ export async function addRoutes({app, service} = {}) {
const cfg = bedrock.config['vc-issuer'];
const baseUrl = `${routePrefix}/:localId`;
const routes = {
credential: `${baseUrl}${cfg.routes.credentials}/:credentialId`,
credentialsIssue: `${baseUrl}${cfg.routes.credentialsIssue}`,
credentialsStatus: `${baseUrl}${cfg.routes.credentialsStatus}`,
publishSlc: `${baseUrl}${cfg.routes.publishSlc}`,
publishTerseSlc: `${baseUrl}${cfg.routes.publishTerseSlc}`,
slc: `${baseUrl}${cfg.routes.slc}`,
Expand All @@ -41,6 +44,44 @@ export async function addRoutes({app, service} = {}) {
uses HTTP signatures + capabilities or OAuth2, not cookies; CSRF is not
possible. */

// return a previously issued VC, if it has `credentialStatus`
app.options(routes.credential, cors());
app.get(
routes.credential,
cors(),
getConfigMiddleware,
middleware.authorizeServiceObjectRequest(),
asyncHandler(async (req, res) => {
try {
const {config} = req.serviceObject;
const {credentialId} = req.params;
const {edvClient} = await getDocumentStore({config});
const {documents: [doc]} = await edvClient.find({
equals: {'meta.credentialId': credentialId}
});
if(!doc) {
throw new BedrockError('Credential not found.', {
name: 'NotFoundError',
details: {
credentialId,
httpStatusCode: 404,
public: true
}
});
}
const {content} = doc;
res.status(200).json({
verifiableCredential: content
});
} catch(error) {
logger.error(error.message, {error});
throw error;
}

// meter operation usage
metering.reportOperationUsage({req});
}));

// issue a VC
app.options(routes.credentialsIssue, cors());
app.post(
Expand Down
11 changes: 10 additions & 1 deletion test/mocha/20-credentials.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*!
* Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as assertions from './assertions.js';
import * as helpers from './helpers.js';
import {agent} from '@bedrock/https-agent';
import {createRequire} from 'node:module';
Expand Down Expand Up @@ -648,11 +649,11 @@ describe('issue APIs', () => {
});
it('issues a valid credential w/ "credentialStatus" and ' +
'suspension status purpose', async () => {
const zcapClient = helpers.createZcapClient({capabilityAgent});
const credential = klona(mockCredential);
let error;
let result;
try {
const zcapClient = helpers.createZcapClient({capabilityAgent});
result = await zcapClient.write({
url: `${bslSuspensionIssuerId}/credentials/issue`,
capability: bslSuspensionRootZcap,
Expand All @@ -679,6 +680,14 @@ describe('issue APIs', () => {
should.exist(verifiableCredential.credentialStatus);
should.exist(verifiableCredential.proof);
verifiableCredential.proof.should.be.an('object');

await assertions.assertStoredCredential({
configId: bslSuspensionIssuerId,
credentialId: verifiableCredential.id,
zcapClient,
capability: bslSuspensionRootZcap,
expectedCredential: verifiableCredential
});
});
it('issues a valid credential w/ terse "credentialStatus" for ' +
'both revocation and suspension status purpose', async () => {
Expand Down
43 changes: 43 additions & 0 deletions test/mocha/assertions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved.
*/
import {CapabilityAgent} from '@digitalbazaar/webkms-client';
import {createZcapClient} from './helpers.js';

export async function assertStoredCredential({
configId, credentialId, zcapClient, capability, expectedCredential} = {}) {
const url = `${configId}/credentials/${encodeURIComponent(credentialId)}`;

let error;
let result;
try {
result = await zcapClient.read({url, capability});
} catch(e) {
error = e;
}
assertNoError(error);
should.exist(result.data);
should.exist(result.data.verifiableCredential);
result.data.verifiableCredential.should.deep.equal(expectedCredential);

// fail to fetch using unauthorized party
{
let error;
let result;
try {
const secret = crypto.randomUUID();
const handle = 'test';
const capabilityAgent = await CapabilityAgent.fromSecret({
secret, handle
});
const zcapClient = createZcapClient({capabilityAgent});
result = await zcapClient.read({url, capability});
} catch(e) {
error = e;
}
should.not.exist(result);
should.exist(error?.data?.name);
error.status.should.equal(403);
error.data.name.should.equal('NotAllowedError');
}
}

0 comments on commit 305b061

Please sign in to comment.