Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Add KCC prototype to PFI Exemplar #39

Merged
merged 7 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions javascript/tbdex-pfi-exemplar/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]
25 changes: 19 additions & 6 deletions javascript/tbdex-pfi-exemplar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions javascript/tbdex-pfi-exemplar/container-start.sh
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions javascript/tbdex-pfi-exemplar/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.8"

services:
postgresdb:
image: postgres:latest
Expand All @@ -21,6 +19,8 @@ services:
dockerfile: Dockerfile
ports:
- "9000:9000"
- "3001:3001"
- "3002:3002"
environment:
NODE_ENV: production
# environment info
Expand Down
1 change: 1 addition & 0 deletions javascript/tbdex-pfi-exemplar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 43 additions & 0 deletions javascript/tbdex-pfi-exemplar/src/example/idv-vendor/server.ts
Original file line number Diff line number Diff line change
@@ -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('https://issuer-pfiexemplar.tbddev.org/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')
}
}
218 changes: 218 additions & 0 deletions javascript/tbdex-pfi-exemplar/src/example/issuer/server.ts
Original file line number Diff line number Diff line change
@@ -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: 'https://idv-pfiexemplar.tbddev.org/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.
`)
Original file line number Diff line number Diff line change
@@ -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))
Original file line number Diff line number Diff line change
@@ -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[0])
Loading
Loading