Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add <issuer-instance>/credentials/<credentialId> endpoint. #163

Merged
merged 1 commit into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
}
}
Loading