From b501ad8ba9fa10ade5a56f08fc4f4373344e4b78 Mon Sep 17 00:00:00 2001 From: Adewale Abati Date: Wed, 14 Aug 2024 20:02:59 +0200 Subject: [PATCH 1/7] Adding KCC prototype --- javascript/tbdex-pfi-exemplar/Dockerfile | 2 + .../src/example/idv-vendor/server.ts | 43 ++++ .../src/example/issuer/server.ts | 218 ++++++++++++++++++ .../example/kcc-wallet/0-create-did-dht.js | 7 + .../1-resolve-idv-service-endpoint.js | 12 + .../kcc-wallet/2-request-siopv2-request.js | 6 + .../kcc-wallet/3-submit-siopv2-response.js | 29 +++ .../4-simulate-idv-form-submission.js | 19 ++ .../5-fetch-credential-issuer-metadata.js | 6 + .../6-fetch-authorization-server-metadata.js | 6 + .../kcc-wallet/7-request-access-token.js | 21 ++ .../kcc-wallet/8-request-credential.js | 38 +++ .../src/example/kcc-wallet/full-flow.sh | 36 +++ .../tbdex-pfi-exemplar/src/example/utils.ts | 45 ++-- 14 files changed, 474 insertions(+), 14 deletions(-) create mode 100644 javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts create mode 100644 javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/0-create-did-dht.js create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/1-resolve-idv-service-endpoint.js create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/2-request-siopv2-request.js create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/3-submit-siopv2-response.js create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/4-simulate-idv-form-submission.js create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/5-fetch-credential-issuer-metadata.js create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/6-fetch-authorization-server-metadata.js create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/7-request-access-token.js create mode 100644 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/8-request-credential.js create mode 100755 javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh diff --git a/javascript/tbdex-pfi-exemplar/Dockerfile b/javascript/tbdex-pfi-exemplar/Dockerfile index 5279a49e..3a900b31 100644 --- a/javascript/tbdex-pfi-exemplar/Dockerfile +++ b/javascript/tbdex-pfi-exemplar/Dockerfile @@ -16,6 +16,8 @@ RUN curl -fsSL https://github.com/amacneil/dbmate/releases/download/v1.12.1/dbma && mv dbmate /usr/local/bin EXPOSE 9000 +EXPOSE 3001 +EXPOSE 3002 # Command to run the migration script, seed the database, and then start the application CMD [ "/home/node/app/container-start.sh" ] \ No newline at end of file diff --git a/javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts b/javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts new file mode 100644 index 00000000..f5b571ac --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts @@ -0,0 +1,43 @@ +import express from 'express' + +const app = express() +const port = 3002 + +app.use(express.json()) + +app.get('/idv-form', (req, res) => { + // todo this is where an HTML page could be returned + res.status(200).send('GET request to the /idv-form endpoint') +}) + +app.post('/idv-form', async (req, res) => { + const r = await fetch('http://localhost:3001/idv/result', { + method: 'POST', + headers: {'content-type': 'application/json'}, + body: JSON.stringify({applicantDid: req.body.didUri}) + }) + if (r.status !== 200) { + res.status(500).end() + return + } + + res.status(201).end() +}) + +let server + +export function startIDVServer() { + server = app.listen(port, () => { + console.log(`IDV server listening at http://localhost:${port}`) + }) +} + +export function stopIDVServer() { + if(server) { + server.close(() => { + console.log('IDV server stopped') + }) + } else { + console.log('IDV server not running') + } +} \ No newline at end of file diff --git a/javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts b/javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts new file mode 100644 index 00000000..435678ed --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts @@ -0,0 +1,218 @@ +import { createOrLoadDid } from '../utils.js' +import express from 'express' +import fs from 'fs' +import crypto from 'crypto' +import {Jwt, VerifiableCredential} from '@web5/credentials' +// +// We need to create an issuer, which will issue VCs to the customer, and is trusted by the PFI. +// This issuer DID has the IDV type service. +const issuerDid = await createOrLoadDid('issuer.json') + +const app = express() +const PORT = 3001 + +console.log('\nIssuer did:', issuerDid.uri) +// write issuer.uri to issuer.txt +fs.writeFileSync('issuerDid.txt', issuerDid.uri) + + +app.use(express.json()) + +let idvResults = {} +let payload + +/** called by wallet/2-request-siopv2-request.js */ +app.get('/siopv2/auth-request', (_, res) => { + const siopv2AuthRequest = { + client_id: issuerDid.uri, + response_type: 'id_token', // NOTE: we don't support vp_token in this proto-exemplar + nonce: crypto.randomBytes(16).toString('hex'), + scope: 'openid', + response_mode: 'direct_post', + response_uri: `http://localhost:${PORT}/siopv2/auth-response`, + client_metadata: { + subject_syntax_types_supported: 'did:dht did:jwk did:web' + } + } + + res.status(200).json(siopv2AuthRequest) +}) + +/** called by wallet/3-submit-siopv2-response.js */ +app.post('/siopv2/auth-response', async (req, res) => { + try { + ({payload} = await Jwt.verify({ jwt: req.body.id_token })) + if (!payload.nonce) { + // todo implement your custom nonce verification logic + console.error('Nonce invalid') + res.status(403).end() + return + } + } catch { + res.status(401).json({ + error: 'invalid_token', + error_description: 'Token verification failed' + }) + return + } + + const credentialOffer = { + credential_issuer: `http://localhost:${PORT}`, + credential_configuration_ids: ['KnownCustomerCredential'], + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': crypto.randomBytes(16).toString('hex') + } + } + } + + idvResults[payload.iss] = 'idv-pending' + + const idvRequest = { + credential_offer: credentialOffer, + url: 'http://localhost:3002/idv-form' // the url for the idv-vendor/ project + } + res.status(201).json(idvRequest) +}) + + +app.get('/.well-known/openid-credential-issuer', (_, res) => { + const credentialIssuerMetadata = { + credential_issuer: `http://localhost:${PORT}`, + credential_endpoint: `http://localhost:${PORT}/oid4vci/credential`, + credential_configurations_supported: { + format: 'jwt_vc_json', + cryptographic_binding_methods_supported: ['did:web', 'did:jwk', 'did:dht'], + credential_signing_alg_values_supported: ['EdDSA', 'ES256K'], + proof_types_supported: {jwt: {proof_signing_alg_values_supported: ['EdDSA', 'ES256K']}} + } + } + + res.status(200).json(credentialIssuerMetadata) +}) + +app.get('/.well-known/oauth-authorization-server', (_, res) => { + const authorizationServerMetadata = { + issuer: `http://localhost:${PORT}`, + token_endpoint: `http://localhost:${PORT}/oid4vci/token` + } + + res.status(200).json(authorizationServerMetadata) +}) + +app.post('/idv/result', (req, res) => { + idvResults[req.body.applicantDid] = 'idv-completed' + res.status(201).end() +}) + +app.post('/oid4vci/token', async (req, res) => { + if (req.body.grant_type !== 'urn:ietf:params:oauth:grant-type:pre-authorized_code') { + res.status(400).json({ + error: 'unsupported_grant_type', + error_description: 'The authorization grant type is not supported by the authorization server', + }) + return + } + + // todo verify pre-auth code + // todo it should be validated against the provided client_id + if (req.body['pre-authorized_code'] === '') { + res.status(400).json({ + 'error': 'invalid_grant', + 'error_description': 'The provided pre-auth code is invalid', + }) + return + } + + if (idvResults[req.body.client_id] === 'idv-pending') { + res.status(428).json({ + 'error': 'authorization_pending', + 'error_description': 'Still waiting to hear back from the IDV submission', + }) + return + } + + const exp = Math.floor(Date.now() / 1000) + (30 * 60) // plus 30 minutes + const claims = { + iss: issuerDid.uri, + sub: req.body.client_id, + iat: Math.floor(Date.now() / 1000), + exp + } + // TODO the JWT typ header will not be properly set + const accessTokenJwt = await Jwt.sign({signerDid: issuerDid, payload: claims}) + + res.status(200).json({ + access_token: accessTokenJwt, + token_type: 'bearer', + expires_in: exp, + c_nonce: crypto.randomBytes(16).toString('hex'), + c_nonce_expires_in: 30 * 60 // 30 minutes + }) +}) + +app.post('/oid4vci/credential', async (req, res) => { + try { + const accessToken = req.headers['authorization'].split(' ')[1] + await Jwt.verify({jwt: accessToken}) + } catch { + res.status(401).end() + return + } + + let applicantDid + try { + const {payload} = await Jwt.verify({jwt: req.body.proof.jwt}) + applicantDid = payload.iss + if (!payload.nonce) { + // todo implement your custom nonce verification logic + console.error('Nonce invalid') + res.status(403).end() + return + } + } catch (e) { + res.status(401).end() + return + } + + const vc = await VerifiableCredential.create({ + type : 'KnownCustomerCredential', + issuer : issuerDid.uri, + subject : applicantDid, + data : { + something: 'relevant' + }, + }) + const vcJwt = await vc.sign({did: issuerDid}) + + res.status(200).json({credential: vcJwt}) +}) + +let server + +export function startIssuerServer() { + server = app.listen(PORT, () => { + console.log(`Issuer server listening at http://localhost:${PORT}`) + }) +} + +export function stopIssuerServer() { + if(server) { + server.close(() => { + console.log('Issuer server stopped') + }) + } else { + console.log('Issuer server not running') + } +} + +console.log(` + +now run: + +> npm run seed-offerings + +to seed the PFI with the list of offerings along with this sanctions issuer did. + +This will ensure that the PFI will trust SanctionsCredentials issued by this issuer for RFQs against those offerings. +`) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/0-create-did-dht.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/0-create-did-dht.js new file mode 100644 index 00000000..a45b7721 --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/0-create-did-dht.js @@ -0,0 +1,7 @@ +import {DidDht} from '@web5/dids' +import fs from 'fs' + +const bearerDid = await DidDht.create() +const portableDid = await bearerDid.export() + +fs.writeFileSync('./portable-did.json', JSON.stringify(portableDid, null, 2)) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/1-resolve-idv-service-endpoint.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/1-resolve-idv-service-endpoint.js new file mode 100644 index 00000000..75b087fc --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/1-resolve-idv-service-endpoint.js @@ -0,0 +1,12 @@ +import {DidDht} from '@web5/dids' + +const didUri = process.argv[2] + +const resolution = await DidDht.resolve(didUri) +if (resolution.didResolutionMetadata.error) { + console.error(`Resolution failed ${JSON.stringify(resolution.didResolutionMetadata, null, 2)}`) + process.exit(1) +} + +const idvService = resolution.didDocument.service.find(x => x.type === "IDV") +console.log(idvService.serviceEndpoint) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/2-request-siopv2-request.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/2-request-siopv2-request.js new file mode 100644 index 00000000..7d38b1ca --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/2-request-siopv2-request.js @@ -0,0 +1,6 @@ +const idvEndpoint = process.argv[2] + +const res = await fetch(idvEndpoint) +const body = await res.json() + +console.log(JSON.stringify(body, null, 2)) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/3-submit-siopv2-response.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/3-submit-siopv2-response.js new file mode 100644 index 00000000..5bb48ef1 --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/3-submit-siopv2-response.js @@ -0,0 +1,29 @@ +import {Jwt} from '@web5/credentials' +import {DidDht} from '@web5/dids' +import fs from 'fs' + +const bearerDid = await DidDht.import({portableDid: JSON.parse(fs.readFileSync("./portable-did.json"))}) +const siopv2Request = JSON.parse(process.argv[2]) + +const siopv2Response = { + id_token: await Jwt.sign({ + signerDid: bearerDid, + payload: { + iss: bearerDid.uri, + sub: bearerDid.uri, + aud: siopv2Request.client_id, + nonce: siopv2Request.nonce, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + (30 * 60), // plus 30 minutes + } + }) +} + +const res = await fetch(siopv2Request.response_uri, { + method: 'POST', + headers: {'content-type': 'application/json'}, + body: JSON.stringify(siopv2Response) +}) +const body = await res.json() + +console.log(JSON.stringify(body, null, 2)) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/4-simulate-idv-form-submission.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/4-simulate-idv-form-submission.js new file mode 100644 index 00000000..27ee9b54 --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/4-simulate-idv-form-submission.js @@ -0,0 +1,19 @@ +import {DidDht} from '@web5/dids' +import fs from 'fs' + +// this script simulates the user going to the HTML form page and submitting their PIIimport {Jwt} from '@web5/credentials' + +const idvFormURL = process.argv[2] + +const bearerDid = await DidDht.import({portableDid: JSON.parse(fs.readFileSync("./portable-did.json"))}) + +await fetch(idvFormURL, { + method: 'POST', + headers: {'content-type': 'application/json'}, + body: JSON.stringify({ + didUri: bearerDid.uri, + firstName: 'Joe', + lastName: 'Shmoe', + veryPrivateDataBeCareful: 'tractor123' + }) +}) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/5-fetch-credential-issuer-metadata.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/5-fetch-credential-issuer-metadata.js new file mode 100644 index 00000000..6677cdcd --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/5-fetch-credential-issuer-metadata.js @@ -0,0 +1,6 @@ +const credentialOffer = JSON.parse(process.argv[2]) + +const res = await fetch(`${credentialOffer.credential_issuer}/.well-known/openid-credential-issuer`) +const body = await res.json() + +console.log(JSON.stringify(body, null, 2)) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/6-fetch-authorization-server-metadata.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/6-fetch-authorization-server-metadata.js new file mode 100644 index 00000000..bf8073db --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/6-fetch-authorization-server-metadata.js @@ -0,0 +1,6 @@ +const credentialOffer = JSON.parse(process.argv[2]) + +const res = await fetch(`${credentialOffer.credential_issuer}/.well-known/oauth-authorization-server`) +const body = await res.json() + +console.log(JSON.stringify(body, null, 2)) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/7-request-access-token.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/7-request-access-token.js new file mode 100644 index 00000000..b647d208 --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/7-request-access-token.js @@ -0,0 +1,21 @@ +import {DidDht} from '@web5/dids' +import fs from 'fs' + +const credentialOffer = JSON.parse(process.argv[2]) +const authorizationServerMetadata = JSON.parse(process.argv[3]) +const bearerDid = await DidDht.import({portableDid: JSON.parse(fs.readFileSync("./portable-did.json"))}) + +const tokenRequest = { + grant_type: 'urn:ietf:params:oauth:grant-type:pre-authorized_code', + 'pre-authorized_code': credentialOffer.grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']['pre-authorized_code'], + client_id: bearerDid.uri +} + +const res = await fetch(authorizationServerMetadata.token_endpoint, { + method: 'POST', + headers: {'content-type': 'application/json'}, + body: JSON.stringify(tokenRequest) +}) +const body = await res.json() + +console.log(JSON.stringify(body, null, 2)) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/8-request-credential.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/8-request-credential.js new file mode 100644 index 00000000..9903a5b7 --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/8-request-credential.js @@ -0,0 +1,38 @@ +import {DidDht} from '@web5/dids' +import fs from 'fs' +import { Jwt } from '@web5/credentials' + +const issuerDidURi = process.argv[2] +const credentialIssuerMetadata = JSON.parse(process.argv[3]) +const accessTokenResponse = JSON.parse(process.argv[4]) +const bearerDid = await DidDht.import({portableDid: JSON.parse(fs.readFileSync("./portable-did.json"))}) + +const claims = { + iss: bearerDid.uri, + aud: issuerDidURi, + iat: Math.floor(Date.now() / 1000), + nonce: accessTokenResponse.c_nonce +} +// TODO the JWS 'typ' header won't be properly set +const proofJwt = await Jwt.sign({signerDid: bearerDid, payload: claims}) + +const res = await fetch(credentialIssuerMetadata.credential_endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'authorization': `Bearer ${accessTokenResponse.access_token}` + }, + body: JSON.stringify({ + format: 'jwt_vc_json', + proof: { + proof_type: 'jwt', + jwt: proofJwt + } + }) +}) +const body = await res.json() + +console.log(JSON.stringify(body, null, 2) + '\n') + +const {payload} = await Jwt.verify({jwt: body.credential}) +console.log(JSON.stringify(payload.vc, null, 2)) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh new file mode 100755 index 00000000..efedc5d3 --- /dev/null +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -e pipefail + +ISSUER_DID_URI=$(jq -r '.uri' ../issuer/portable-did.json) + +# resolve the did:dht for the IDV service endpoint +echo "Resolving did:dht..." +IDV_INITIATE_ENDPOINT=$(node 1-resolve-idv-service-endpoint.js $ISSUER_DID_URI) + +# request the SIOPv2 Auth Request (https://openid.github.io/SIOPv2/openid-connect-self-issued-v2-wg-draft.html#name-self-issued-openid-provider-a) +echo "Requesting SIOPv2 Auth Request..." +SIOPV2_REQUEST=$(node 2-request-siopv2-request.js $IDV_INITIATE_ENDPOINT) + +# submit the SIOPv2 Auth Response (https://openid.github.io/SIOPv2/openid-connect-self-issued-v2-wg-draft.html#name-self-issued-openid-provider-au) +echo "Submitting SIOPv2 Auth Response..." +IDV_REQUEST=$(node 3-submit-siopv2-response.js "$SIOPV2_REQUEST") +IDV_FORM_URL=$(echo $IDV_REQUEST | jq -r '.url') +CREDENTIAL_OFFER=$(echo $IDV_REQUEST | jq -r '.credential_offer') + +# simulate submitting the form PII data +echo "Simulating IDV Form submission..." +node 4-simulate-idv-form-submission.js $IDV_FORM_URL + +# fetch metadata +echo "Fetching metadata..." +CREDENTIAL_ISSUSER_METADATA=$(node 5-fetch-credential-issuer-metadata.js "$CREDENTIAL_OFFER") +AUTH_SERVER_METADATA=$(node 6-fetch-authorization-server-metadata.js "$CREDENTIAL_OFFER") + +# request access token +echo "Requesting access token..." +ACCESS_TOKEN_RESPONSE=$(node 7-request-access-token.js "$CREDENTIAL_OFFER" "$AUTH_SERVER_METADATA") + +# request the credential +echo -e "Requesting credential...\n" +node 8-request-credential.js $ISSUER_DID_URI "$CREDENTIAL_ISSUSER_METADATA" "$ACCESS_TOKEN_RESPONSE" \ No newline at end of file diff --git a/javascript/tbdex-pfi-exemplar/src/example/utils.ts b/javascript/tbdex-pfi-exemplar/src/example/utils.ts index 22802388..58c6023d 100644 --- a/javascript/tbdex-pfi-exemplar/src/example/utils.ts +++ b/javascript/tbdex-pfi-exemplar/src/example/utils.ts @@ -3,31 +3,48 @@ import fs from 'fs/promises' export async function createOrLoadDid(filename: string, serviceEndpoint: string = 'http://localhost:9000'): Promise { - // Check if the file exists try { + // Attempt to read the existing DID from the file const data = await fs.readFile(filename, 'utf-8') const portableDid: PortableDid = JSON.parse(data) const bearerDid = await DidDht.import({ portableDid }) return bearerDid + } catch (error) { - // If the file doesn't exist, generate a new DID + // If the file doesn't exist, create a new DID if (error.code === 'ENOENT') { - const bearerDid = await DidDht.create(filename.includes('pfi') && { - options: { - services: [ - { - id: 'pfi', - type: 'PFI', - serviceEndpoint: serviceEndpoint - }] - }}) + const services = [] - const portableDid = await bearerDid.export() + // Include PFI service if filename includes 'pfi' + if (filename.includes('pfi')) { + services.push({ + id: 'pfi', + type: 'PFI', + serviceEndpoint: serviceEndpoint, + }) + } + + // Include IDV service if filename includes 'issuer' + if (filename.includes('issuer')) { + services.push({ + id: 'identity-verification-1', + type: 'IDV', + serviceEndpoint: 'http://localhost:3001/siopv2/auth-request', + }) + } + + const options = services.length > 0 ? { options: { services } } : undefined + const bearerDid = await DidDht.create(options) + const portableDid = await bearerDid.export() await fs.writeFile(filename, JSON.stringify(portableDid, null, 2)) + return bearerDid + + } else { + // Handle any other errors + console.error('Error reading from file:', error) } - console.error('Error reading from file:', error) } -} \ No newline at end of file +} From 8435596a487e3fdfec48f2d4a33fb49e1454feec Mon Sep 17 00:00:00 2001 From: Adewale Abati Date: Thu, 15 Aug 2024 13:52:11 +0200 Subject: [PATCH 2/7] complete kcc integration --- .../tbdex-pfi-exemplar/container-start.sh | 11 +++++++++ .../tbdex-pfi-exemplar/docker-compose.yaml | 4 ++-- .../1-resolve-idv-service-endpoint.js | 2 +- .../src/example/kcc-wallet/full-flow.sh | 23 +++++++++++-------- javascript/tbdex-pfi-exemplar/src/main.ts | 7 ++++++ 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/javascript/tbdex-pfi-exemplar/container-start.sh b/javascript/tbdex-pfi-exemplar/container-start.sh index 91659438..fba3f6ba 100755 --- a/javascript/tbdex-pfi-exemplar/container-start.sh +++ b/javascript/tbdex-pfi-exemplar/container-start.sh @@ -1,8 +1,19 @@ #!/bin/bash set -euo pipefail + export DATABASE_URL="postgres://${SEC_DB_USER}:${SEC_DB_PASSWORD}@${SEC_DB_HOST}:${SEC_DB_PORT}/${SEC_DB_NAME}" + set -x export DBMATE_MIGRATIONS_DIR="./db/migrations" + +# Log before running dbmate +echo "Running dbmate to apply migrations..." dbmate --wait --wait-timeout=60s up + +# Log before starting the server +echo "Starting the Node.js server..." + +npm run example-create-issuer +npm run seed-offerings # npm run seed-offerings npm run server \ No newline at end of file diff --git a/javascript/tbdex-pfi-exemplar/docker-compose.yaml b/javascript/tbdex-pfi-exemplar/docker-compose.yaml index 43867789..b2813f77 100644 --- a/javascript/tbdex-pfi-exemplar/docker-compose.yaml +++ b/javascript/tbdex-pfi-exemplar/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "3.8" - services: postgresdb: image: postgres:latest @@ -21,6 +19,8 @@ services: dockerfile: Dockerfile ports: - "9000:9000" + - "3001:3001" + - "3002:3002" environment: NODE_ENV: production # environment info diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/1-resolve-idv-service-endpoint.js b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/1-resolve-idv-service-endpoint.js index 75b087fc..676a5547 100644 --- a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/1-resolve-idv-service-endpoint.js +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/1-resolve-idv-service-endpoint.js @@ -9,4 +9,4 @@ if (resolution.didResolutionMetadata.error) { } const idvService = resolution.didDocument.service.find(x => x.type === "IDV") -console.log(idvService.serviceEndpoint) +console.log(idvService.serviceEndpoint[0]) diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh index efedc5d3..1dd0df41 100755 --- a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh @@ -2,35 +2,40 @@ set -e pipefail -ISSUER_DID_URI=$(jq -r '.uri' ../issuer/portable-did.json) +ISSUER_DID_URI=$(cat issuerDid.txt) +SCRIPT_PATH="./src/example/kcc-wallet/" + +# create DID DHT +echo "Creating DID DHT..." +node "$SCRIPT_PATH/0-create-did-dht.js" # resolve the did:dht for the IDV service endpoint echo "Resolving did:dht..." -IDV_INITIATE_ENDPOINT=$(node 1-resolve-idv-service-endpoint.js $ISSUER_DID_URI) +IDV_INITIATE_ENDPOINT=$(node "$SCRIPT_PATH/1-resolve-idv-service-endpoint.js" $ISSUER_DID_URI) # request the SIOPv2 Auth Request (https://openid.github.io/SIOPv2/openid-connect-self-issued-v2-wg-draft.html#name-self-issued-openid-provider-a) echo "Requesting SIOPv2 Auth Request..." -SIOPV2_REQUEST=$(node 2-request-siopv2-request.js $IDV_INITIATE_ENDPOINT) +SIOPV2_REQUEST=$(node "$SCRIPT_PATH/2-request-siopv2-request.js" $IDV_INITIATE_ENDPOINT) # submit the SIOPv2 Auth Response (https://openid.github.io/SIOPv2/openid-connect-self-issued-v2-wg-draft.html#name-self-issued-openid-provider-au) echo "Submitting SIOPv2 Auth Response..." -IDV_REQUEST=$(node 3-submit-siopv2-response.js "$SIOPV2_REQUEST") +IDV_REQUEST=$(node "$SCRIPT_PATH/3-submit-siopv2-response.js" "$SIOPV2_REQUEST") IDV_FORM_URL=$(echo $IDV_REQUEST | jq -r '.url') CREDENTIAL_OFFER=$(echo $IDV_REQUEST | jq -r '.credential_offer') # simulate submitting the form PII data echo "Simulating IDV Form submission..." -node 4-simulate-idv-form-submission.js $IDV_FORM_URL +node "$SCRIPT_PATH/4-simulate-idv-form-submission.js" $IDV_FORM_URL # fetch metadata echo "Fetching metadata..." -CREDENTIAL_ISSUSER_METADATA=$(node 5-fetch-credential-issuer-metadata.js "$CREDENTIAL_OFFER") -AUTH_SERVER_METADATA=$(node 6-fetch-authorization-server-metadata.js "$CREDENTIAL_OFFER") +CREDENTIAL_ISSUSER_METADATA=$(node "$SCRIPT_PATH/5-fetch-credential-issuer-metadata.js" "$CREDENTIAL_OFFER") +AUTH_SERVER_METADATA=$(node "$SCRIPT_PATH/6-fetch-authorization-server-metadata.js" "$CREDENTIAL_OFFER") # request access token echo "Requesting access token..." -ACCESS_TOKEN_RESPONSE=$(node 7-request-access-token.js "$CREDENTIAL_OFFER" "$AUTH_SERVER_METADATA") +ACCESS_TOKEN_RESPONSE=$(node "$SCRIPT_PATH/7-request-access-token.js" "$CREDENTIAL_OFFER" "$AUTH_SERVER_METADATA") # request the credential echo -e "Requesting credential...\n" -node 8-request-credential.js $ISSUER_DID_URI "$CREDENTIAL_ISSUSER_METADATA" "$ACCESS_TOKEN_RESPONSE" \ No newline at end of file +node "$SCRIPT_PATH/8-request-credential.js" $ISSUER_DID_URI "$CREDENTIAL_ISSUSER_METADATA" "$ACCESS_TOKEN_RESPONSE" \ No newline at end of file diff --git a/javascript/tbdex-pfi-exemplar/src/main.ts b/javascript/tbdex-pfi-exemplar/src/main.ts index 91fa1d76..133321a5 100644 --- a/javascript/tbdex-pfi-exemplar/src/main.ts +++ b/javascript/tbdex-pfi-exemplar/src/main.ts @@ -16,6 +16,8 @@ import { DidDht } from '@web5/dids' import { BearerDid } from '@web5/dids' import { createOrLoadDid } from './example/utils.js' import { VerifiableCredential } from '@web5/credentials' +import { startIDVServer, stopIDVServer } from './example/idv-vendor/server.js' +import { startIssuerServer, stopIssuerServer } from './example/issuer/server.js' await Postgres.connect() @@ -97,6 +99,9 @@ const server = httpApi.listen(config.port, () => { log.info(`Mock PFI listening on port ${config.port}`) }) +startIDVServer() +startIssuerServer() + console.log('PFI DID FROM SERVER: ', config.pfiDid.uri) httpApi.api.get('/', (req, res) => { @@ -108,6 +113,8 @@ httpApi.api.get('/', (req, res) => { const httpServerShutdownHandler = new HttpServerShutdownHandler(server) function gracefulShutdown() { + stopIDVServer() + stopIssuerServer() httpServerShutdownHandler.stop(async () => { log.info('http server stopped.') From 6c1bf1b6a7734ab3cad827202f26d070b964023b Mon Sep 17 00:00:00 2001 From: Adewale Abati Date: Thu, 15 Aug 2024 16:56:36 +0200 Subject: [PATCH 3/7] Update the readme --- javascript/tbdex-pfi-exemplar/README.md | 25 ++++++++++++++----- javascript/tbdex-pfi-exemplar/package.json | 1 + .../src/example/kcc-wallet/full-flow.sh | 4 +-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/javascript/tbdex-pfi-exemplar/README.md b/javascript/tbdex-pfi-exemplar/README.md index 9a12d289..eaa8b81d 100644 --- a/javascript/tbdex-pfi-exemplar/README.md +++ b/javascript/tbdex-pfi-exemplar/README.md @@ -2,17 +2,21 @@ This is a starter kit for building a **Participating Financial Institution (PFI)** gateway to provide liquidity services on the **[tbDEX](https://developer.tbd.website/projects/tbdex/) network**. You can fork -this and use it (or use it as inspiration!). Contains mock implementations of -some features of a PFI, as well as a **Verifiable Credential (VC)** issuer using a +this and use it (or use it as inspiration!). + +Contains: +- mock implementations of some features of a PFI, +- as well as a **Verifiable Credential (VC)** issuer using a **Decentralized Identifier (DID)**. +- Example mock implementation of a **Known Customer Credential (KCC)** by the issuer using a mock Identity vendor. -Mock TypeScript PFI implementation for example purposes using: +## Mock TypeScript PFI implementation for example purposes using: * [@tbdex/http-server](https://www.npmjs.com/package/@tbdex/http-server) * PostgreSQL as underlying DB -## Running in codesandbox +## Running in CodeSandbox You can run try this example in codesandbox, or locally. To run in codesandbox, use the link below and then open a terminal. Then @@ -93,7 +97,7 @@ npm run example-create-issuer Creates a new VC issuer, which will be needed by the PFI. > [!NOTE] ->`issuer.json` stores the private key info for the issuer, `issuerDid.txt` has the public DID which will be trusted by the PFI. +>`issuer.json` stores the private key info for the issuer, `issuerDid.txt` has the public DID which will be trusted by the PFI. ### Step 3: Configure the PFI database with offerings and VC issuer @@ -128,6 +132,15 @@ Run the server (or restart it) in another terminal window: npm run server ``` +This also runs a mock Identity Vendor (IDV) and Issuer server in the background for KCC. + +### Issue a KCC VC to a Wallet user +Go through an example flow of a wallet Create a DID for a new customer and going through the process of issuing them a Known customer credential. + +```bash +npm run example-wallet-issue-kcc +``` + > [!NOTE] > (optional) If you want to run this over a network, please set HOST environment to an appropriate name that clients can connect to, as this will be set in the PFIs did as a `serviceEndpoint` (otherwise it defaults to http://localhost:9000) @@ -146,7 +159,7 @@ a quote, place an order, and finally check for status. Each interaction happens in the context of an "Exchange" which is a record of the interaction between the customer and the PFI. -This PFI has support for "stored balances", to try this out: +This PFI has support for "stored balances", to try this out: ```bash npm run example-stored-balance diff --git a/javascript/tbdex-pfi-exemplar/package.json b/javascript/tbdex-pfi-exemplar/package.json index 18120d30..4b8eb902 100644 --- a/javascript/tbdex-pfi-exemplar/package.json +++ b/javascript/tbdex-pfi-exemplar/package.json @@ -55,6 +55,7 @@ "example-issue-credential": "npm run _debug -- dist/example/issue-credential.js", "example-e2e-exchange": "npm run _debug -- dist/example/full-tbdex-exchange.js", "example-stored-balance": "npm run _debug -- dist/example/full-stored-balances.js", + "example-wallet-issue-kcc": "bash ./src/example/kcc-wallet/full-flow.sh", "lint": "eslint . --ext .ts --max-warnings 0", "lint:fix": "eslint . --ext .ts --fix", "seed-offerings": "npm run _start -- dist/seed-offerings.js", diff --git a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh index 1dd0df41..34458ceb 100755 --- a/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh +++ b/javascript/tbdex-pfi-exemplar/src/example/kcc-wallet/full-flow.sh @@ -6,11 +6,11 @@ ISSUER_DID_URI=$(cat issuerDid.txt) SCRIPT_PATH="./src/example/kcc-wallet/" # create DID DHT -echo "Creating DID DHT..." +echo "Creating did:dht for customer..." node "$SCRIPT_PATH/0-create-did-dht.js" # resolve the did:dht for the IDV service endpoint -echo "Resolving did:dht..." +echo "Resolving Issuer's did:dht..." IDV_INITIATE_ENDPOINT=$(node "$SCRIPT_PATH/1-resolve-idv-service-endpoint.js" $ISSUER_DID_URI) # request the SIOPv2 Auth Request (https://openid.github.io/SIOPv2/openid-connect-self-issued-v2-wg-draft.html#name-self-issued-openid-provider-a) From c50e5f524c49a44d1ba37656676690a316010dc9 Mon Sep 17 00:00:00 2001 From: Adewale Abati Date: Thu, 15 Aug 2024 17:08:22 +0200 Subject: [PATCH 4/7] set deployment hosts for DIDs --- javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts | 2 +- javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts | 2 +- javascript/tbdex-pfi-exemplar/src/example/utils.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts b/javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts index f5b571ac..5f67483a 100644 --- a/javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts +++ b/javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts @@ -11,7 +11,7 @@ app.get('/idv-form', (req, res) => { }) app.post('/idv-form', async (req, res) => { - const r = await fetch('http://localhost:3001/idv/result', { + const r = await fetch('https://issuer-pfiexemplar.tbddev.org/idv/result', { method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify({applicantDid: req.body.didUri}) diff --git a/javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts b/javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts index 435678ed..9f4d8865 100644 --- a/javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts +++ b/javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts @@ -70,7 +70,7 @@ app.post('/siopv2/auth-response', async (req, res) => { const idvRequest = { credential_offer: credentialOffer, - url: 'http://localhost:3002/idv-form' // the url for the idv-vendor/ project + url: 'https://idv-pfiexemplar.tbddev.org/idv-form' // the url for the idv-vendor/ project } res.status(201).json(idvRequest) }) diff --git a/javascript/tbdex-pfi-exemplar/src/example/utils.ts b/javascript/tbdex-pfi-exemplar/src/example/utils.ts index 58c6023d..2dc2060f 100644 --- a/javascript/tbdex-pfi-exemplar/src/example/utils.ts +++ b/javascript/tbdex-pfi-exemplar/src/example/utils.ts @@ -30,7 +30,7 @@ export async function createOrLoadDid(filename: string, serviceEndpoint: string services.push({ id: 'identity-verification-1', type: 'IDV', - serviceEndpoint: 'http://localhost:3001/siopv2/auth-request', + serviceEndpoint: 'https://issuer-pfiexemplar.tbddev.org/siopv2/auth-request', }) } From fdab2b18f3d9e41cc1de200f1391760430bd1339 Mon Sep 17 00:00:00 2001 From: Adewale Abati Date: Thu, 15 Aug 2024 19:46:20 +0200 Subject: [PATCH 5/7] update offerings protocol and KCC requirement --- javascript/tbdex-pfi-exemplar/src/seed-offerings.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts b/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts index 829b8de1..572f9029 100644 --- a/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts +++ b/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts @@ -16,10 +16,10 @@ async function main() { const issuerDid = await fs.readFile('issuerDid.txt', 'utf8') const offering1 = Offering.create({ - metadata: { from: config.pfiDid.uri }, + metadata: { from: config.pfiDid.uri, protocol: '2.0' }, data: { cancellation: { enabled: false }, - description: 'fake offering 1', + description: 'Exchange USD to KES via MOMO_MPESA', payoutUnitsPerPayinUnit: '0.0069', // ex. we send 100 dollars, so that means 14550.00 KES payin: { currencyCode: 'USD', @@ -114,7 +114,7 @@ async function main() { }) const offering2 = Offering.create({ - metadata: { from: config.pfiDid.uri }, + metadata: { from: config.pfiDid.uri, protocol: '2.0' }, data: { cancellation: { enabled: false }, description: 'USD to USDC wire transfer to stored balance', @@ -142,7 +142,7 @@ async function main() { }) const offering3 = Offering.create({ - metadata: { from: config.pfiDid.uri }, + metadata: { from: config.pfiDid.uri, protocol: '2.0' }, data: { cancellation: { enabled: false }, description: 'USDC to USD wire transfer from stored balance', @@ -170,7 +170,7 @@ async function main() { }) const offering4 = Offering.create({ - metadata: { from: config.pfiDid.uri }, + metadata: { from: config.pfiDid.uri, protocol: '2.0' }, data: { cancellation: { enabled: false }, description: 'Stored balance (in USDC) to MOMO_MPESA', @@ -224,7 +224,7 @@ async function main() { path: ['$.type[*]'], filter: { type: 'string', - pattern: '^SanctionCredential$', + pattern: '^KnownCustomerCredential$', }, }, { From ee58ae01545fe87bcf8a2840eafba324efd5009d Mon Sep 17 00:00:00 2001 From: Adewale Abati Date: Thu, 15 Aug 2024 21:50:32 +0200 Subject: [PATCH 6/7] revert manual protocol specification --- javascript/tbdex-pfi-exemplar/src/seed-offerings.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts b/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts index 572f9029..d384a281 100644 --- a/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts +++ b/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts @@ -16,7 +16,7 @@ async function main() { const issuerDid = await fs.readFile('issuerDid.txt', 'utf8') const offering1 = Offering.create({ - metadata: { from: config.pfiDid.uri, protocol: '2.0' }, + metadata: { from: config.pfiDid.uri }, data: { cancellation: { enabled: false }, description: 'Exchange USD to KES via MOMO_MPESA', @@ -114,7 +114,7 @@ async function main() { }) const offering2 = Offering.create({ - metadata: { from: config.pfiDid.uri, protocol: '2.0' }, + metadata: { from: config.pfiDid.uri}, data: { cancellation: { enabled: false }, description: 'USD to USDC wire transfer to stored balance', @@ -142,7 +142,7 @@ async function main() { }) const offering3 = Offering.create({ - metadata: { from: config.pfiDid.uri, protocol: '2.0' }, + metadata: { from: config.pfiDid.uri }, data: { cancellation: { enabled: false }, description: 'USDC to USD wire transfer from stored balance', @@ -170,7 +170,7 @@ async function main() { }) const offering4 = Offering.create({ - metadata: { from: config.pfiDid.uri, protocol: '2.0' }, + metadata: { from: config.pfiDid.uri }, data: { cancellation: { enabled: false }, description: 'Stored balance (in USDC) to MOMO_MPESA', From f7ecc6bbe3fc686addd7a0bc8dbc1f01afa0fec5 Mon Sep 17 00:00:00 2001 From: Adewale Abati Date: Mon, 19 Aug 2024 20:33:21 +0200 Subject: [PATCH 7/7] Update KCC crede t --- javascript/tbdex-pfi-exemplar/src/seed-offerings.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts b/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts index d384a281..ef1c3f02 100644 --- a/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts +++ b/javascript/tbdex-pfi-exemplar/src/seed-offerings.ts @@ -221,11 +221,13 @@ async function main() { constraints: { fields: [ { - path: ['$.type[*]'], + path: [ + '$.credentialSchema[*].id' + ], filter: { type: 'string', - pattern: '^KnownCustomerCredential$', - }, + const: 'https://vc.schemas.host/kcc.schema.json' + } }, { path: ['$.issuer'],