Skip to content

Commit

Permalink
Merge pull request #81 from verida/feature/veridajs4
Browse files Browse the repository at this point in the history
updates for verida-js v4
  • Loading branch information
nick-verida authored Jul 25, 2024
2 parents 74885a4 + 3dffcc3 commit 213dbe6
Show file tree
Hide file tree
Showing 16 changed files with 352 additions and 668 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
2023-08-22 (2.2.0)
--------------------

Breaking changes:

- `.env` `DID_NETWORK` renamed to `VERIDA_NETWORK`. Represents the Verida network (ie: `myrtle`) this node is operating on.
- Some environment variables have moved out of `.env` and are now hard coded as protocol variables. See `src/config.js`

Other changes:

- Refactor to process replication entries differently depending on those that are missing, broken or need touching (update expiry)
- Log the full replication identifier when replication entry fails to update
- Improve insert / update error logging
Expand Down
10 changes: 1 addition & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ yarn serve

A `sample.env` is included. Copy this to `.env` and update the configuration:

- `VERIDA_NETWORK`: Verida network to use. See https://developers.verida.network/docs/infrastructure/networks for valid networks.
- `DID_CACHE_DURATION`: How long to cache DIDs before reloading
- `VERIDA_NETWORK`: Verida network to use. See https://developers.verida.network/docs/infrastructure/networks for valid networks. (ie: `banksia`)
- `DB_PROTOCOL`: Protocol to use when connecting to CouchDB (`http` or `https`).
- `DB_USER`: Username of CouchDB Admin (has access to create users and databases).
- `DB_PASS`: Password of CouchDB Admin.
Expand All @@ -51,10 +50,7 @@ A `sample.env` is included. Copy this to `.env` and update the configuration:
- `DB_REJECT_UNAUTHORIZED_SSL`: Boolean indicating if unauthorized SSL certificates should be rejected (`true` or `false`). Defaults to `false` for development testing. Must be `true` for production environments otherwise SSL certificates won't be verified.
- `DB_PUBLIC_USER`: Alphanumeric string for a public database user. These credentials can be requested by anyone and provide access to all databases where the permissions have been set to `public`.
- `DB_PUBLIC_PASS`: Alphanumeric string for a public database password.
- `ACCESS_TOKEN_EXPIRY`: Number of seconds before an access token expires. The protocol will use the refresh token to obtain a new access token. CouchDB does not support a way to force the expiry of an issued token, so the access token expiry should always be set to 5 minutes (300)
- `REFRESH_TOKEN_EXPIRY`: Number of seconds before a refresh token expires. Users will be forced to re-login once this time limit is reached. This should be set to 7 days (604800).
- `DB_REFRESH_TOKENS`: Internal CouchDB database that stores refresh tokens (ie: `verida_refresh_tokens`)
- `GC_PERCENT`: How often garbage collection runs on tokens (ie: `0.1` = 10% of requests)
- `ACCESS_JWT_SIGN_PK`: The access token private key. The base64 version of this must be specified in the CouchDB configuration under `jwt_keys/hmac:_default`
- `REFRESH_JWT_SIGN_PK`: The refresh token private key
- `DB_PROTOCOL_INTERNAL`: Internal database protocol (`http` or `https`).
Expand All @@ -65,11 +61,7 @@ A `sample.env` is included. Copy this to `.env` and update the configuration:
- `DB_PORT_INTERNAL`: External database port (ie: `5984`)
- `ENDPOINT_URI`: The public URI of this storage node server (Will match what is stored in DID Documents). Note: Must include the port and have NO trailing slash. (ie: `"http://localhost:5000"`)
- `VDA_PRIVATE_KEY`: Verida network private key as a hex string. Including leading 0x. This is used to sign server responses and in the future, prove VDA tokens are staked for this node. (ie: `0xaaaabbbb...`)
- `DEFAULT_USER_CONTEXT_LIMIT_MB`: Maximum number of Megabytes for a storage context
- `MAX_USERS`: Maximum number of users supported by this node (ie: `10000`)
- `REPLICATION_EXPIRY_MINUTES`: How many minutes before the replication expires on an open database. Should be 2x ACCESS_TOKEN_EXPIRY. (ie: `20`)
- `DB_DIDS`: Database for storing DID documents (ie: `verida_dids`)
- `DB_REPLICATER_CREDS`: Database for storing replication credentials to third party nodes (ie: `verida_replicater_creds`)
- `PORT`: Port this server runs on (ie: `5151`)


