From 0e5a44d0325e51b54afa9cddba7fe00c17ed96d8 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 8 May 2024 18:40:15 -0400 Subject: [PATCH 01/11] add healthcheck --- healthcheck.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/app.js | 21 +++++++++++++- src/config.js | 2 +- 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 healthcheck.js diff --git a/healthcheck.js b/healthcheck.js new file mode 100644 index 0000000..e179299 --- /dev/null +++ b/healthcheck.js @@ -0,0 +1,79 @@ +import nodemailer from 'nodemailer' +import axios from 'axios' + +const serviceURL = process.env.HEALTH_CHECK_SERVICE_URL +const serviceName = process.env.HEALTH_CHECK_SERVICE_NAME +const shouldPostToWebHook = process.env.HEALTH_CHECK_WEB_HOOK +const shouldSendEmail = + process.env.HEALTH_CHECK_SMTP_HOST && + process.env.HEALTH_CHECK_SMTP_USER && + process.env.HEALTH_CHECK_SMTP_PASS && + process.env.HEALTH_CHECK_EMAIL_FROM && + process.env.HEALTH_CHECK_EMAIL_RECIPIENT + +axios + .get(serviceURL) + .then(async function (response) { + try { + const body = response.data + if (body.healthy === true) { + process.exit(0) + } + await notify(`${serviceName} is unhealthy: ${body.error}`) + process.exit(1) + } catch (error) { + await notify( + `${serviceName} is potentially unhealthy - error: ${JSON.stringify(error)}` + ) + process.exit(1) + } + }) + .catch(async (error) => { + await notify( + `${serviceName} is unhealthy and will restart after 3 tries. Error: ${error.message}` + ) + process.exit(1) + }) + +async function notify(message) { + console.log(message) + if (shouldSendEmail) await sendMail(message) + if (shouldPostToWebHook) await postToWebHook(message) +} + +async function postToWebHook(text) { + await axios + .post(process.env.HEALTH_CHECK_WEB_HOOK, { text }) + .catch((error) => { + console.error(error) + }) +} + +async function sendMail(message) { + const messageParams = { + from: process.env.HEALTH_CHECK_EMAIL_FROM, + to: process.env.HEALTH_CHECK_EMAIL_RECIPIENT, + subject: process.env.HEALTH_CHECK_EMAIL_SUBJECT, + text: message + } + + const mailTransport = { + host: process.env.HEALTH_CHECK_SMTP_HOST, + auth: { + user: process.env.HEALTH_CHECK_SMTP_USER, + pass: process.env.HEALTH_CHECK_SMTP_PASS + }, + ...(process.env.HEALTH_CHECK_SMTP_PORT && { + port: process.env.HEALTH_CHECK_SMTP_PORT + }) + } + + const transporter = nodemailer.createTransport(mailTransport) + + try { + await transporter.sendMail(messageParams) + } catch (error) { + console.log('the email send error: ') + console.log(error) + } +} diff --git a/src/app.js b/src/app.js index c145fc4..6b7899d 100644 --- a/src/app.js +++ b/src/app.js @@ -6,7 +6,8 @@ import errorHandler from './middleware/errorHandler.js' import errorLogger from './middleware/errorLogger.js' import invalidPathHandler from './middleware/invalidPathHandler.js' import verifyAuthHeader from './verifyAuthHeader.js' -import { getConfig } from './config.js' +import { getConfig, defaultTenantName } from './config.js' +import { getUnsignedVC } from './test-fixtures/vc.js' function IssuingException (code, message, error = null) { this.code = code @@ -27,6 +28,24 @@ export async function build (opts = {}) { app.use(express.urlencoded({ extended: false })) app.use(cors()) + app.get('/healthz', async function (req, res) { + try { + const { data } = await axios.post( + `${req.protocol}://${req.headers.host}/instance/${defaultTenantName}/credentials/issue`, + getUnsignedVC() + ) + if (!data.proof) + throw new SigningException(503, 'issuer-coordinator healthz failed') + } catch (e) { + console.log(`exception in healthz: ${JSON.stringify(e)}`) + return res.status(503).json({ + error: `issuer-coordinator healthz check failed with error: ${e}`, + healthy: false + }) + } + res.send({ message: 'issuer-coordinator server status: ok.', healthy: true }) + }) + app.get('/', async function (req, res, next) { if (enableStatusService) { try { diff --git a/src/config.js b/src/config.js index 62fe589..bc77880 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,6 @@ let CONFIG const defaultPort = 4005 -const defaultTenantName = 'test' +export const defaultTenantName = 'test' const demoTenantName = 'testing' const randomTenantName = 'random' const randtomTenantToken = 'UNPROTECTED' From 58fab59372b59ab3ebeac9215aa72c4631055398 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 9 May 2024 07:13:07 -0400 Subject: [PATCH 02/11] fix lint errors --- healthcheck.js | 6 +++--- src/app.js | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/healthcheck.js b/healthcheck.js index e179299..05dedba 100644 --- a/healthcheck.js +++ b/healthcheck.js @@ -35,13 +35,13 @@ axios process.exit(1) }) -async function notify(message) { +async function notify (message) { console.log(message) if (shouldSendEmail) await sendMail(message) if (shouldPostToWebHook) await postToWebHook(message) } -async function postToWebHook(text) { +async function postToWebHook (text) { await axios .post(process.env.HEALTH_CHECK_WEB_HOOK, { text }) .catch((error) => { @@ -49,7 +49,7 @@ async function postToWebHook(text) { }) } -async function sendMail(message) { +async function sendMail (message) { const messageParams = { from: process.env.HEALTH_CHECK_EMAIL_FROM, to: process.env.HEALTH_CHECK_EMAIL_RECIPIENT, diff --git a/src/app.js b/src/app.js index 6b7899d..3adfe77 100644 --- a/src/app.js +++ b/src/app.js @@ -34,8 +34,7 @@ export async function build (opts = {}) { `${req.protocol}://${req.headers.host}/instance/${defaultTenantName}/credentials/issue`, getUnsignedVC() ) - if (!data.proof) - throw new SigningException(503, 'issuer-coordinator healthz failed') + if (!data.proof) { throw new IssuingException(503, 'issuer-coordinator healthz failed') } } catch (e) { console.log(`exception in healthz: ${JSON.stringify(e)}`) return res.status(503).json({ From 514ae15322fbf7f40f52b72ed4d03a4df8d1a701 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 9 May 2024 08:03:28 -0400 Subject: [PATCH 03/11] add nodemailer and test script --- .dockerignore | 22 +++++++++++++++++++++- .gitignore | 7 ++++++- package-lock.json | 9 +++++++++ package.json | 4 +++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index 28fda52..39a8dac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,22 @@ **/*.env -node_modules \ No newline at end of file +.git +.github +.husky +coverage +logs +node_modules +.dockerignore +.editorconfig +.eslintrc.cjs +.gitignore +.lintstagedrc.json +.prettierignore +.prettierrc.js +compose-test.yaml +compose.yaml +compose-health-test.yaml +Dockerfile +README +server-dev-only.cert +server-dev-only.key + .env.healthcheck.testing \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3d1c3a8..1fa6f36 100644 --- a/.gitignore +++ b/.gitignore @@ -107,4 +107,9 @@ dist .tern-port # vscode -.vscode \ No newline at end of file +.vscode + +compose-test.yaml +compose.yaml +compose-health-test.yaml +.env.healthcheck.testing \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4a5efb3..5d6cdce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dotenv": "^16.0.3", "express": "~4.16.1", "morgan": "~1.9.1", + "nodemailer": "^6.9.13", "winston": "^3.9.0" }, "devDependencies": { @@ -2981,6 +2982,14 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", diff --git a/package.json b/package.json index 755f194..af03100 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "dev-noenv": "nodemon server.js", "lint": "eslint . --ext .js", "lint-fix": "eslint --fix . --ext .js", - "test": "NODE_OPTIONS=--experimental-vm-modules npx mocha --timeout 10000 -r dotenv/config dotenv_config_path=src/test-fixtures/.env.testing src/app.test.js " + "test": "NODE_OPTIONS=--experimental-vm-modules npx mocha --timeout 10000 -r dotenv/config dotenv_config_path=src/test-fixtures/.env.testing src/app.test.js", + "test:health": "node -r dotenv/config healthcheck.js dotenv_config_path=./.env.healthcheck.testing" }, "dependencies": { "axios": "^1.4.0", @@ -18,6 +19,7 @@ "dotenv": "^16.0.3", "express": "~4.16.1", "morgan": "~1.9.1", + "nodemailer": "^6.9.13", "winston": "^3.9.0" }, "devDependencies": { From dcf727acce91e222acc6b8039d9ce85f5806eefe Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 9 May 2024 10:10:34 -0400 Subject: [PATCH 04/11] add healthz tests --- src/app.test.js | 32 ++++++- .../nocks/healthz_status_signing.js | 86 +++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/test-fixtures/nocks/healthz_status_signing.js diff --git a/src/app.test.js b/src/app.test.js index 32fc326..ea51333 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -7,7 +7,7 @@ import protectedNock from './test-fixtures/nocks/protected_status_signing.js' import unprotectedStatusUpdateNock from './test-fixtures/nocks/unprotected_status_update.js' import unknownStatusIdNock from './test-fixtures/nocks/unknown_status_id_nock.js' import protectedStatusUpdateNock from './test-fixtures/nocks/protected_status_update.js' - +import healthzStatusSigningNock from './test-fixtures/nocks/healthz_status_signing.js' import { build } from './app.js' let testTenantToken @@ -228,4 +228,34 @@ describe('api', () => { expect(response.status).to.eql(200) }) }) + + describe('/healthz', () => { + it('returns 200 when healthy', async () => { + healthzStatusSigningNock() + await request(app) + .get(`/healthz`) + .expect('Content-Type', /json/) + .expect((res) => { + expect(res.body.message).to.contain('ok') + }) + .expect(200) + }) + }) + + describe('/healthz fail', () => { + // to force an error with the health check, we + // simply don't set the nock for the signing and + // status services + + it('returns 503 when not healthy', async () => { + await request(app) + .get(`/healthz`) + .expect('Content-Type', /json/) + .expect((res) => { + expect(res.body.error).to.contain('error') + }) + .expect(503) + }) + }) + }) diff --git a/src/test-fixtures/nocks/healthz_status_signing.js b/src/test-fixtures/nocks/healthz_status_signing.js new file mode 100644 index 0000000..f9f543e --- /dev/null +++ b/src/test-fixtures/nocks/healthz_status_signing.js @@ -0,0 +1,86 @@ +import nock from 'nock' +const signedVC = { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', 'https://w3id.org/vc/status-list/2021/v1', 'https://w3id.org/security/suites/ed25519-2020/v1', 'https://w3id.org/vc/status-list/2021/v1'], id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: { id: 'did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy', type: 'Profile', name: 'Dr David Malan', description: 'Gordon McKay Professor of the Practice of Computer Science, Harvard University', url: 'https://cs.harvard.edu/malan/', image: { id: 'https://certificates.cs50.io/static/success.jpg', type: 'Image' } }, issuanceDate: '2020-01-01T00:00:00Z', name: 'Introduction to Computer Science - CS50x', credentialSubject: { type: 'AchievementSubject', identifier: { type: 'IdentityObject', identityHash: 'jc.chartrand@gmail.com', hashed: 'false' }, achievement: { id: 'http://cs50.harvard.edu', type: 'Achievement', criteria: { narrative: 'Completion of CS50X, including ten problem sets, ten labs, and one final project.' }, description: 'CS50 congratulates on completion of CS50x.', name: 'Introduction to Computer Science - CS50x' } }, credentialStatus: { id: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1', type: 'StatusList2021Entry', statusPurpose: 'revocation', statusListIndex: 1, statusListCredential: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB' }, proof: { type: 'Ed25519Signature2020', created: '2023-08-22T20:11:09Z', verificationMethod: 'did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy#z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy', proofPurpose: 'assertionMethod', proofValue: 'z51uH32BFx2mNntaGE55MeHwespoAjetxDkTHBMKtbgGDdc5XiGSTaEGrRgANtT8DV5a6rTNnhT8FKRD4oVnhnxtG' } } +import { defaultTenantName } from '../../config.js' + +export default () => { + nock('http://localhost:4006', { encodedQueryParams: true }) + .post(`/instance/${defaultTenantName}/credentials/sign`, { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', 'https://w3id.org/vc/status-list/2021/v1', 'https://w3id.org/security/suites/ed25519-2020/v1', 'https://w3id.org/vc/status-list/2021/v1'], id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: { id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', type: 'Profile', name: 'Dr David Malan', description: 'Gordon McKay Professor of the Practice of Computer Science, Harvard University', url: 'https://cs.harvard.edu/malan/', image: { id: 'https://certificates.cs50.io/static/success.jpg', type: 'Image' } }, issuanceDate: '2020-01-01T00:00:00Z', name: 'Introduction to Computer Science - CS50x', credentialSubject: { type: 'AchievementSubject', identifier: { type: 'IdentityObject', identityHash: 'jc.chartrand@gmail.com', hashed: 'false' }, achievement: { id: 'http://cs50.harvard.edu', type: 'Achievement', criteria: { narrative: 'Completion of CS50X, including ten problem sets, ten labs, and one final project.' }, description: 'CS50 congratulates on completion of CS50x.', name: 'Introduction to Computer Science - CS50x' } }, credentialStatus: { id: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1', type: 'StatusList2021Entry', statusPurpose: 'revocation', statusListIndex: 1, statusListCredential: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB' } }) + .reply(200, signedVC, [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'application/json; charset=utf-8', + 'Content-Length', + '1810', + 'ETag', + 'W/"712-fUBsd5PM46QPKrivsShMP8gvwtc"', + 'Date', + 'Tue, 22 Aug 2023 20:11:09 GMT', + 'Connection', + 'keep-alive', + 'Keep-Alive', + 'timeout=5' + ]) + nock('http://localhost:4008', { encodedQueryParams: true }) + .post('/credentials/status/allocate', { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', 'https://w3id.org/vc/status-list/2021/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'], id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: { id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', type: 'Profile', name: 'Dr David Malan', description: 'Gordon McKay Professor of the Practice of Computer Science, Harvard University', url: 'https://cs.harvard.edu/malan/', image: { id: 'https://certificates.cs50.io/static/success.jpg', type: 'Image' } }, issuanceDate: '2020-01-01T00:00:00Z', name: 'Introduction to Computer Science - CS50x', credentialSubject: { type: 'AchievementSubject', identifier: { type: 'IdentityObject', identityHash: 'jc.chartrand@gmail.com', hashed: 'false' }, achievement: { id: 'http://cs50.harvard.edu', type: 'Achievement', criteria: { narrative: 'Completion of CS50X, including ten problem sets, ten labs, and one final project.' }, description: 'CS50 congratulates on completion of CS50x.', name: 'Introduction to Computer Science - CS50x' } } }) + .reply(200, { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', 'https://w3id.org/vc/status-list/2021/v1', 'https://w3id.org/security/suites/ed25519-2020/v1', 'https://w3id.org/vc/status-list/2021/v1'], id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: { id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', type: 'Profile', name: 'Dr David Malan', description: 'Gordon McKay Professor of the Practice of Computer Science, Harvard University', url: 'https://cs.harvard.edu/malan/', image: { id: 'https://certificates.cs50.io/static/success.jpg', type: 'Image' } }, issuanceDate: '2020-01-01T00:00:00Z', name: 'Introduction to Computer Science - CS50x', credentialSubject: { type: 'AchievementSubject', identifier: { type: 'IdentityObject', identityHash: 'jc.chartrand@gmail.com', hashed: 'false' }, achievement: { id: 'http://cs50.harvard.edu', type: 'Achievement', criteria: { narrative: 'Completion of CS50X, including ten problem sets, ten labs, and one final project.' }, description: 'CS50 congratulates on completion of CS50x.', name: 'Introduction to Computer Science - CS50x' } }, credentialStatus: { id: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1', type: 'StatusList2021Entry', statusPurpose: 'revocation', statusListIndex: 1, statusListCredential: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB' } }, [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'application/json; charset=utf-8', + 'Content-Length', + '1470', + 'ETag', + 'W/"5be-fsduSOAlXIbTnkhg3Eo5U7uNYRQ"', + 'Date', + 'Tue, 22 Aug 2023 20:11:09 GMT', + 'Connection', + 'keep-alive', + 'Keep-Alive', + 'timeout=5' + ]) + +/* nock('http://127.0.0.1:55225', {"encodedQueryParams":true}) + .post('/instance/un_protected_test/credentials/issue', {"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1","https://w3id.org/security/suites/ed25519-2020/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"Dr David Malan","description":"Gordon McKay Professor of the Practice of Computer Science, Harvard University","url":"https://cs.harvard.edu/malan/","image":{"id":"https://certificates.cs50.io/static/success.jpg","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"Introduction to Computer Science - CS50x","credentialSubject":{"type":"AchievementSubject","identifier":{"type":"IdentityObject","identityHash":"jc.chartrand@gmail.com","hashed":"false"},"achievement":{"id":"http://cs50.harvard.edu","type":"Achievement","criteria":{"narrative":"Completion of CS50X, including ten problem sets, ten labs, and one final project."},"description":"CS50 congratulates on completion of CS50x.","name":"Introduction to Computer Science - CS50x"}}}) + .reply(200, {"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1","https://w3id.org/security/suites/ed25519-2020/v1","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy","type":"Profile","name":"Dr David Malan","description":"Gordon McKay Professor of the Practice of Computer Science, Harvard University","url":"https://cs.harvard.edu/malan/","image":{"id":"https://certificates.cs50.io/static/success.jpg","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"Introduction to Computer Science - CS50x","credentialSubject":{"type":"AchievementSubject","identifier":{"type":"IdentityObject","identityHash":"jc.chartrand@gmail.com","hashed":"false"},"achievement":{"id":"http://cs50.harvard.edu","type":"Achievement","criteria":{"narrative":"Completion of CS50X, including ten problem sets, ten labs, and one final project."},"description":"CS50 congratulates on completion of CS50x.","name":"Introduction to Computer Science - CS50x"}},"credentialStatus":{"id":"https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1","type":"StatusList2021Entry","statusPurpose":"revocation","statusListIndex":1,"statusListCredential":"https://jchartrand.github.io/status-test-three/DKSPRCX9WB"},"proof":{"type":"Ed25519Signature2020","created":"2023-08-22T20:11:09Z","verificationMethod":"did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy#z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy","proofPurpose":"assertionMethod","proofValue":"z51uH32BFx2mNntaGE55MeHwespoAjetxDkTHBMKtbgGDdc5XiGSTaEGrRgANtT8DV5a6rTNnhT8FKRD4oVnhnxtG"}}, [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'application/json; charset=utf-8', + 'Content-Length', + '1810', + 'ETag', + 'W/"712-fUBsd5PM46QPKrivsShMP8gvwtc"', + 'Date', + 'Tue, 22 Aug 2023 20:11:09 GMT', + 'Connection', + 'close' +]); */ +} + +/* export default () => {nock('http://localhost:4006', {"encodedQueryParams":true}) + .post('/instance/testing3/credentials/sign', {"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1","https://w3id.org/security/suites/ed25519-2020/v1","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"Dr David Malan","description":"Gordon McKay Professor of the Practice of Computer Science, Harvard University","url":"https://cs.harvard.edu/malan/","image":{"id":"https://certificates.cs50.io/static/success.jpg","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"Introduction to Computer Science - CS50x","credentialSubject":{"type":"AchievementSubject","identifier":{"type":"IdentityObject","identityHash":"jc.chartrand@gmail.com","hashed":"false"},"achievement":{"id":"http://cs50.harvard.edu","type":"Achievement","criteria":{"narrative":"Completion of CS50X, including ten problem sets, ten labs, and one final project."},"description":"CS50 congratulates on completion of CS50x.","name":"Introduction to Computer Science - CS50x"}},"credentialStatus":{"id":"https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1","type":"StatusList2021Entry","statusPurpose":"revocation","statusListIndex":1,"statusListCredential":"https://jchartrand.github.io/status-test-three/DKSPRCX9WB"}}) + .reply(200, {"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1","https://w3id.org/security/suites/ed25519-2020/v1","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6Mkuoj16AELhDkUk8tvTLA6e6yenGXSNoZ5urtprJoqhuww","type":"Profile","name":"Dr David Malan","description":"Gordon McKay Professor of the Practice of Computer Science, Harvard University","url":"https://cs.harvard.edu/malan/","image":{"id":"https://certificates.cs50.io/static/success.jpg","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"Introduction to Computer Science - CS50x","credentialSubject":{"type":"AchievementSubject","identifier":{"type":"IdentityObject","identityHash":"jc.chartrand@gmail.com","hashed":"false"},"achievement":{"id":"http://cs50.harvard.edu","type":"Achievement","criteria":{"narrative":"Completion of CS50X, including ten problem sets, ten labs, and one final project."},"description":"CS50 congratulates on completion of CS50x.","name":"Introduction to Computer Science - CS50x"}},"credentialStatus":{"id":"https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1","type":"StatusList2021Entry","statusPurpose":"revocation","statusListIndex":1,"statusListCredential":"https://jchartrand.github.io/status-test-three/DKSPRCX9WB"},"proof":{"type":"Ed25519Signature2020","created":"2023-08-03T17:27:29Z","verificationMethod":"did:key:z6Mkuoj16AELhDkUk8tvTLA6e6yenGXSNoZ5urtprJoqhuww#z6Mkuoj16AELhDkUk8tvTLA6e6yenGXSNoZ5urtprJoqhuww","proofPurpose":"assertionMethod","proofValue":"z53EF47PshAVsVtRBTBBv8A1vJvWptWn5p4QupVnAeZYWZJnTGAcABmAVYRZ4CR1xAjWyPrg7ktXerJ9PfUgSLfTh"}}, [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'application/json; charset=utf-8', + 'Content-Length', + '1810', + 'ETag', + 'W/"712-0nL+TtiN38hiHrSNvQHR9Iqira4"', + 'Date', + 'Thu, 03 Aug 2023 17:27:29 GMT', + 'Connection', + 'keep-alive', + 'Keep-Alive', + 'timeout=5' +])} */ From d982afb7c22c14d0fe6a30fc9215d348b67928a7 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 9 May 2024 10:14:59 -0400 Subject: [PATCH 05/11] lint fixes --- src/app.test.js | 7 +++---- src/test-fixtures/nocks/healthz_status_signing.js | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app.test.js b/src/app.test.js index ea51333..7542791 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -233,7 +233,7 @@ describe('api', () => { it('returns 200 when healthy', async () => { healthzStatusSigningNock() await request(app) - .get(`/healthz`) + .get('/healthz') .expect('Content-Type', /json/) .expect((res) => { expect(res.body.message).to.contain('ok') @@ -244,12 +244,12 @@ describe('api', () => { describe('/healthz fail', () => { // to force an error with the health check, we - // simply don't set the nock for the signing and + // simply don't set the nock for the signing and // status services it('returns 503 when not healthy', async () => { await request(app) - .get(`/healthz`) + .get('/healthz') .expect('Content-Type', /json/) .expect((res) => { expect(res.body.error).to.contain('error') @@ -257,5 +257,4 @@ describe('api', () => { .expect(503) }) }) - }) diff --git a/src/test-fixtures/nocks/healthz_status_signing.js b/src/test-fixtures/nocks/healthz_status_signing.js index f9f543e..d92e9b9 100644 --- a/src/test-fixtures/nocks/healthz_status_signing.js +++ b/src/test-fixtures/nocks/healthz_status_signing.js @@ -1,6 +1,6 @@ import nock from 'nock' -const signedVC = { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', 'https://w3id.org/vc/status-list/2021/v1', 'https://w3id.org/security/suites/ed25519-2020/v1', 'https://w3id.org/vc/status-list/2021/v1'], id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: { id: 'did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy', type: 'Profile', name: 'Dr David Malan', description: 'Gordon McKay Professor of the Practice of Computer Science, Harvard University', url: 'https://cs.harvard.edu/malan/', image: { id: 'https://certificates.cs50.io/static/success.jpg', type: 'Image' } }, issuanceDate: '2020-01-01T00:00:00Z', name: 'Introduction to Computer Science - CS50x', credentialSubject: { type: 'AchievementSubject', identifier: { type: 'IdentityObject', identityHash: 'jc.chartrand@gmail.com', hashed: 'false' }, achievement: { id: 'http://cs50.harvard.edu', type: 'Achievement', criteria: { narrative: 'Completion of CS50X, including ten problem sets, ten labs, and one final project.' }, description: 'CS50 congratulates on completion of CS50x.', name: 'Introduction to Computer Science - CS50x' } }, credentialStatus: { id: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1', type: 'StatusList2021Entry', statusPurpose: 'revocation', statusListIndex: 1, statusListCredential: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB' }, proof: { type: 'Ed25519Signature2020', created: '2023-08-22T20:11:09Z', verificationMethod: 'did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy#z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy', proofPurpose: 'assertionMethod', proofValue: 'z51uH32BFx2mNntaGE55MeHwespoAjetxDkTHBMKtbgGDdc5XiGSTaEGrRgANtT8DV5a6rTNnhT8FKRD4oVnhnxtG' } } import { defaultTenantName } from '../../config.js' +const signedVC = { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', 'https://w3id.org/vc/status-list/2021/v1', 'https://w3id.org/security/suites/ed25519-2020/v1', 'https://w3id.org/vc/status-list/2021/v1'], id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: { id: 'did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy', type: 'Profile', name: 'Dr David Malan', description: 'Gordon McKay Professor of the Practice of Computer Science, Harvard University', url: 'https://cs.harvard.edu/malan/', image: { id: 'https://certificates.cs50.io/static/success.jpg', type: 'Image' } }, issuanceDate: '2020-01-01T00:00:00Z', name: 'Introduction to Computer Science - CS50x', credentialSubject: { type: 'AchievementSubject', identifier: { type: 'IdentityObject', identityHash: 'jc.chartrand@gmail.com', hashed: 'false' }, achievement: { id: 'http://cs50.harvard.edu', type: 'Achievement', criteria: { narrative: 'Completion of CS50X, including ten problem sets, ten labs, and one final project.' }, description: 'CS50 congratulates on completion of CS50x.', name: 'Introduction to Computer Science - CS50x' } }, credentialStatus: { id: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1', type: 'StatusList2021Entry', statusPurpose: 'revocation', statusListIndex: 1, statusListCredential: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB' }, proof: { type: 'Ed25519Signature2020', created: '2023-08-22T20:11:09Z', verificationMethod: 'did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy#z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy', proofPurpose: 'assertionMethod', proofValue: 'z51uH32BFx2mNntaGE55MeHwespoAjetxDkTHBMKtbgGDdc5XiGSTaEGrRgANtT8DV5a6rTNnhT8FKRD4oVnhnxtG' } } export default () => { nock('http://localhost:4006', { encodedQueryParams: true }) From b914d00ce9192a674355921f9ef31a30536496e3 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 9 May 2024 17:51:02 -0400 Subject: [PATCH 06/11] update README with healthz --- README.md | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 93c178d..3e8b6ef 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Note that you needn't clone this repository to use the issuer - you can simply r - [Usage](#usage) - [Issuing](#issuing) - [Revoking](#revoking) +- [Health Check](#health-check) - [Learner Credential Wallet](#learner-credential-wallet) - [Development](#development) - [Testing](#testing) @@ -42,6 +43,15 @@ Implements two [VC-API](https://w3c-ccg.github.io/vc-api/) http endpoints: * [POST /credentials/issue](https://w3c-ccg.github.io/vc-api/#issue-credential) * [POST /credentials/status](https://w3c-ccg.github.io/vc-api/#update-status) + and additionally two utility endpoints for generating new dids: + + * [GET /did-key-generator](#didkey) + * [POST /did-web-generator](#didweb) + +and finally an endpoint that returns the health of the service, and is typically meant to be used with Docker HEALTHCHECK: + + * [GET /heathz]() + We've tried hard to make this simple to install and maintain, and correspondingly easy to evaluate and understand as you consider whether digital credentials are useful for your project, and whether this issuer would work for you. In particular, we've separated the discrete parts of an issuer into smaller self-contained apps that are consequently easier to understand and evaluate, and easier to *wire* together to compose functionality. The apps are wired together in a simple docker compose network that pulls images from DockerHub. @@ -64,11 +74,11 @@ Create a file called docker-compose.yml and add the following version: '3.5' services: coordinator: - image: digitalcredentials/issuer-coordinator:0.2.0 + image: digitalcredentials/issuer-coordinator:0.3.0 ports: - "4005:4005" signer: - image: digitalcredentials/signing-service:0.3.0 + image: digitalcredentials/signing-service:0.4.0 ``` ### Run it @@ -196,7 +206,7 @@ NOTE: CURL can get a bit clunky if you want to experiment, so you might consider NOTE: Revocation is not enabled in the Quick Start. You've got to setup a couple of thigs to [enable revocation](#enable-revocation). -Great - you've issued a cryptographically signed credential. Now you'll want to configure the application to issue credentials signed with your own private key (the credential you just issued was signed with a test key that is freely shared so can't be used in production). +Great - you've issued a cryptographically signed credential. Now you'll want to configure the application to issue credentials signed with your own private key (the credential you just issued was signed with a test key that is freely shared so can't be used in production). First a quick word about versioning, and then on to configuration... ## Versioning @@ -278,11 +288,11 @@ TENANT_SEED_ECON101=UNPROTECTED If you set a value other than UNPROTECTED then that value must be included as a Bearer token in the Authorization header of any calls to the endpoint. -We also suggest using IP filtering on your endpoints to only allow set IPs to access the issuer. Set filtering in your nginx or similar. +We also suggest using IP filtering on your endpoints to only allow set IPs to access the issuer. Set filtering in your nginx config, or similar. ##### .signing-service.env -The [signing-service README](https://github.com/digitalcredentials/signing-service/blob/main/README.md#didkey-generator) explains how to set your DID, whether using did:key or did:web. Note that the signing-service docs describe using convenience endpoints to generate new DIDs. You can call those endpoints directly in the signing-serive, or call the same endpoints in the coordinator, as described above in the [Generate a new key section](#generate-a-new-key). The coordinator endpoints simply forward the request to the signing-service. +The [signing-service README](https://github.com/digitalcredentials/signing-service/blob/main/README.md#didkey-generator) explains how to set your DID, whether using did:key or did:web. Note that the signing-service docs describe using convenience endpoints to generate new DIDs. You can call those endpoints directly in the signing-service, or call the same endpoints in the coordinator, as described above in the [Generate a new key section](#generate-a-new-key). The coordinator endpoints simply forward the request to the signing-service. #### Use a tenant @@ -386,7 +396,7 @@ For the moment, the issuer is set up to use the did:key implemenation of a DID w ### did:web -The did:web implementation is likely where many implementations will end up, and so you'll eventually want to move to becuase it allows you to rotate (change) your signing keys whithout having to update every document that points at the old keys. We'll provide did:web support in time, but if you need it now just let us know. +The did:web implementation is likely where many implementations will end up, and so you'll eventually want to move to becuase it allows you to rotate (change) your signing keys whithout having to update every document that points at the old keys. ## Usage @@ -440,6 +450,17 @@ So again, an important point here is that you must store the credentialStatus.id NOTE: you'll of course have to have [set up revocation](#enable-revocation) for this to work. If you've only done the QuickStart then you'll not be able to revoke. +## Health Check + +Docker has a [HEALTHCHECK](https://docs.docker.com/reference/dockerfile/#healthcheck) option for monitoring the +state (health) of a container. We've included an endpoint `GET /healthz` that checks the health of the signing service (by running a test signature). The endpoint can be directly specified in a CURL or WGET call on the HEALTHCHECK, but we also provide a [healthcheck.js](./healthcheck.js) function that can be similarly invoked by the HEALTHCHECK and which itself hits the `healthz` endpoint, and additionally provides options for both email and Slack notifications when the service is unhealthy. + +You can see how we've configured the HEALTHCHECK in our [example compose files](https://github.com/digitalcredentials/docs/blob/main/deployment-guide/DCCDeploymentGuide.md#docker-compose-examples). Our compose files also include an example of how to use [autoheal](https://github.com/willfarrell/docker-autoheal) together with HEALTHCHECK to restart an unhealthy container. + +If you want failing health notifications sent to a Slack channel, you'll have to set up a Slack [web hook](https://api.slack.com/messaging/webhooks). + +If you want failing health notifications sent to an email address, you'll need an SMTP server to which you can send emails, so something like sendgrid, mailchimp, mailgun, or even your own email account if it allows direct SMTP sends. Gmail can apparently be configured to so so. + ## Learner Credential Wallet You might now consider importing your new credential into the [Learner Credential Wallet](https://lcw.app) to see how credentials can be managed and shared from an app based wallet. Simply copy the verifiable credential you just generated and paste it into the text box on the 'add credential' screen of the wallet. From 2ae6266ea0900def70540867e8fa7428d4639bb3 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 May 2024 07:41:53 -0400 Subject: [PATCH 07/11] update config and README --- README.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/config.js | 8 +++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3e8b6ef..a542b55 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Note that you needn't clone this repository to use the issuer - you can simply r - [Usage](#usage) - [Issuing](#issuing) - [Revoking](#revoking) +- [Logging](#logging) - [Health Check](#health-check) - [Learner Credential Wallet](#learner-credential-wallet) - [Development](#development) @@ -222,9 +223,44 @@ If you do ever want to work from the source code in the repository and build you ## Configuration -There are a few things you'll want to configure, in particular setting your own signing keys (so that only you can sign your credentials). Other options include enabling revocation, and allowing for 'multi-tenant' signing, which you might use, for example, to sign credentials for different courses with a different key. +There are a few things you'll want to configure, in particular setting your own signing keys (so that only you can sign your credentials). Other options include enabling revocation, enabling healthchecks, and allowing for 'multi-tenant' signing, which you might use, for example, to sign credentials for different courses with a different key. -The app is configured with three .env files: +Because the issuer-coordinator coordinates calls to other microservices, you'll need to configure both the coordinator itself, and the microservices it calls. + +You can set the environment variables in any of the usual ways that environment variables are set, including .env files or even setting the variables directly in the docker compose yaml file. Our quick start compose files, for example, all set the variables directly in the compose so as to make it possible to start up the compose with a single command. Further below we describe sample .env files for the coorindator and dependent services. + +### Environment Variables + +The variables that can be configured specifically for the issuer-coordinator: + + +TO ADD: + enableAccessLogging: env.ENABLE_ACCESS_LOGGING?.toLowerCase() === 'true', + enableStatusService: env.ENABLE_STATUS_SERVICE?.toLowerCase() === 'true', + statusServiceEndpoint: env.STATUS_SERVICE_ENDPOINT ? env.STATUS_SERVICE_ENDPOINT : defaultStatusServiceEndpoint, + signingServiceEndpoint: env.SIGNING_SERVICE_ENDPOINT ? env.SIGNING_SERVICE_ENDPOINT : defaultSigningServiceEndpoint, + +| Key | Description | Default | Required | +| --- | --- | --- | --- | +| `PORT` | http port on which to run the express app | 4005 | no | +| `ENABLE_HTTPS_FOR_DEV` | runs the dev server over https - ONLY FOR DEV - typically to allow CORS calls from a browser | false | no | +| `TENANT_TOKEN_{TENANT_NAME}` | see [tenants](#tenants) section for instructions | no | no | +| `ENABLE_ACCESS_LOGGING` | log all http calls to the service - see [Logging](#logging) | true | no | +| `ERROR_LOG_FILE` | log file for all errors - see [Logging](#logging) | no | no | +| `LOG_ALL_FILE` | log file for everything - see [Logging](#logging) | no | no | +| `CONSOLE_LOG_LEVEL` | console log level - see [Logging](#logging) | silly | no | +| `LOG_LEVEL` | log level for application - see [Logging](#logging) | silly | no | +| `HEALTH_CHECK_SMTP_HOST` | SMTP host for unhealthy notification emails - see [Health Check](#health-check) | no | no | +| `HEALTH_CHECK_SMTP_USER` | SMTP user for unhealthy notification emails - see [Health Check](#health-check) | no | no | +| `HEALTH_CHECK_SMTP_PASS` | SMTP password for unhealthy notification emails - see [Health Check](#health-check) | no | no | +| `HEALTH_CHECK_EMAIL_FROM` | name of email sender for unhealthy notifications emails - see [Health Check](#health-check) | no | no | +| `HEALTH_CHECK_EMAIL_RECIPIENT` | recipient when unhealthy - see [Health Check](#health-check) | no | no | +| `HEALTH_CHECK_EMAIL_SUBJECT` | email subject when unhealthy - see [Health Check](#health-check) | no | no | +| `HEALTH_CHECK_WEB_HOOK` | posted to when unhealthy - see [Health Check](#health-check) | no | no | +| `HEALTH_CHECK_SERVICE_URL` | local url for this service - see [Health Check](#health-check) | http://SIGNER:4006/healthz | no | +| `HEALTH_CHECK_SERVICE_NAME` | service name to use in error messages - see [Health Check](#health-check) | SIGNING-SERVICE | no | + +The environment variables can be set directly in the docker compose using the ENV directive, or alternatively with three .env files: * [.coordinator.env](./.coordinator.env) * [.signing-service.env](./.signing-service.env) @@ -242,7 +278,6 @@ To issue your own credentials you must generate your own signing key and keep it `curl --location 'http://localhost:4005/did-key-generator'` - #### did:web `curl --location 'http://localhost:4005/did-web-generator'` @@ -450,6 +485,55 @@ So again, an important point here is that you must store the credentialStatus.id NOTE: you'll of course have to have [set up revocation](#enable-revocation) for this to work. If you've only done the QuickStart then you'll not be able to revoke. +## Logging + +We support the following log levels: + +``` + error: 0, + warn: 1, + info: 2, + http: 3, + verbose: 4, + debug: 5, + silly: 6 +``` + +Logging is configured with environment variables, as defined in the [Environment Variables](#environment-variables) section. + +By default, everything is logged to the console (log level `silly`). + +All http calls to the service are logged by default, which might bloat the log. You can disable access logging with: + +```ENABLE_ACCESS_LOGGING=false``` + +You may set the log level for the application as whole, e.g., + +```LOG_LEVEL=http``` + +Which would only log messages with severity 'http' and all below it (info, warn, error). + +The default is to log everything (level 'silly'). + +You can also set the log level for console logging, e.g., + +```CONSOLE_LOG_LEVEL=debug``` + +This would log everything for severity 'debug' and lower (i.e., verbose, http, info, warn, error). This of course assumes that you've set the log level for the application as a whole to at least the same level. + +The default log level for the console is 'silly', which logs everything. + +There are also two log files that can be enabled: + +* errors (only logs errors) +* all (logs everything - all log levels) + +Enable each log by setting an env variable for each, indicating the path to the appropriate file, like this example: + +``` +LOG_ALL_FILE=logs/all.log +ERROR_LOG_FILE=logs/error.log + ## Health Check Docker has a [HEALTHCHECK](https://docs.docker.com/reference/dockerfile/#healthcheck) option for monitoring the diff --git a/src/config.js b/src/config.js index bc77880..1235fa8 100644 --- a/src/config.js +++ b/src/config.js @@ -37,12 +37,16 @@ function parseTenantTokens () { function parseConfig () { const env = process.env const config = Object.freeze({ + port: env.PORT ? parseInt(env.PORT) : defaultPort, enableHttpsForDev: env.ENABLE_HTTPS_FOR_DEV?.toLowerCase() === 'true', enableAccessLogging: env.ENABLE_ACCESS_LOGGING?.toLowerCase() === 'true', + consoleLogLevel: env.CONSOLE_LOG_LEVEL?.toLocaleLowerCase() || defaultConsoleLogLevel, + logLevel: env.LOG_LEVEL?.toLocaleLowerCase() || defaultLogLevel, + errorLogFile: env.ERROR_LOG_FILE, + logAllFile: env.LOG_ALL_FILE, enableStatusService: env.ENABLE_STATUS_SERVICE?.toLowerCase() === 'true', statusServiceEndpoint: env.STATUS_SERVICE_ENDPOINT ? env.STATUS_SERVICE_ENDPOINT : defaultStatusServiceEndpoint, - signingServiceEndpoint: env.SIGNING_SERVICE_ENDPOINT ? env.SIGNING_SERVICE_ENDPOINT : defaultSigningServiceEndpoint, - port: env.PORT ? parseInt(env.PORT) : defaultPort + signingServiceEndpoint: env.SIGNING_SERVICE_ENDPOINT ? env.SIGNING_SERVICE_ENDPOINT : defaultSigningServiceEndpoint }) return config } From 6997bd8e2a96a65ac9b9e2908e7a2e7644bb9597 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 May 2024 09:43:13 -0400 Subject: [PATCH 08/11] fix lint errors --- README.md | 52 ++++++++++++++++++++++++++++++++------------------- src/config.js | 2 ++ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a542b55..5d5a185 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ Note that you needn't clone this repository to use the issuer - you can simply r ## Table of Contents - [Summary](#summary) +- [API](#api) + - [VC-API](#vc-api) + - [DID Generators](#did-generators) + - [healthz endpoint](#healthz-endpoint) - [Quick Start](#quick-start) - [Configuration](#configuration) - [Generate a New Key](#generate-a-new-key) @@ -37,25 +41,11 @@ Note that you needn't clone this repository to use the issuer - you can simply r ## Summary -Use this server to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) with a [revocation status](https://www.w3.org/TR/vc-status-list/) that can later be updated to revoke the credential. +Use this app to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) with a [revocation status](https://www.w3.org/TR/vc-status-list/) that can later be updated to revoke the credential. -Implements two [VC-API](https://w3c-ccg.github.io/vc-api/) http endpoints: +We've tried hard to make this simple to install and maintain, and consequently easy to evaluate and understand as you consider whether digital credentials are useful for your project, and whether this issuer would work for you. - * [POST /credentials/issue](https://w3c-ccg.github.io/vc-api/#issue-credential) - * [POST /credentials/status](https://w3c-ccg.github.io/vc-api/#update-status) - - and additionally two utility endpoints for generating new dids: - - * [GET /did-key-generator](#didkey) - * [POST /did-web-generator](#didweb) - -and finally an endpoint that returns the health of the service, and is typically meant to be used with Docker HEALTHCHECK: - - * [GET /heathz]() - -We've tried hard to make this simple to install and maintain, and correspondingly easy to evaluate and understand as you consider whether digital credentials are useful for your project, and whether this issuer would work for you. - -In particular, we've separated the discrete parts of an issuer into smaller self-contained apps that are consequently easier to understand and evaluate, and easier to *wire* together to compose functionality. The apps are wired together in a simple docker compose network that pulls images from DockerHub. +In particular, we've separated the discrete parts of an issuer into smaller self-contained apps that are therefore easier to understand and evaluate, and easier to *wire* together to compose functionality. The apps are typically wired together in a simple docker compose network that pulls images from DockerHub. We've made installation a gradual process starting with a simple version that can be up and running in about five minutes, and then progressing with configuration as needed. @@ -421,13 +411,13 @@ The `CRED_STATUS_DID_SEED` is set to a default seed, usable by anyone for testin ### DID Registries -To know that a credential was signed with a key that is in fact owned by the claimed issuer, the key (encoded as a DID) has to be confirmed as really belonging to that issuer. This is typically done by adding the DID to a well known registry that the verifier checks when verifying a credential. +To know that a credential was signed with a key that is in fact owned by the claimed issuer, the key (encoded as a [DID](https://www.w3.org/TR/did-core/)) has to be confirmed as really belonging to that issuer. This is typically done by adding the [DID](https://www.w3.org/TR/did-core/) to a well known registry that the verifier checks when verifying a credential. The DCC provides a number of registries that work with the verifiers in the Learner Credential Wallet and in the online web based [Verifier Plus](https://verifierplus.org). The DCC registries use Github for storage. To request that your DID be added to a registry, submit a pull request in which you've added your [DID](https://www.w3.org/TR/did-core/) to the registry file. ### did:key -For the moment, the issuer is set up to use the did:key implemenation of a DID which is one of the simpler implementations and doesn't require that the DID document be hosted anywhere. +For the moment, the issuer is set up to use the did:key implemenation of a [DID](https://www.w3.org/TR/did-core/) which is one of the simpler implementations and doesn't require that the [DID](https://www.w3.org/TR/did-core/) document be hosted anywhere. ### did:web @@ -485,6 +475,30 @@ So again, an important point here is that you must store the credentialStatus.id NOTE: you'll of course have to have [set up revocation](#enable-revocation) for this to work. If you've only done the QuickStart then you'll not be able to revoke. +### API + +#### VC-API + +This app implements two [VC-API](https://w3c-ccg.github.io/vc-api/) http endpoints: + + * [POST /credentials/issue](https://w3c-ccg.github.io/vc-api/#issue-credential) + * [POST /credentials/status](https://w3c-ccg.github.io/vc-api/#update-status) + +The hope is that by following the VC-API spec, you should be able to substitute any implementation of the spec, thereby allowing you to later switch implementations and/or vendors. + +#### DID Generators + +The app additionally two utility endpoints for generating new [DIDs](https://www.w3.org/TR/did-core/): + + * [GET /did-key-generator](#didkey) + * [POST /did-web-generator](#didweb) + +#### healthz endpoint + +and finally an endpoint that returns the health of the service, and is typically meant to be used with Docker [HEALTHCHECK](https://docs.docker.com/reference/dockerfile/#healthcheck): + + * [GET /heathz]() + ## Logging We support the following log levels: diff --git a/src/config.js b/src/config.js index 1235fa8..f9650ec 100644 --- a/src/config.js +++ b/src/config.js @@ -1,5 +1,7 @@ let CONFIG const defaultPort = 4005 +const defaultConsoleLogLevel = 'silly' +const defaultLogLevel = 'silly' export const defaultTenantName = 'test' const demoTenantName = 'testing' const randomTenantName = 'random' From f0d2616a1c9cca61b3dd9655397ff9d52d469291 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 May 2024 09:45:53 -0400 Subject: [PATCH 09/11] update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5d5a185..f6d3415 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Docker have made this straightforward, with [installers for Windows, Mac, and Li ### Make a Docker Compose file -Create a file called docker-compose.yml and add the following +Create a file called compose.yaml and add the following ``` version: '3.5' @@ -74,7 +74,7 @@ services: ### Run it -From the terminal in the same directory that contains your docker-compose.yml file: +From the terminal in the same directory that contains your compose.yaml file: ```docker compose up``` @@ -207,7 +207,7 @@ The images on Docker Hub will of course be updated to add new functionality and We DO NOT provide a `latest` tag so you must provide a tag name (i.e, the version number) for the images in your docker compose file, as we've done [here](./docker-compose.yml). -To ensure you've got compatible versions of the services and the coordinator, the `major` number for each should match. At the time of writing, the versions for each are at 0.2.0, and the `major` number (the leftmost number) agrees across all three. +To ensure you've got compatible versions of the services and the coordinator, take a look at our [sample compose files](https://github.com/digitalcredentials/docs/blob/main/deployment-guide/DCCDeploymentGuide.md#docker-compose-examples). If you do ever want to work from the source code in the repository and build your own images, we've tagged the commits in Github that were used to build the corresponding Docker image. So a github tag of v0.1.0 coresponds to a docker image tag of 0.1.0 From 0ba6eca70172592ba30fde061f23bd247150a9d7 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 14 May 2024 11:31:42 -0400 Subject: [PATCH 10/11] update env section of README --- .coordinator.env | 9 ++++++--- README.md | 43 ++++++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/.coordinator.env b/.coordinator.env index 5c2e093..58dfe79 100644 --- a/.coordinator.env +++ b/.coordinator.env @@ -7,10 +7,12 @@ # default is false # ENABLE_ACCESS_LOGGING=true # default is false -ENABLE_STATUS_SERVICE=true +# ENABLE_STATUS_SERVICE=true # set the service endpoints -# defaults are as follows +# defaults are as follows, +# and point at the service names +# within the docker compose network # STATUS_SERVICE_ENDPOINT=STATUS:4008 # SIGNING_SERVICE_ENDPOINT=SIGNER:4006 @@ -26,7 +28,8 @@ TENANT_TOKEN_UN_PROTECTED_TEST=UNPROTECTED TENANT_TOKEN_PROTECTED_TEST=jds TENANT_TOKEN_RANDOM_TESTING=UNPROTECTED -# The tenant name is specified in the issuing/status invocations like so +# The tenant name is then specified in +# the issuing/status invocations like so # (for tenant name econ101): # http://myhost.org/instance/econ101/credentials/issue # http://myhost.org/instance/econ101/credentials/status diff --git a/README.md b/README.md index f6d3415..7d0170f 100644 --- a/README.md +++ b/README.md @@ -41,17 +41,18 @@ Note that you needn't clone this repository to use the issuer - you can simply r ## Summary -Use this app to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) with a [revocation status](https://www.w3.org/TR/vc-status-list/) that can later be updated to revoke the credential. +Use this app to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) with or without a [revocation status](https://www.w3.org/TR/vc-status-list/) that can later be updated to revoke the credential. We've tried hard to make this simple to install and maintain, and consequently easy to evaluate and understand as you consider whether digital credentials are useful for your project, and whether this issuer would work for you. In particular, we've separated the discrete parts of an issuer into smaller self-contained apps that are therefore easier to understand and evaluate, and easier to *wire* together to compose functionality. The apps are typically wired together in a simple docker compose network that pulls images from DockerHub. -We've made installation a gradual process starting with a simple version that can be up and running in about five minutes, and then progressing with configuration as needed. +We've made installation and evaluation a gradual process starting with a simple version that can be up and running in about five minutes, and then progressing with configuration as needed. ## Quick Start -These four step should take less than five minutes in total: +These four steps should take less than five minutes, and will get you started with your own compose file. Alternatively, we've got a hosted compose file that makes things even a bit easier, the instructions for which are [here](https://github.com/digitalcredentials/docs/blob/main/deployment-guide/DCCDeploymentGuide.md#simple-signing-demo +), but the quick start we now describe is pretty easy too. ### Install Docker @@ -215,21 +216,14 @@ If you do ever want to work from the source code in the repository and build you There are a few things you'll want to configure, in particular setting your own signing keys (so that only you can sign your credentials). Other options include enabling revocation, enabling healthchecks, and allowing for 'multi-tenant' signing, which you might use, for example, to sign credentials for different courses with a different key. -Because the issuer-coordinator coordinates calls to other microservices, you'll need to configure both the coordinator itself, and the microservices it calls. +Because the issuer-coordinator coordinates calls to other microservices, you'll need to configure both the coordinator itself, and the microservices it calls. Read about configuring the status-service in the [Enable Revocation](#enable-revocation) section and read about configuring the signing-service in the [Add a Tenant](#add-a-tenant) section. -You can set the environment variables in any of the usual ways that environment variables are set, including .env files or even setting the variables directly in the docker compose yaml file. Our quick start compose files, for example, all set the variables directly in the compose so as to make it possible to start up the compose with a single command. Further below we describe sample .env files for the coorindator and dependent services. +You can set the environment variables in any of the usual ways that environment variables are set, including .env files or even setting the variables directly in the docker compose yaml file. Our quick start compose files, for example, all set the variables directly in the compose so as to make it possible to start up the compose with a single command. Further below we describe sample .env files for the coordinator and the dependent services. ### Environment Variables The variables that can be configured specifically for the issuer-coordinator: - -TO ADD: - enableAccessLogging: env.ENABLE_ACCESS_LOGGING?.toLowerCase() === 'true', - enableStatusService: env.ENABLE_STATUS_SERVICE?.toLowerCase() === 'true', - statusServiceEndpoint: env.STATUS_SERVICE_ENDPOINT ? env.STATUS_SERVICE_ENDPOINT : defaultStatusServiceEndpoint, - signingServiceEndpoint: env.SIGNING_SERVICE_ENDPOINT ? env.SIGNING_SERVICE_ENDPOINT : defaultSigningServiceEndpoint, - | Key | Description | Default | Required | | --- | --- | --- | --- | | `PORT` | http port on which to run the express app | 4005 | no | @@ -249,20 +243,28 @@ TO ADD: | `HEALTH_CHECK_WEB_HOOK` | posted to when unhealthy - see [Health Check](#health-check) | no | no | | `HEALTH_CHECK_SERVICE_URL` | local url for this service - see [Health Check](#health-check) | http://SIGNER:4006/healthz | no | | `HEALTH_CHECK_SERVICE_NAME` | service name to use in error messages - see [Health Check](#health-check) | SIGNING-SERVICE | no | +| `ENABLE_STATUS_SERVICE` | whether to allocate status - see [Enable Revocation](#enable-revocation) | false | no | +| `STATUS_SERVICE_ENDPOINT` | the endpoint of the status service | STATUS:4008 | no | +| `SIGNING_SERVICE_ENDPOINT` | the endpoint of the signing service | SIGNER:4006 | no | -The environment variables can be set directly in the docker compose using the ENV directive, or alternatively with three .env files: +The environment variables can be set directly in the docker compose using the ENV directive, or alternatively within an .env file like this one: * [.coordinator.env](./.coordinator.env) + +You'll also need .env files for the signing and status services, something like so: + * [.signing-service.env](./.signing-service.env) * [.status-service.env](./.status-service.env) +Note: the env variables for the status service and signing service are described below in the [Enable Revocation](#enable-revocation) and [Add a Tenant](#add-a-tenant) sections respectively. + If you've used the QuickStart docker-compose.yml, then you'll have to change it a bit to point at these files. Alternatively, we've pre-configured this [docker-compose.yml](./docker-compose.yml), though, so you can just use that. The issuer is pre-configured with a preset signing key for testing that can only be used for testing and evaluation. Any credentials signed with this key are meaningless because anyone else can use it to sign credentials, and so could create fake copies of your credentials which would appear to be properly signed. There would be no way to know that it was fake. So, you'll want to add our own key which you do by generating a new key and setting it for a new tenant name. ### Generate a new key -To issue your own credentials you must generate your own signing key and keep it private. We've tried to make that a little easier by providing two convenience endpoints in the issuer that you can use to generate a brand new random key - one using the did:key method and one using the did:web method. You can hit the endpoints with the following CURL command (in a terminal): +To issue your own credentials you must generate your own signing key and keep it private. We've tried to make that a little easier by providing two endpoints in the signing-service that you can use to generate a brand new random key - one using the did:key method and one using the did:web method. You can hit the endpoints directly on the signing-service or if you've got your issuer-coordinator running, you can hit the following convenience endpoints, which simply forward the request to the signer (and return the result) with the following CURL commands (in a terminal): #### did:key @@ -272,9 +274,14 @@ To issue your own credentials you must generate your own signing key and keep it `curl --location 'http://localhost:4005/did-web-generator'` -Both endpoints simply forward your call to the equivalent endpoint in the signing-service. You can read about the endpoints in the [Signing Key section of the signing-service README](https://github.com/digitalcredentials/signing-service/blob/main/README.md#didkey-generator). +Again, both endpoints simply forward your call to the equivalent endpoint in the signing-service. You can read about the endpoints in the [Signing Key section of the signing-service README](https://github.com/digitalcredentials/signing-service/blob/main/README.md#didkey-generator). + +Now that you've got your key you'll need to do two things: -Now that you've got your key you'll want to enable it by adding a new tenant to use the seed... +* register it with the signing-service +* enable it the issuer-coordinator + +We describe both next... ### Tenants @@ -317,7 +324,9 @@ We also suggest using IP filtering on your endpoints to only allow set IPs to ac ##### .signing-service.env -The [signing-service README](https://github.com/digitalcredentials/signing-service/blob/main/README.md#didkey-generator) explains how to set your DID, whether using did:key or did:web. Note that the signing-service docs describe using convenience endpoints to generate new DIDs. You can call those endpoints directly in the signing-service, or call the same endpoints in the coordinator, as described above in the [Generate a new key section](#generate-a-new-key). The coordinator endpoints simply forward the request to the signing-service. +The [signing-service README](https://github.com/digitalcredentials/signing-service/blob/main/README.md#didkey-generator) explains how to set your DID for use by the signing service, whether using did:key or did:web. + +Note that the signing-service docs describe using convenience endpoints to generate new DIDs. You can call those endpoints directly in the signing-service, or call the same endpoints in the issuer-coordinator, as described above in the [Generate a new key section](#generate-a-new-key). The coordinator endpoints simply forward the request to the signing-service. #### Use a tenant From 7bc2462bf37bcb113c06d28b25e6a39832879bb5 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 16 Aug 2024 16:16:45 -0400 Subject: [PATCH 11/11] fix healthz test --- src/app.js | 6 ++---- src/test-fixtures/nocks/healthz_status_signing.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app.js b/src/app.js index 0d706c7..453d3f9 100644 --- a/src/app.js +++ b/src/app.js @@ -39,10 +39,8 @@ export async function build (opts = {}) { app.get('/healthz', async function (req, res) { try { - const { data } = await axios.post( - `${req.protocol}://${req.headers.host}/instance/${defaultTenantName}/credentials/issue`, - getUnsignedVC() - ) + const endpoint = `${req.protocol}://${req.headers.host}/instance/${defaultTenantName}/credentials/issue` + const { data } = await axios.post(endpoint, getUnsignedVC()) if (!data.proof) { throw new IssuingException(503, 'issuer-coordinator healthz failed') } } catch (e) { console.log(`exception in healthz: ${JSON.stringify(e)}`) diff --git a/src/test-fixtures/nocks/healthz_status_signing.js b/src/test-fixtures/nocks/healthz_status_signing.js index d92e9b9..130612b 100644 --- a/src/test-fixtures/nocks/healthz_status_signing.js +++ b/src/test-fixtures/nocks/healthz_status_signing.js @@ -24,7 +24,7 @@ export default () => { 'timeout=5' ]) nock('http://localhost:4008', { encodedQueryParams: true }) - .post('/credentials/status/allocate', { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', 'https://w3id.org/vc/status-list/2021/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'], id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: { id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', type: 'Profile', name: 'Dr David Malan', description: 'Gordon McKay Professor of the Practice of Computer Science, Harvard University', url: 'https://cs.harvard.edu/malan/', image: { id: 'https://certificates.cs50.io/static/success.jpg', type: 'Image' } }, issuanceDate: '2020-01-01T00:00:00Z', name: 'Introduction to Computer Science - CS50x', credentialSubject: { type: 'AchievementSubject', identifier: { type: 'IdentityObject', identityHash: 'jc.chartrand@gmail.com', hashed: 'false' }, achievement: { id: 'http://cs50.harvard.edu', type: 'Achievement', criteria: { narrative: 'Completion of CS50X, including ten problem sets, ten labs, and one final project.' }, description: 'CS50 congratulates on completion of CS50x.', name: 'Introduction to Computer Science - CS50x' } } }) + .post('/credentials/status/allocate') .reply(200, { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', 'https://w3id.org/vc/status-list/2021/v1', 'https://w3id.org/security/suites/ed25519-2020/v1', 'https://w3id.org/vc/status-list/2021/v1'], id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: { id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', type: 'Profile', name: 'Dr David Malan', description: 'Gordon McKay Professor of the Practice of Computer Science, Harvard University', url: 'https://cs.harvard.edu/malan/', image: { id: 'https://certificates.cs50.io/static/success.jpg', type: 'Image' } }, issuanceDate: '2020-01-01T00:00:00Z', name: 'Introduction to Computer Science - CS50x', credentialSubject: { type: 'AchievementSubject', identifier: { type: 'IdentityObject', identityHash: 'jc.chartrand@gmail.com', hashed: 'false' }, achievement: { id: 'http://cs50.harvard.edu', type: 'Achievement', criteria: { narrative: 'Completion of CS50X, including ten problem sets, ten labs, and one final project.' }, description: 'CS50 congratulates on completion of CS50x.', name: 'Introduction to Computer Science - CS50x' } }, credentialStatus: { id: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB#1', type: 'StatusList2021Entry', statusPurpose: 'revocation', statusListIndex: 1, statusListCredential: 'https://jchartrand.github.io/status-test-three/DKSPRCX9WB' } }, [ 'X-Powered-By', 'Express',