Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

updates for verida-js v4 #81

Merged
merged 16 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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