Expand Down
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@verida/storage-node",
"version": "2.3.0",
"version": "4.0.0",
"description": "Verida Storage Node middleware that bridges decentralised identities so they can control access to databases within a CouchDB storage engine",
"main": "dist/server.js",
"scripts": {
Expand Down Expand Up @@ -46,9 +46,12 @@
"homepage": "https://github.com/verida/storage-node/README.md",
"dependencies": {
"@babel/runtime": "^7.16.7",
"@verida/did-client": "^3.0.1",
"@verida/did-document": "^3.0.1",
"@verida/encryption-utils": "^3.0.0",
"@verida/did-client": "^4.0.0",
"@verida/did-document": "^4.0.0",
"@verida/encryption-utils": "^4.0.0",
"@verida/types": "^4.0.0",
"@verida/vda-common": "^4.0.0",
"@verida/vda-did-resolver": "^4.0.0",
"aws-serverless-express": "^3.4.0",
"axios": "^1.2.1",
"cors": "^2.8.5",
Expand All @@ -70,8 +73,8 @@
"@babel/plugin-transform-runtime": "^7.16.7",
"@babel/polyfill": "^7.8.0",
"@babel/preset-env": "^7.20.2",
"@verida/account-node": "^2.4.0-rc5",
"@verida/client-ts": "^2.4.0-rc5",
"@verida/account-node": "4.0.0-alpha.0",
"@verida/client-ts": "4.0.0-alpha.0",
"claudia": "^5.14.1",
"ethers": "^5.7.2",
"mocha": "^7.0.0",
Expand Down
21 changes: 5 additions & 16 deletions sample.env
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
VERIDA_NETWORK=banksia
DID_CACHE_DURATION=3600

# Admin username and password (for system operations)
# MUST be set to something random
DB_USER="admin"
DB_PASS="admin"

# Replication username and password (for replicating data to other nodes)
# MUST be set to something random
# MUST not change once the node is operational
Expand All @@ -26,28 +27,16 @@ ENDPOINT_URI="http://localhost:5000"

DB_REJECT_UNAUTHORIZED_SSL=true
ACCESS_JWT_SIGN_PK=insert-random-access-symmetric-key
# 10 Minutes
ACCESS_TOKEN_EXPIRY=600
REFRESH_JWT_SIGN_PK=insert-random-refresh-symmetric-key
# 30 Days
REFRESH_TOKEN_EXPIRY=2592000
DB_REFRESH_TOKENS=verida_refresh_tokens
# How often garbage collection runs (1=100%, 0.5 = 50%)
GC_PERCENT=0.1

# Verida Private Key as hex string (used to sign responses). Including leading 0x.
VDA_PRIVATE_KEY=
# Default maximum number of Megabytes for a storage context
DEFAULT_USER_CONTEXT_LIMIT_MB=10

# Maximum number of users supported by this node
MAX_USERS=10000
# How many minutes before the replication expires on an open database
# Should be 2x ACCESS_TOKEN_EXPIRY
REPLICATION_EXPIRY_MINUTES=20

# Alpha numeric only
DB_PUBLIC_USER=784c2n780c9cn0789
DB_PUBLIC_PASS=784c2n780c9cn0789
DB_DIDS=verida_dids
DB_REPLICATER_CREDS=verida_replicater_creds

PORT=5151
PORT=5151
2 changes: 1 addition & 1 deletion src/build.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const BUILD_DETAILS = {buildTimestamp: "2024-04-05T02:24:03+00:00"};
export const BUILD_DETAILS = {buildTimestamp: "2024-06-23T01:23:06+00:00"};
70 changes: 41 additions & 29 deletions src/components/authManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import randtoken from 'rand-token';
import jwt from 'jsonwebtoken';
import mcache from 'memory-cache';

import { DIDClient } from '@verida/did-client'
import EncryptionUtils from '@verida/encryption-utils';
import Utils from './utils.js';
import Db from './db.js';
import CONFIG from '../config.js'
import dbManager from './dbManager.js';
import { getResolver } from '@verida/vda-did-resolver';
import { DIDDocument } from '@verida/did-document';
import { Resolver } from 'did-resolver';

const vdaDidResolver = getResolver()
const didResolver = new Resolver(vdaDidResolver)

dotenv.config();

Expand Down Expand Up @@ -77,10 +83,10 @@ class AuthManager {
}

const consentMessage = `Authenticate this application context: "${contextName}"?\n\n${did}\n${decodedJwt.authRequestId}`
return this.verifySignedConsentMessage(did, signature, consentMessage)
return this.verifySignedConsentMessage(did, signature, consentMessage, contextName)
}

async verifySignedConsentMessage(did, signature, consentMessage) {
async verifySignedConsentMessage(did, signature, consentMessage, contextName) {
// Verify the signature signed the correct string
try {
const didDocument = await this.getDidDocument(did)
Expand All @@ -89,11 +95,17 @@ class AuthManager {
return false
}

const result = didDocument.verifySig(consentMessage, signature)
// Check signature sourced from context key
const result = didDocument.verifyContextSignature(consentMessage, process.env.VERIDA_NETWORK, contextName, signature)

if (!result) {
console.info('Invalid signature when verifying signed consent message')
return false
// Check signature sourced from master DID key
const result2 = didDocument.verifySig(consentMessage, signature)

if (!result2) {
console.info('Invalid signature when verifying signed consent message')
return false
}
}

return true
Expand All @@ -104,6 +116,14 @@ class AuthManager {
}
}

/**
*
* @todo: Refactor to use @verida/vda-did-resolver, Ensure signature checks verify context
*
* @param {*} did
* @param {*} ignoreCache
* @returns
*/
async getDidDocument(did, ignoreCache=false) {
// Verify the signature signed the correct string
const cacheKey = did
Expand All @@ -116,21 +136,13 @@ class AuthManager {

if (!didDocument) {
console.info(`DID document not in cache: ${did}, fetching`)
if (!didClient) {
console.info(`DID client didn't exist, creating`)
const didClientConfig = {
network: process.env.VERIDA_NETWORK ? process.env.VERIDA_NETWORK : 'banksia',
rpcUrl: process.env.DID_RPC_URL
}

didClient = new DIDClient(didClientConfig);
}

didDocument = await didClient.get(did)

const response = await didResolver.resolve(did)
didDocument = new DIDDocument(response.didDocument)

if (didDocument) {
console.info(`Adding DID document to cache: ${did}`)
const { DID_CACHE_DURATION } = process.env
const { DID_CACHE_DURATION } = CONFIG
mcache.put(cacheKey, didDocument, DID_CACHE_DURATION * 1000)
}
}
Expand Down Expand Up @@ -162,7 +174,7 @@ class AuthManager {

// Set the token to expire
if (!expiresIn) {
expiresIn = parseInt(process.env.REFRESH_TOKEN_EXPIRY)
expiresIn = parseInt(CONFIG.REFRESH_TOKEN_EXPIRY)
}

const deviceHash = EncryptionUtils.hash(`${did}/${contextName}/${deviceId}`)
Expand All @@ -182,7 +194,7 @@ class AuthManager {

// Save refresh token in the database
const couch = Db.getCouch();
const tokenDb = couch.db.use(process.env.DB_REFRESH_TOKENS);
const tokenDb = couch.db.use(CONFIG.DB_REFRESH_TOKENS);

const now = parseInt((new Date()).getTime() / 1000.0)
const tokenRow = {
Expand Down Expand Up @@ -231,7 +243,7 @@ class AuthManager {

// check this refresh token is in the database (hasn't been invalidated)
const couch = Db.getCouch();
const tokenDb = couch.db.use(process.env.DB_REFRESH_TOKENS);
const tokenDb = couch.db.use(CONFIG.DB_REFRESH_TOKENS);

try {
const tokenRow = await tokenDb.get(decodedJwt.id);
Expand Down Expand Up @@ -266,7 +278,7 @@ class AuthManager {
}

const couch = Db.getCouch();
const tokenDb = couch.db.use(process.env.DB_REFRESH_TOKENS);
const tokenDb = couch.db.use(CONFIG.DB_REFRESH_TOKENS);

try {
const tokenRow = await tokenDb.get(decodedJwt.id);
Expand Down Expand Up @@ -297,7 +309,7 @@ class AuthManager {
async invalidateDeviceId(did, contextName, deviceId, signature) {
did = did.toLowerCase()
const consentMessage = `Invalidate device for this application context: "${contextName}"?\n\n${did}\n${deviceId}`
const validSignature = await this.verifySignedConsentMessage(did, signature, consentMessage)
const validSignature = await this.verifySignedConsentMessage(did, signature, consentMessage, contextName)

if (!validSignature) {
return false
Expand All @@ -312,7 +324,7 @@ class AuthManager {
};

const couch = Db.getCouch();
const tokenDb = couch.db.use(process.env.DB_REFRESH_TOKENS);
const tokenDb = couch.db.use(CONFIG.DB_REFRESH_TOKENS);
const tokenRows = await tokenDb.find(query)

if (!tokenRows || !tokenRows.docs.length) {
Expand Down Expand Up @@ -344,7 +356,7 @@ class AuthManager {

const username = Utils.generateUsername(decodedJwt.sub.toLowerCase(), decodedJwt.contextName);

const expiresIn = parseInt(process.env.ACCESS_TOKEN_EXPIRY)
const expiresIn = parseInt(CONFIG.ACCESS_TOKEN_EXPIRY)

// generate new request token
const requestTokenId = randtoken.generate(256);
Expand Down Expand Up @@ -390,7 +402,7 @@ class AuthManager {
async initDb() {
const couch = Db.getCouch('internal');
try {
await couch.db.create(process.env.DB_REFRESH_TOKENS)
await couch.db.create(CONFIG.DB_REFRESH_TOKENS)
} catch (err) {
if (err.message.match(/already exists/)) {
// Database already exists
Expand All @@ -401,7 +413,7 @@ class AuthManager {
}

try {
await couch.db.create(process.env.DB_REPLICATER_CREDS)
await couch.db.create(CONFIG.DB_REPLICATER_CREDS)
} catch (err) {
if (err.message.match(/already exists/)) {
// Database already exists
Expand Down Expand Up @@ -452,7 +464,7 @@ class AuthManager {
const replicatorDb = couch.db.use('_replicator');
await replicatorDb.createIndex(expiryIndex);

const tokenDb = couch.db.use(process.env.DB_REFRESH_TOKENS);
const tokenDb = couch.db.use(CONFIG.DB_REFRESH_TOKENS);

const deviceIndex = {
index: { fields: ['deviceHash'] },
Expand Down Expand Up @@ -546,7 +558,7 @@ class AuthManager {
};

const couch = Db.getCouch();
const tokenDb = couch.db.use(process.env.DB_REFRESH_TOKENS);
const tokenDb = couch.db.use(CONFIG.DB_REFRESH_TOKENS);
const tokenRows = await tokenDb.find(query)

if (tokenRows && tokenRows.docs && tokenRows.docs.length) {
Expand Down
13 changes: 7 additions & 6 deletions src/components/replicationManager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Db from './db.js'
import Utils from './utils.js'
import DbManager from './dbManager.js';
import CONFIG from '../config.js'
import AuthManager from './authManager.js';
import Axios from 'axios'
import EncryptionUtils from '@verida/encryption-utils';
Expand Down Expand Up @@ -121,7 +122,7 @@ class ReplicationManager {
console.error(`${Utils.serverUri()}: Attempting to touched replication entry that doesn't exist: ${endpointUri} (${replicatorId}-${dbHash})`)
continue;
}
doc.expiry = (now() + process.env.REPLICATION_EXPIRY_MINUTES*60)
doc.expiry = (now() + CONFIG.REPLICATION_EXPIRY_MINUTES*60)
const result = await DbManager._insertOrUpdate(replicationDb, doc, doc._id)
//console.log(`${Utils.serverUri()}: Touched replication entry for ${endpointUri} (${replicatorId}-${dbHash})`)
} catch (err) {
Expand Down Expand Up @@ -170,7 +171,7 @@ class ReplicationManager {
create_target: false,
continuous: true,
owner: 'admin',
expiry: (now() + process.env.REPLICATION_EXPIRY_MINUTES*60)
expiry: (now() + CONFIG.REPLICATION_EXPIRY_MINUTES*60)
}

try {
Expand All @@ -193,12 +194,12 @@ class ReplicationManager {
async getReplicationEndpoints(did, contextName) {
// Lookup DID document and get list of endpoints for this context
let didDocument = await AuthManager.getDidDocument(did)
let didService = didDocument.locateServiceEndpoint(contextName, 'database')
let didService = didDocument.locateServiceEndpoint(contextName, 'database', process.env.VERIDA_NETWORK)

if (!didService) {
// Service not found, try to fetch the DID document without caching (as it may have been updated)
didDocument = await AuthManager.getDidDocument(did, true)
didService = didDocument.locateServiceEndpoint(contextName, 'database')
didService = didDocument.locateServiceEndpoint(contextName, 'database', process.env.VERIDA_NETWORK)
}

if (!didService) {
Expand Down Expand Up @@ -240,9 +241,9 @@ class ReplicationManager {
* @returns
*/
async fetchReplicaterCredentials(remoteEndpointUri, did, contextName, force = false) {
// Check process.env.DB_REPLICATER_CREDS for existing credentials
// Check CONFIG.DB_REPLICATER_CREDS for existing credentials
const couch = Db.getCouch('internal');
const replicaterCredsDb = await couch.db.use(process.env.DB_REPLICATER_CREDS)
const replicaterCredsDb = await couch.db.use(CONFIG.DB_REPLICATER_CREDS)

const thisEndointUri = Utils.serverUri()
const thisReplicaterUsername = Utils.generateReplicaterUsername(Utils.serverUri())
Expand Down
3 changes: 2 additions & 1 deletion src/components/userManager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import crypto from 'crypto';
import Db from './db.js'
import Utils from './utils.js'
import CONFIG from '../config.js'
import DbManager from './dbManager.js';
import AuthManager from './authManager';
import ReplicationManager from './replicationManager';
Expand Down Expand Up @@ -43,7 +44,7 @@ class UserManager {
const couch = Db.getCouch()
const password = crypto.createHash('sha256').update(signature).digest("hex")

const storageLimit = process.env.DEFAULT_USER_CONTEXT_LIMIT_MB*1048576
const storageLimit = CONFIG.DEFAULT_USER_CONTEXT_LIMIT_MB*1048576

// Create CouchDB database user matching username and password
let userData = {
Expand Down
Loading

0 comments on commit 213dbe6

Please sign in to comment.