From c927e574613904486a15ea74da5f687990755b07 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 3 Apr 2022 16:33:37 +0930 Subject: [PATCH 01/27] Better handling opening a context without an account. Update storage node consent message. --- packages/client-ts/src/context/context.ts | 5 +- .../context/engines/ContextNotFoundError.ts | 7 ++ .../client-ts/src/context/engines/base.ts | 10 ++- .../context/engines/verida/database/client.ts | 10 +-- .../context/engines/verida/database/engine.ts | 69 +++++++++++-------- .../test/verida/database.context.tests.ts | 1 - 6 files changed, 63 insertions(+), 39 deletions(-) create mode 100644 packages/client-ts/src/context/engines/ContextNotFoundError.ts diff --git a/packages/client-ts/src/context/context.ts b/packages/client-ts/src/context/context.ts index 14f7766b..92fb7276 100644 --- a/packages/client-ts/src/context/context.ts +++ b/packages/client-ts/src/context/context.ts @@ -149,6 +149,7 @@ class Context { `Unsupported database engine type specified: ${engineType}` ); } + const engine = DATABASE_ENGINES[engineType]; // @todo type cast correctly const databaseEngine = new engine( this.contextName, @@ -342,8 +343,9 @@ class Context { config ); - config.saveDatabase = false; + config.isOwner = false; + config.saveDatabase = false; if (config.contextName && config.contextName != this.contextName) { // We are opening a database for a different context. // Open the new context @@ -358,7 +360,6 @@ class Context { } const databaseEngine = await this.getDatabaseEngine(did); - return databaseEngine.openDatabase(databaseName, config); } diff --git a/packages/client-ts/src/context/engines/ContextNotFoundError.ts b/packages/client-ts/src/context/engines/ContextNotFoundError.ts new file mode 100644 index 00000000..6ff991ac --- /dev/null +++ b/packages/client-ts/src/context/engines/ContextNotFoundError.ts @@ -0,0 +1,7 @@ + +export default class ContextNotFoundError extends Error { + constructor(message: string) { + super(message) + this.name = "ContextNotFoundError" + } +} \ No newline at end of file diff --git a/packages/client-ts/src/context/engines/base.ts b/packages/client-ts/src/context/engines/base.ts index dcda1d16..78397b98 100644 --- a/packages/client-ts/src/context/engines/base.ts +++ b/packages/client-ts/src/context/engines/base.ts @@ -4,6 +4,7 @@ import { DatabaseOpenConfig, DatastoreOpenConfig } from "../interfaces"; import Database from "../database"; import Datastore from "../datastore"; import DbRegistry from "../db-registry"; +import ContextNotFoundError from "./ContextNotFoundError"; /** * @category @@ -28,8 +29,13 @@ class BaseStorageEngine { } public async connectAccount(account: Account) { - this.account = account; - this.keyring = await account.keyring(this.storageContext); + try { + this.account = account; + this.keyring = await account.keyring(this.storageContext); + } catch (err: any) { + this.account = undefined + throw new ContextNotFoundError("Unable to generate Keyring") + } } public async openDatabase( diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index 00cd2427..6e5ef20e 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -27,12 +27,14 @@ class DatastoreServerClient { public async setAccount(account: Account) { this.account = account; - const did = await account.did(); - const keyring = await account.keyring(this.storageContext); + let did = await account.did(); + did = did.toLowerCase() + + const signature = await account.sign(`Do you wish to authenticate this storage context: "${this.storageContext}"?\n\n${did}`) this.authentication = { - username: did.toLowerCase(), - signature: keyring.getSeed(), + username: did, + signature: signature }; } diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index 5bd5986e..6f5b7c85 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -35,42 +35,44 @@ class StorageEngineVerida extends BaseStorageEngine { } public async connectAccount(account: Account) { + //try { try { await super.connectAccount(account); await this.client.setAccount(account); this.accountDid = await this.account!.did(); - - // Fetch user details from server - let response; - try { - response = await this.client.getUser(this.accountDid!); - } catch (err: any) { - if ( - err.response && - err.response.data.data && - err.response.data.data.did == "Invalid DID specified" - ) { - // User doesn't exist, so create them - response = await this.client.createUser(); - } else if (err.response && err.response.statusText == "Unauthorized") { - throw new Error( - "Invalid signature or permission to access DID server" - ); - } else { - // Unknown error - throw err; - } + } catch (err: any) { + if (err.name == "ContextNotFoundError") { + return } - const user = response.data.user; - this.dsn = user.dsn; + throw err + } + + // Fetch user details from server + let response; + try { + response = await this.client.getUser(this.accountDid!); } catch (err: any) { - //console.log(err) - // Connecting the account may fail. - // For example, the user is connect via `account-web-vault` and doesn't have - // a keyring for the context associated with this storage engine + if ( + err.response && + err.response.data.data && + err.response.data.data.did == "Invalid DID specified" + ) { + // User doesn't exist, so create them + response = await this.client.createUser(); + } else if (err.response && err.response.statusText == "Unauthorized") { + throw new Error( + "Invalid signature or permission to access DID server" + ); + } else { + // Unknown error + throw err; + } } + + const user = response.data.user; + this.dsn = user.dsn; } /** @@ -89,6 +91,10 @@ class StorageEngineVerida extends BaseStorageEngine { * @returns {string} */ protected async buildExternalDsn(endpointUri: string): Promise { + if (!this.account) { + throw new Error('Unable to connect to external storage node. No account connected.') + } + const client = new DatastoreServerClient(this.storageContext, endpointUri); await client.setAccount(this.account!); let response; @@ -137,7 +143,10 @@ class StorageEngineVerida extends BaseStorageEngine { ); // Default to user's account did if not specified - config.isOwner = config.did == this.accountDid; + if (typeof(config.isOwner) == 'undefined') { + config.isOwner = config.did == this.accountDid; + } + config.saveDatabase = config.isOwner; // always save this database to registry if user is the owner let did = config.did!.toLowerCase(); @@ -172,7 +181,7 @@ class StorageEngineVerida extends BaseStorageEngine { let dsn = config.isOwner ? this.dsn! : config.dsn!; if (!dsn) { - throw new Error("Unable to determine DSN for this user and this context"); + throw new Error(`Unable to determine DSN for this user (${did}) and this context (${this.storageContext})`); } // force read only access if the current user doesn't have write access @@ -276,7 +285,7 @@ class StorageEngineVerida extends BaseStorageEngine { * - Need to talk to the db hash for the did that owns the database */ - if (!config.isOwner) { + if (!config.isOwner && this.account) { // need to build a complete dsn dsn = await this.buildExternalDsn(config.dsn!); } diff --git a/packages/client-ts/test/verida/database.context.tests.ts b/packages/client-ts/test/verida/database.context.tests.ts index 52b76c9a..396972d3 100644 --- a/packages/client-ts/test/verida/database.context.tests.ts +++ b/packages/client-ts/test/verida/database.context.tests.ts @@ -47,7 +47,6 @@ describe('Verida database tests relating to contexts', () => { did1 = await account1.did() await network.connect(account1) context = await network.openContext(CONTEXT_1, true) - const database = db1 = await context.openDatabase(DB_NAME_PUBLIC_WRITE, { permissions: { read: 'public', From d44b283630caf8469ca417ad281ac0cb8230a170 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 27 Apr 2022 18:08:18 +0930 Subject: [PATCH 02/27] Support CouchDB token auth for public and encrypted databases. --- .../engines/verida/database/base-db.ts | 2 + .../context/engines/verida/database/client.ts | 31 ++++++++- .../engines/verida/database/db-encrypted.ts | 7 ++ .../engines/verida/database/db-public.ts | 21 +++++- .../context/engines/verida/database/engine.ts | 66 +++++-------------- .../engines/verida/database/interfaces.ts | 1 + packages/client-ts/src/context/interfaces.ts | 7 +- 7 files changed, 80 insertions(+), 55 deletions(-) diff --git a/packages/client-ts/src/context/engines/verida/database/base-db.ts b/packages/client-ts/src/context/engines/verida/database/base-db.ts index d095584e..104f1e14 100644 --- a/packages/client-ts/src/context/engines/verida/database/base-db.ts +++ b/packages/client-ts/src/context/engines/verida/database/base-db.ts @@ -20,6 +20,7 @@ class BaseDb extends EventEmitter implements Database { protected databaseName: string; protected did: string; protected dsn: string; + protected token?: string; protected storageContext: string; protected permissions?: PermissionsConfig; @@ -42,6 +43,7 @@ class BaseDb extends EventEmitter implements Database { this.databaseName = config.databaseName; this.did = config.did.toLowerCase(); this.dsn = config.dsn; + this.token = config.token; this.storageContext = config.storageContext; this.isOwner = config.isOwner; diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index 6e5ef20e..e0d89a92 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -9,11 +9,17 @@ interface RemoteClientAuthentication { signature: string; } +export interface CouchDbAuthentication { + host: string; + token: string; + username: string; +} + /** * @category * Modules */ -class DatastoreServerClient { +export class DatastoreServerClient { private serverUrl: string; private storageContext: string; @@ -38,8 +44,27 @@ class DatastoreServerClient { }; } - public async getUser(did: string) { - return this.getAxios(true).get(this.serverUrl + "user/get?did=" + did); + public async getUser(did: string): Promise { + let response + try { + response = await this.getAxios(true).get(this.serverUrl + "user/get?did=" + did); + } catch (err: any) { + if ( + err.response && + err.response.data.data && + err.response.data.data.did == "Invalid DID specified" + ) { + // User doesn't exist, so create on this endpointUri server + response = await this.createUser(); + } else if (err.response && err.response.statusText == "Unauthorized") { + throw new Error("Invalid signature or permission to access DID server"); + } else { + // Unknown error + throw err; + } + } + + return response.data.user } public async getPublicUser() { diff --git a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts index 3df25a0c..e1c808c4 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts @@ -28,6 +28,7 @@ PouchDBCrypt.plugin(CryptoPouch); class EncryptedDatabase extends BaseDb { protected encryptionKey: Buffer; protected password?: string; + protected token: string private dbRegistry: DbRegistry; private _sync: any; @@ -52,6 +53,7 @@ class EncryptedDatabase extends BaseDb { this.dbRegistry = dbRegistry; this.encryptionKey = config.encryptionKey!; + this.token = config.token!; // PouchDB sync object this._sync = null; @@ -83,8 +85,13 @@ class EncryptedDatabase extends BaseDb { // Setting to 1,000 -- Any higher and it takes too long on mobile devices }); + const authToken = this.token this._remoteDbEncrypted = new PouchDB(this.dsn + this.databaseHash, { skip_setup: true, + fetch: function(url: string, opts: any) { + opts.headers.set('Authorization', `Bearer ${authToken}`) + return PouchDB.fetch(url, opts) + } }); let info; diff --git a/packages/client-ts/src/context/engines/verida/database/db-public.ts b/packages/client-ts/src/context/engines/verida/database/db-public.ts index 70d8d3f9..bba99907 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-public.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-public.ts @@ -13,7 +13,6 @@ PouchDB.plugin(PouchDBFind); * Modules */ class PublicDatabase extends BaseDb { - //constructor(dbHumanName: string, dbName: string, dataserver: any, did: string, permissions: PermissionsConfig, isOwner: boolean) { private _remoteDb: any; public async init() { @@ -24,9 +23,27 @@ class PublicDatabase extends BaseDb { await super.init(); const databaseName = this.databaseName; + const dbConfig: any = { + skip_setup: true, + } - this._remoteDb = new PouchDB(this.dsn + this.databaseHash, { + if (this.token) { + const authToken = this.token + dbConfig['fetch'] = function(url: string, opts: any) { + opts.headers.set('Authorization', `Bearer ${authToken}`) + return PouchDB.fetch(url, opts) + } + } + + this._remoteDb = new PouchDB(`${this.dsn}/${this.databaseHash}`, dbConfig); + + const authToken = this.token + this._remoteDbEncrypted = new PouchDB(this.dsn + this.databaseHash, { skip_setup: true, + fetch: function(url: string, opts: any) { + opts.headers.set('Authorization', `Bearer ${authToken}`) + return PouchDB.fetch(url, opts) + } }); try { diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index 6f5b7c85..aa46b4ab 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -2,7 +2,7 @@ import BaseStorageEngine from "../../base"; import EncryptedDatabase from "./db-encrypted"; import Database from "../../../database"; import { DatabaseOpenConfig } from "../../../interfaces"; -import DatastoreServerClient from "./client"; +import { DatastoreServerClient, CouchDbAuthentication } from "./client"; import { Account } from "@verida/account"; import PublicDatabase from "./db-public"; import DbRegistry from "../../../db-registry"; @@ -19,7 +19,7 @@ class StorageEngineVerida extends BaseStorageEngine { private publicCredentials: any; // @todo private accountDid?: string; - private dsn?: string; + private auth?: CouchDbAuthentication // @todo: dbmanager constructor( @@ -35,7 +35,6 @@ class StorageEngineVerida extends BaseStorageEngine { } public async connectAccount(account: Account) { - //try { try { await super.connectAccount(account); @@ -49,30 +48,7 @@ class StorageEngineVerida extends BaseStorageEngine { throw err } - // Fetch user details from server - let response; - try { - response = await this.client.getUser(this.accountDid!); - } catch (err: any) { - if ( - err.response && - err.response.data.data && - err.response.data.data.did == "Invalid DID specified" - ) { - // User doesn't exist, so create them - response = await this.client.createUser(); - } else if (err.response && err.response.statusText == "Unauthorized") { - throw new Error( - "Invalid signature or permission to access DID server" - ); - } else { - // Unknown error - throw err; - } - } - - const user = response.data.user; - this.dsn = user.dsn; + this.auth = await this.client.getUser(this.accountDid!); } /** @@ -90,33 +66,16 @@ class StorageEngineVerida extends BaseStorageEngine { * @param did * @returns {string} */ - protected async buildExternalDsn(endpointUri: string): Promise { + protected async buildExternalAuth(endpointUri: string): Promise { if (!this.account) { throw new Error('Unable to connect to external storage node. No account connected.') } const client = new DatastoreServerClient(this.storageContext, endpointUri); await client.setAccount(this.account!); - let response; - try { - response = await client.getUser(this.accountDid!); - } catch (err: any) { - if ( - err.response && - err.response.data.data && - err.response.data.data.did == "Invalid DID specified" - ) { - // User doesn't exist, so create on this endpointUri server - response = await client.createUser(); - } else if (err.response && err.response.statusText == "Unauthorized") { - throw new Error("Invalid signature or permission to access DID server"); - } else { - // Unknown error - throw err; - } - } - return response.data.user.dsn; + const auth = await client.getUser(this.accountDid!); + return auth } /** @@ -179,7 +138,12 @@ class StorageEngineVerida extends BaseStorageEngine { } } - let dsn = config.isOwner ? this.dsn! : config.dsn!; + let dsn = config.isOwner ? this.auth!.host! : config.dsn!; + if (!dsn) { + throw new Error(`Unable to determine DSN for this user (${did}) and this context (${this.storageContext})`); + } + + let token = config.isOwner ? this.auth!.token : config.token!; if (!dsn) { throw new Error(`Unable to determine DSN for this user (${did}) and this context (${this.storageContext})`); } @@ -220,6 +184,7 @@ class StorageEngineVerida extends BaseStorageEngine { storageContext: this.storageContext, signContext: options.signingContext!, dsn, + token, permissions: config.permissions, readOnly: config.readOnly, encryptionKey, @@ -248,6 +213,7 @@ class StorageEngineVerida extends BaseStorageEngine { databaseName, did, dsn, + token, storageContext: this.storageContext, signContext: options.signingContext!, permissions: config.permissions, @@ -287,7 +253,8 @@ class StorageEngineVerida extends BaseStorageEngine { if (!config.isOwner && this.account) { // need to build a complete dsn - dsn = await this.buildExternalDsn(config.dsn!); + const auth = await this.buildExternalAuth(config.dsn!); + dsn = auth.host } const storageContextKey = await this.keyring!.getStorageContextKey( @@ -304,6 +271,7 @@ class StorageEngineVerida extends BaseStorageEngine { storageContext: this.storageContext, signContext: options.signingContext!, dsn, + token, permissions: config.permissions, readOnly: config.readOnly, encryptionKey, diff --git a/packages/client-ts/src/context/engines/verida/database/interfaces.ts b/packages/client-ts/src/context/engines/verida/database/interfaces.ts index f97519c0..c297cb74 100644 --- a/packages/client-ts/src/context/engines/verida/database/interfaces.ts +++ b/packages/client-ts/src/context/engines/verida/database/interfaces.ts @@ -9,6 +9,7 @@ export interface VeridaDatabaseConfig { databaseName: string; did: string; dsn: string; + token?: string; storageContext: string; permissions?: PermissionsConfig; diff --git a/packages/client-ts/src/context/interfaces.ts b/packages/client-ts/src/context/interfaces.ts index a8625d0a..55b82493 100644 --- a/packages/client-ts/src/context/interfaces.ts +++ b/packages/client-ts/src/context/interfaces.ts @@ -17,10 +17,15 @@ export interface DatabaseOpenConfig { did?: string; /** - * Specify a specific database connection string to use when opening the database. + * Specify a database connection string to use when opening the database. */ dsn?: string; + /** + * Specify a JWT token to use when opening the database. + */ + token?: string; + /** * Save this database into the user's master list of opened databases. */ From 2d7db2ca767ef48d262b024f0c38386ba01ba60b Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 27 Apr 2022 18:14:26 +0930 Subject: [PATCH 03/27] Include token with user permissions --- packages/client-ts/src/context/engines/verida/database/engine.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index aa46b4ab..2650f99d 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -255,6 +255,7 @@ class StorageEngineVerida extends BaseStorageEngine { // need to build a complete dsn const auth = await this.buildExternalAuth(config.dsn!); dsn = auth.host + token = auth.token } const storageContextKey = await this.keyring!.getStorageContextKey( From c0adfbc6cca9dba356b6ee1aa1ad428f30dfac37 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 15 Jun 2022 16:57:01 -0400 Subject: [PATCH 04/27] Add more progress --- .../account-web-vault/src/vault-account.ts | 18 ++++++-- .../context/engines/verida/database/client.ts | 46 ++++++++++++++++--- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index 3dab8643..8d738e34 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -58,7 +58,7 @@ export default class VaultAccount extends Account { } this.setDid(response.did) - vaultAccount.addContext(response.context, response.contextConfig, new Keyring(response.signature)) + vaultAccount.addContext(response.context, response.contextConfig, new Keyring(response.signature), response.contextAuth) resolve(true) } @@ -85,7 +85,7 @@ export default class VaultAccount extends Account { const response = storedSessions[contextName] this.setDid(response.did) - this.addContext(response.context, response.contextConfig, new Keyring(response.signature)) + this.addContext(response.context, response.contextConfig, new Keyring(response.signature), response.contextAuth) if (typeof(this.config!.callback) === "function") { this.config!.callback(response) @@ -102,10 +102,20 @@ export default class VaultAccount extends Account { return this.contextCache[contextName].keyring } - public addContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, keyring: Keyring) { + // @todo: need this to be in the interface for all account instances? + public async getContextAuth(contextName: string) { + if (typeof(this.contextCache[contextName]) == 'undefined') { + throw new Error(`Unable to connect to requested context: ${contextName}`) + } + + return this.contextCache[contextName].contextAuth + } + + public addContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, keyring: Keyring, contextAuth: any) { this.contextCache[contextName] = { keyring, - contextConfig + contextConfig, + contextAuth } } diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index e0d89a92..1c6c27a2 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -5,14 +5,15 @@ import { Account } from "@verida/account"; * Interface for RemoteClientAuthentication */ interface RemoteClientAuthentication { - username: string; - signature: string; + refreshToken: string; + accessToken: string; + host: string; } export interface CouchDbAuthentication { - host: string; - token: string; - username: string; + host: string; // host name + token: string; // access token + //username: string; // is this needed? } /** @@ -36,18 +37,43 @@ export class DatastoreServerClient { let did = await account.did(); did = did.toLowerCase() + /* const signature = await account.sign(`Do you wish to authenticate this storage context: "${this.storageContext}"?\n\n${did}`) this.authentication = { username: did, signature: signature - }; + };*/ } public async getUser(did: string): Promise { + if (this.authentication) { + return { + host: this.authentication.host, + token: this.authentication.accessToken + } + } + + if (!this.account) { + throw new Error("Unable to connect. No account set.") + } + + const contextAuth = await this.account!.getContextAuth(this.storageContext) + + if (!contextAuth) { + throw new Error("Unable to connect. Unable to authenticate.") + } + + // @todo: test if connection is valid? + // @todo: get a new access token if invalid + // @todo: get a new refresh token if getting close to expiring, save to cache (account.updateContextAuth()?) + // @todo: how are invalid access tokens going to produce an error? how to catch and then regenerate? + // - expired token stored in session when loading the app + // - token expires while using the app + let response try { - response = await this.getAxios(true).get(this.serverUrl + "user/get?did=" + did); + response = await this.getAxios(true).get(this.serverUrl + "auth/connect?did=" + did); } catch (err: any) { if ( err.response && @@ -67,11 +93,17 @@ export class DatastoreServerClient { return response.data.user } + private async authenticate() { + // throw error if no account + + } + public async getPublicUser() { return this.getAxios(false).get(this.serverUrl + "user/public"); } public async createUser() { + throw new Error('create user is no longer supported due to refresh token refactor') if (!this.account) { throw new Error( "Unable to create storage account. No Verida account connected." From 2293771be0fb4556fb595ec11c8803c4a0a2d5c9 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 16 Jul 2022 12:37:07 +0930 Subject: [PATCH 05/27] Adding context auth interface --- packages/account-node/src/auto.ts | 4 ++++ packages/account-web-vault/src/vault-account.ts | 9 +++++++-- packages/account/src/account.ts | 5 +++++ packages/account/src/index.ts | 3 ++- packages/account/src/interfaces.ts | 13 ++++++++++++- .../src/context/engines/verida/database/client.ts | 4 +++- 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/account-node/src/auto.ts b/packages/account-node/src/auto.ts index f6df8314..09265b68 100644 --- a/packages/account-node/src/auto.ts +++ b/packages/account-node/src/auto.ts @@ -98,6 +98,10 @@ export default class AutoAccount extends Account { return await StorageLink.setContextService(this.didClient, contextName, endpointType, serverType, endpointUri) } + public async getContextAuth(contextName: string) { + //@todo + } + public getDidClient() { return this.didClient } diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index 8d738e34..6e7c5905 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -52,7 +52,7 @@ export default class VaultAccount extends Account { if (!storedSessions) { storedSessions = {} } - + storedSessions[contextName] = response store.set(VERIDA_AUTH_CONTEXT, storedSessions) } @@ -103,8 +103,13 @@ export default class VaultAccount extends Account { } // @todo: need this to be in the interface for all account instances? - public async getContextAuth(contextName: string) { + public async getContextAuth(contextName: string, forceCreate: boolean = false) { if (typeof(this.contextCache[contextName]) == 'undefined') { + if (forceCreate) { + await this.connectContext(contextName) + return this.getContextAuth(contextName, false) + } + throw new Error(`Unable to connect to requested context: ${contextName}`) } diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index 40bc820c..cfea6165 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -2,6 +2,7 @@ import { Keyring } from '@verida/keyring' import { Interfaces } from '@verida/storage-link' import { createJWT, ES256KSigner } from 'did-jwt' import { encodeBase64 } from "tweetnacl-util" +import { ContextAuth } from "./interfaces" const _ = require('lodash') @@ -65,6 +66,10 @@ export default class Account { throw new Error("Not implemented") } + getContextAuth(contextName: string): Promise { + throw new Error("Not implemented") + } + /** * Create a DID-JWT from a data object * @param {*} data diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index 874d49b1..536ea201 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -1,10 +1,11 @@ import Account from "./account" -import { AccountConfig, EnvironmentType } from "./interfaces" +import { AccountConfig, EnvironmentType, ContextAuth } from "./interfaces" import Config from "./config" export { Account, AccountConfig, EnvironmentType, + ContextAuth, Config } \ No newline at end of file diff --git a/packages/account/src/interfaces.ts b/packages/account/src/interfaces.ts index 9e4b1182..403c36b7 100644 --- a/packages/account/src/interfaces.ts +++ b/packages/account/src/interfaces.ts @@ -11,4 +11,15 @@ export enum EnvironmentType { LOCAL = 'local', TESTNET = 'testnet', MAINNET = 'mainnet' -} \ No newline at end of file +} + +/** + * A generic interface that represents the authorization details + * of a given context. + * + * The actual implementation will depend on the type of service. + * + * Each implementation should extend this interface with it's + * own appropriate configuration (ie: access, refresh token etc.) + */ +export interface ContextAuth {} \ No newline at end of file diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index 1c6c27a2..3909ad12 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -58,13 +58,15 @@ export class DatastoreServerClient { throw new Error("Unable to connect. No account set.") } - const contextAuth = await this.account!.getContextAuth(this.storageContext) + // Fetch context auth details and force creation of a new account if required + const contextAuth = await this.account!.getContextAuth(this.storageContext, true) if (!contextAuth) { throw new Error("Unable to connect. Unable to authenticate.") } // @todo: test if connection is valid? + // @todo: get a new access token if invalid // @todo: get a new refresh token if getting close to expiring, save to cache (account.updateContextAuth()?) // @todo: how are invalid access tokens going to produce an error? how to catch and then regenerate? From 3dd5ce049938e872aec71097d728a3c66ed271e8 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 23 Aug 2022 13:12:33 +0930 Subject: [PATCH 06/27] Remove redundant contextAuth --- packages/account-node/src/auto.ts | 4 ---- packages/account-web-vault/src/vault-account.ts | 14 -------------- packages/account/src/account.ts | 5 ----- packages/account/src/index.ts | 3 +-- packages/account/src/interfaces.ts | 13 +------------ 5 files changed, 2 insertions(+), 37 deletions(-) diff --git a/packages/account-node/src/auto.ts b/packages/account-node/src/auto.ts index 09265b68..f6df8314 100644 --- a/packages/account-node/src/auto.ts +++ b/packages/account-node/src/auto.ts @@ -98,10 +98,6 @@ export default class AutoAccount extends Account { return await StorageLink.setContextService(this.didClient, contextName, endpointType, serverType, endpointUri) } - public async getContextAuth(contextName: string) { - //@todo - } - public getDidClient() { return this.didClient } diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index 6e7c5905..d98652f8 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -102,20 +102,6 @@ export default class VaultAccount extends Account { return this.contextCache[contextName].keyring } - // @todo: need this to be in the interface for all account instances? - public async getContextAuth(contextName: string, forceCreate: boolean = false) { - if (typeof(this.contextCache[contextName]) == 'undefined') { - if (forceCreate) { - await this.connectContext(contextName) - return this.getContextAuth(contextName, false) - } - - throw new Error(`Unable to connect to requested context: ${contextName}`) - } - - return this.contextCache[contextName].contextAuth - } - public addContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, keyring: Keyring, contextAuth: any) { this.contextCache[contextName] = { keyring, diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index cfea6165..40bc820c 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -2,7 +2,6 @@ import { Keyring } from '@verida/keyring' import { Interfaces } from '@verida/storage-link' import { createJWT, ES256KSigner } from 'did-jwt' import { encodeBase64 } from "tweetnacl-util" -import { ContextAuth } from "./interfaces" const _ = require('lodash') @@ -66,10 +65,6 @@ export default class Account { throw new Error("Not implemented") } - getContextAuth(contextName: string): Promise { - throw new Error("Not implemented") - } - /** * Create a DID-JWT from a data object * @param {*} data diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index 536ea201..874d49b1 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -1,11 +1,10 @@ import Account from "./account" -import { AccountConfig, EnvironmentType, ContextAuth } from "./interfaces" +import { AccountConfig, EnvironmentType } from "./interfaces" import Config from "./config" export { Account, AccountConfig, EnvironmentType, - ContextAuth, Config } \ No newline at end of file diff --git a/packages/account/src/interfaces.ts b/packages/account/src/interfaces.ts index 403c36b7..9e4b1182 100644 --- a/packages/account/src/interfaces.ts +++ b/packages/account/src/interfaces.ts @@ -11,15 +11,4 @@ export enum EnvironmentType { LOCAL = 'local', TESTNET = 'testnet', MAINNET = 'mainnet' -} - -/** - * A generic interface that represents the authorization details - * of a given context. - * - * The actual implementation will depend on the type of service. - * - * Each implementation should extend this interface with it's - * own appropriate configuration (ie: access, refresh token etc.) - */ -export interface ContextAuth {} \ No newline at end of file +} \ No newline at end of file From 7b86d9d66764eda18f3490cc553184835499b836 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 23 Aug 2022 13:17:19 +0930 Subject: [PATCH 07/27] Core authJWT refactor complete. Still need edge cases and fixing some tests. --- packages/client-ts/src/context/datastore.ts | 6 +- .../engines/verida/database/base-db.ts | 2 +- .../context/engines/verida/database/client.ts | 185 ++++++++++-------- .../engines/verida/database/db-encrypted.ts | 4 +- .../engines/verida/database/db-public.ts | 9 - .../context/engines/verida/database/engine.ts | 14 +- 6 files changed, 114 insertions(+), 106 deletions(-) diff --git a/packages/client-ts/src/context/datastore.ts b/packages/client-ts/src/context/datastore.ts index 2cccde56..437c031e 100644 --- a/packages/client-ts/src/context/datastore.ts +++ b/packages/client-ts/src/context/datastore.ts @@ -1,5 +1,5 @@ const _ = require("lodash"); -import { DatastoreOpenConfig } from "./interfaces"; +import { DatabaseOpenConfig, DatastoreOpenConfig } from "./interfaces"; import Context from "./context"; import Schema from "./schema"; import { DIDDocument } from "@verida/did-document"; @@ -219,10 +219,10 @@ class Datastore { this.db = await this.context.openExternalDatabase( dbName, this.config.did!, - this.config + this.config ); } else { - this.db = await this.context.openDatabase(dbName, this.config); + this.db = await this.context.openDatabase(dbName, this.config); } let indexes = schemaJson.database.indexes; diff --git a/packages/client-ts/src/context/engines/verida/database/base-db.ts b/packages/client-ts/src/context/engines/verida/database/base-db.ts index 104f1e14..a279fa22 100644 --- a/packages/client-ts/src/context/engines/verida/database/base-db.ts +++ b/packages/client-ts/src/context/engines/verida/database/base-db.ts @@ -411,7 +411,7 @@ class BaseDb extends EventEmitter implements Database { }; try { - await this.client.createDatabase(this.did, this.databaseHash, options); + await this.client.createDatabase(this.did, this.databaseName, options); // There's an odd timing issue that needs a deeper investigation await Utils.sleep(1000); } catch (err) { diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index 3909ad12..c016095a 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -4,18 +4,12 @@ import { Account } from "@verida/account"; /** * Interface for RemoteClientAuthentication */ -interface RemoteClientAuthentication { +export interface ContextAuth { refreshToken: string; accessToken: string; host: string; } -export interface CouchDbAuthentication { - host: string; // host name - token: string; // access token - //username: string; // is this needed? -} - /** * @category * Modules @@ -24,98 +18,126 @@ export class DatastoreServerClient { private serverUrl: string; private storageContext: string; - private authentication?: RemoteClientAuthentication; + private contextAuth?: ContextAuth; private account?: Account; + private deviceId: string - constructor(storageContext: string, serverUrl: string) { + constructor(storageContext: string, serverUrl: string, deviceId: string="Test device") { this.storageContext = storageContext; this.serverUrl = serverUrl; + this.deviceId = deviceId; } public async setAccount(account: Account) { this.account = account; let did = await account.did(); did = did.toLowerCase() - - /* - const signature = await account.sign(`Do you wish to authenticate this storage context: "${this.storageContext}"?\n\n${did}`) - - this.authentication = { - username: did, - signature: signature - };*/ } - public async getUser(did: string): Promise { - if (this.authentication) { - return { - host: this.authentication.host, - token: this.authentication.accessToken - } - } - + public async getContextAuth(forceAccessToken=false): Promise { if (!this.account) { throw new Error("Unable to connect. No account set.") } - // Fetch context auth details and force creation of a new account if required - const contextAuth = await this.account!.getContextAuth(this.storageContext, true) + // @todo: how are invalid access tokens going to produce an error? how to catch and then regenerate? + // - expired token stored in session when loading the app + // - token expires while using the app + + // ------------------------- - if (!contextAuth) { - throw new Error("Unable to connect. Unable to authenticate.") + // We already have a context auth object, so reuse it unless + // requested to force create access token. + // This can happen if the access token has expired when being + // used and it can automatically be re-requested. + if (this.contextAuth && !forceAccessToken) { + return this.contextAuth } - // @todo: test if connection is valid? + const did = await this.account!.did() - // @todo: get a new access token if invalid - // @todo: get a new refresh token if getting close to expiring, save to cache (account.updateContextAuth()?) - // @todo: how are invalid access tokens going to produce an error? how to catch and then regenerate? - // - expired token stored in session when loading the app - // - token expires while using the app + // No context auth or no refresh token, so generate it by signing a consent message + if (!this.contextAuth || !this.contextAuth.refreshToken) { + // @todo: get a new refresh token if getting close to expiring? + + let authJwt + try { + // Generate an auth token to start auth process + const authJwtResponse = await this.getAxios().post(this.serverUrl + "auth/generateAuthJwt",{ + did, + contextName: this.storageContext + }); - let response - try { - response = await this.getAxios(true).get(this.serverUrl + "auth/connect?did=" + did); - } catch (err: any) { - if ( - err.response && - err.response.data.data && - err.response.data.data.did == "Invalid DID specified" - ) { - // User doesn't exist, so create on this endpointUri server - response = await this.createUser(); - } else if (err.response && err.response.statusText == "Unauthorized") { - throw new Error("Invalid signature or permission to access DID server"); - } else { - // Unknown error - throw err; + // @todo: handle connection error + + //console.log("generateAuthJwt response", authJwtResponse.data) + authJwt = authJwtResponse.data.authJwt + } catch (err: any) { + throw new Error(`Context Authentication Error. ${err.message}`) } - } - return response.data.user - } + let refreshResponse + try { + // Generate a refresh token by authenticating + const consentMessage = `Authenticate this application context: "${this.storageContext}"?\n\n${did.toLowerCase()}\n${authJwt.authRequestId}` + const signature = await this.account!.sign(consentMessage) + + refreshResponse = await this.getAxios().post(this.serverUrl + "auth/authenticate",{ + authJwt: authJwt.authJwt, + did, + contextName: this.storageContext, + signature, + deviceId: this.deviceId + }); + + // @todo: handle auth error (refreshResponse.data.status != 'success') + } catch (err: any) { + // @todo: handle connection error + console.log(err) + throw new Error(`Context Authentication Error. ${err.message}`) + } - private async authenticate() { - // throw error if no account + //console.log("authenticate response", refreshResponse.data) - } + const refreshToken = refreshResponse.data.refreshToken + const host = refreshResponse.data.host + const accessToken = refreshResponse.data.accessToken - public async getPublicUser() { - return this.getAxios(false).get(this.serverUrl + "user/public"); - } + this.contextAuth = { + refreshToken, + accessToken, + host + } - public async createUser() { - throw new Error('create user is no longer supported due to refresh token refactor') - if (!this.account) { - throw new Error( - "Unable to create storage account. No Verida account connected." - ); + //console.log(this.contextAuth!) + + return this.contextAuth } - const did = await this.account!.did(); - return this.getAxios(true).post(this.serverUrl + "user/create", { - did: did, - }); + // No access token, but have a refresh token, so generate access token + if (this.contextAuth && !this.contextAuth.accessToken) { + const accessResponse = await this.getAxios().post(this.serverUrl + "auth/connect",{ + refreshToken: this.contextAuth.refreshToken, + did, + contextName: this.storageContext + }); + + // @todo: handle connect error + // @todo: handle connect error (accessResponse.data.status != 'success') + + //console.log("connect response", accessResponse.data) + + const accessToken = accessResponse.data.accessToken + this.contextAuth.accessToken = accessToken + return this.contextAuth + } + + // @todo: test if connection is valid? + + return this.contextAuth! + } + + public async getPublicUser() { + return this.getAxios().get(this.serverUrl + "auth/public"); } public async createDatabase( @@ -123,7 +145,9 @@ export class DatastoreServerClient { databaseName: string, config: any = {} ) { - return this.getAxios(true).post(this.serverUrl + "user/createDatabase", { + const contextAuth = await this.getContextAuth() + + return this.getAxios(contextAuth.accessToken).post(this.serverUrl + "user/createDatabase", { did: did, databaseName: databaseName, options: config, @@ -135,14 +159,16 @@ export class DatastoreServerClient { databaseName: string, config: any = {} ) { - return this.getAxios(true).post(this.serverUrl + "user/updateDatabase", { + const contextAuth = await this.getContextAuth() + + return this.getAxios(contextAuth.accessToken).post(this.serverUrl + "user/updateDatabase", { did: did, databaseName: databaseName, options: config, }); } - private getAxios(includeAuth: boolean) { + private getAxios(accessToken?: string) { let config: any = { headers: { // @todo: Application-Name needs to become Storage-Context @@ -150,17 +176,8 @@ export class DatastoreServerClient { }, }; - if (includeAuth) { - if (!this.authentication) { - throw new Error( - "Unable to authenticate as there is no authentication defined" - ); - } - - config["auth"] = { - username: this.authentication.username.replace(/:/g, "_"), - password: this.authentication.signature, - }; + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}` } return Axios.create(config); diff --git a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts index e1c808c4..7a903120 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts @@ -86,7 +86,7 @@ class EncryptedDatabase extends BaseDb { }); const authToken = this.token - this._remoteDbEncrypted = new PouchDB(this.dsn + this.databaseHash, { + this._remoteDbEncrypted = new PouchDB(`${this.dsn}/${this.databaseHash}`, { skip_setup: true, fetch: function(url: string, opts: any) { opts.headers.set('Authorization', `Bearer ${authToken}`) @@ -267,7 +267,7 @@ class EncryptedDatabase extends BaseDb { }; try { - this.client.updateDatabase(this.did, this.databaseHash, options); + this.client.updateDatabase(this.did, this.databaseName, options); if (this.config.saveDatabase !== false) { await this.dbRegistry.saveDb(this); diff --git a/packages/client-ts/src/context/engines/verida/database/db-public.ts b/packages/client-ts/src/context/engines/verida/database/db-public.ts index bba99907..62ab7724 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-public.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-public.ts @@ -37,15 +37,6 @@ class PublicDatabase extends BaseDb { this._remoteDb = new PouchDB(`${this.dsn}/${this.databaseHash}`, dbConfig); - const authToken = this.token - this._remoteDbEncrypted = new PouchDB(this.dsn + this.databaseHash, { - skip_setup: true, - fetch: function(url: string, opts: any) { - opts.headers.set('Authorization', `Bearer ${authToken}`) - return PouchDB.fetch(url, opts) - } - }); - try { let info = await this._remoteDb.info(); if (info.error && info.error == "not_found") { diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index 2650f99d..b508eb43 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -2,7 +2,7 @@ import BaseStorageEngine from "../../base"; import EncryptedDatabase from "./db-encrypted"; import Database from "../../../database"; import { DatabaseOpenConfig } from "../../../interfaces"; -import { DatastoreServerClient, CouchDbAuthentication } from "./client"; +import { DatastoreServerClient, ContextAuth } from "./client"; import { Account } from "@verida/account"; import PublicDatabase from "./db-public"; import DbRegistry from "../../../db-registry"; @@ -19,7 +19,7 @@ class StorageEngineVerida extends BaseStorageEngine { private publicCredentials: any; // @todo private accountDid?: string; - private auth?: CouchDbAuthentication + private auth?: ContextAuth // @todo: dbmanager constructor( @@ -48,7 +48,7 @@ class StorageEngineVerida extends BaseStorageEngine { throw err } - this.auth = await this.client.getUser(this.accountDid!); + this.auth = await this.client.getContextAuth(); } /** @@ -66,7 +66,7 @@ class StorageEngineVerida extends BaseStorageEngine { * @param did * @returns {string} */ - protected async buildExternalAuth(endpointUri: string): Promise { + protected async buildExternalAuth(endpointUri: string): Promise { if (!this.account) { throw new Error('Unable to connect to external storage node. No account connected.') } @@ -74,7 +74,7 @@ class StorageEngineVerida extends BaseStorageEngine { const client = new DatastoreServerClient(this.storageContext, endpointUri); await client.setAccount(this.account!); - const auth = await client.getUser(this.accountDid!); + const auth = await client.getContextAuth(); return auth } @@ -143,7 +143,7 @@ class StorageEngineVerida extends BaseStorageEngine { throw new Error(`Unable to determine DSN for this user (${did}) and this context (${this.storageContext})`); } - let token = config.isOwner ? this.auth!.token : config.token!; + let token = config.isOwner ? this.auth!.accessToken : config.token!; if (!dsn) { throw new Error(`Unable to determine DSN for this user (${did}) and this context (${this.storageContext})`); } @@ -255,7 +255,7 @@ class StorageEngineVerida extends BaseStorageEngine { // need to build a complete dsn const auth = await this.buildExternalAuth(config.dsn!); dsn = auth.host - token = auth.token + token = auth.accessToken } const storageContextKey = await this.keyring!.getStorageContextKey( From 92f4e2cc26c25c42097d64b0149caacc2c3b160f Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 23 Aug 2022 16:38:23 +0930 Subject: [PATCH 08/27] Fix deprecated substr() reference --- .../client-ts/src/context/engines/verida/database/base-db.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-ts/src/context/engines/verida/database/base-db.ts b/packages/client-ts/src/context/engines/verida/database/base-db.ts index a279fa22..4d3d7f31 100644 --- a/packages/client-ts/src/context/engines/verida/database/base-db.ts +++ b/packages/client-ts/src/context/engines/verida/database/base-db.ts @@ -81,7 +81,7 @@ class BaseDb extends EventEmitter implements Database { this.databaseName, ].join("/"); - const hash = EncryptionUtils.hash(text).substr(2); + const hash = EncryptionUtils.hash(text).substring(2); // Database name in CouchDB must start with a letter, so prepend a `v` return "v" + hash; From 5766a2a93938e0ba402f8b301c14d896d794d920 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 23 Aug 2022 16:41:06 +0930 Subject: [PATCH 09/27] Update context name --- packages/client-ts/test/verida/messaging.tests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client-ts/test/verida/messaging.tests.ts b/packages/client-ts/test/verida/messaging.tests.ts index df29623e..46706e3f 100644 --- a/packages/client-ts/test/verida/messaging.tests.ts +++ b/packages/client-ts/test/verida/messaging.tests.ts @@ -18,8 +18,8 @@ const MESSAGE_DATA = { }] } -const CONTEXT_1 = "Verida Testing: Messaging Application 1" -const CONTEXT_2 = "Verida Testing: Messaging Application 2" +const CONTEXT_1 = "Verida Tests: Messaging Application 1" +const CONTEXT_2 = "Verida Tests: Messaging Application 2" /** * From ca612e8f845f6ee49bb863333e5721a7dd86f55c Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 24 Aug 2022 08:50:43 +0930 Subject: [PATCH 10/27] Support manual injection of contxt name --- .../src/context/engines/verida/database/engine.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index b508eb43..607ac4ef 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -101,6 +101,8 @@ class StorageEngineVerida extends BaseStorageEngine { options ); + const contextName = config.contextName ? config.contextName : this.storageContext + // Default to user's account did if not specified if (typeof(config.isOwner) == 'undefined') { config.isOwner = config.did == this.accountDid; @@ -140,12 +142,12 @@ class StorageEngineVerida extends BaseStorageEngine { let dsn = config.isOwner ? this.auth!.host! : config.dsn!; if (!dsn) { - throw new Error(`Unable to determine DSN for this user (${did}) and this context (${this.storageContext})`); + throw new Error(`Unable to determine DSN for this user (${did}) and this context (${contextName})`); } let token = config.isOwner ? this.auth!.accessToken : config.token!; if (!dsn) { - throw new Error(`Unable to determine DSN for this user (${did}) and this context (${this.storageContext})`); + throw new Error(`Unable to determine DSN for this user (${did}) and this context (${contextName})`); } // force read only access if the current user doesn't have write access @@ -181,7 +183,7 @@ class StorageEngineVerida extends BaseStorageEngine { { databaseName, did, - storageContext: this.storageContext, + storageContext: contextName, signContext: options.signingContext!, dsn, token, @@ -214,7 +216,7 @@ class StorageEngineVerida extends BaseStorageEngine { did, dsn, token, - storageContext: this.storageContext, + storageContext: contextName, signContext: options.signingContext!, permissions: config.permissions, readOnly: config.readOnly, @@ -269,7 +271,7 @@ class StorageEngineVerida extends BaseStorageEngine { { databaseName, did, - storageContext: this.storageContext, + storageContext: contextName, signContext: options.signingContext!, dsn, token, From fc4c82b1ab78bad31c878911c5a2a68100ab7945 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 25 Aug 2022 13:09:25 +0930 Subject: [PATCH 11/27] Move getAuthContext into account. Refactor. --- packages/account-node/package.json | 3 +- .../src/authTypes/VeridaDatabase.ts | 129 +++++++++++++++++ packages/account-node/src/auto.ts | 15 +- packages/account-node/src/index.ts | 2 + packages/account/src/account.ts | 6 + packages/account/src/index.ts | 7 +- packages/account/src/interfaces.ts | 28 ++++ packages/client-ts/src/context/context.ts | 2 +- .../client-ts/src/context/engines/base.ts | 7 +- .../engines/verida/database/base-db.ts | 4 + .../context/engines/verida/database/client.ts | 136 ++++++------------ .../context/engines/verida/database/engine.ts | 13 +- yarn.lock | 24 +++- 13 files changed, 268 insertions(+), 108 deletions(-) create mode 100644 packages/account-node/src/authTypes/VeridaDatabase.ts diff --git a/packages/account-node/package.json b/packages/account-node/package.json index 74d92a34..a855dcff 100644 --- a/packages/account-node/package.json +++ b/packages/account-node/package.json @@ -21,7 +21,8 @@ "@verida/did-client": "^0.1.8", "@verida/did-document": "^1.0.5", "@verida/encryption-utils": "^1.1.3", - "@verida/keyring": "^1.1.3" + "@verida/keyring": "^1.1.3", + "axios": "^0.27.2" }, "devDependencies": { "did-jwt": "5.7.0", diff --git a/packages/account-node/src/authTypes/VeridaDatabase.ts b/packages/account-node/src/authTypes/VeridaDatabase.ts new file mode 100644 index 00000000..da84e72c --- /dev/null +++ b/packages/account-node/src/authTypes/VeridaDatabase.ts @@ -0,0 +1,129 @@ +import Axios from "axios"; +import AutoAccount from "../auto"; +import { Account, VeridaDatabaseAuthContext, AuthType, VeridaDatabaseAuthTypeConfig } from "@verida/account"; + + + +export default class VeridaDatabaseAuthType implements AuthType { + + private contextAuth?: VeridaDatabaseAuthContext + + public async getAuthContext(account: AutoAccount, contextName: string, config: VeridaDatabaseAuthTypeConfig): Promise { + const serverUrl = config.serverUrl + const deviceId = config.serverUrl + const forceAccessToken = config.forceAccessToken + const publicSigningKey = config.publicSigningKey + + // @todo: how are invalid access tokens going to produce an error? how to catch and then regenerate? + // - expired token stored in session when loading the app + // - token expires while using the app + + // ------------------------- + + // We already have a context auth object, so reuse it unless + // requested to force create access token. + // This can happen if the access token has expired when being + // used and it can automatically be re-requested. + if (this.contextAuth && !forceAccessToken) { + //console.log('getContextAuth(): exists, returning') + return this.contextAuth + } + + const did = await account!.did() + + // No context auth or no refresh token, so generate it by signing a consent message + if (!this.contextAuth || !this.contextAuth.refreshToken) { + //console.log('getContextAuth(): no refreshtoken, generating') + // @todo: get a new refresh token if getting close to expiring? + + let authJwt + try { + // Generate an auth token to start auth process + const authJwtResponse = await this.getAxios(contextName).post(serverUrl + "auth/generateAuthJwt",{ + did, + contextName + }) + + authJwt = authJwtResponse.data.authJwt + } catch (err: any) { + throw new Error(`Unable to connect to storage node (${serverUrl}): ${err.message}`) + } + + let refreshResponse + try { + // Generate a refresh token by authenticating + const consentMessage = `Authenticate this application context: "${contextName}"?\n\n${did.toLowerCase()}\n${authJwt.authRequestId}` + const signature = await account!.sign(consentMessage) + + refreshResponse = await this.getAxios(contextName).post(serverUrl + "auth/authenticate",{ + authJwt: authJwt.authJwt, + did, + contextName, + signature, + deviceId: deviceId + }); + + //console.log('refresh response', refreshResponse.data) + } catch (err: any) { + throw new Error(`Unable to authenticate with storage node (${serverUrl}): ${err.message}`) + } + + //console.log("authenticate response", refreshResponse.data) + + const refreshToken = refreshResponse.data.refreshToken + const host = refreshResponse.data.host + const accessToken = refreshResponse.data.accessToken + + this.contextAuth = { + refreshToken, + accessToken, + host, + publicSigningKey + } + + //console.log(this.contextAuth!) + + return this.contextAuth! + } + + // No access token, but have a refresh token, so generate access token + if (this.contextAuth && !this.contextAuth.accessToken) { + //console.log('getContextAuth(): no access token, but refresh token, so generating access token') + + const accessResponse = await this.getAxios(contextName).post(serverUrl + "auth/connect",{ + refreshToken: this.contextAuth.refreshToken, + did, + contextName: contextName + }); + + // @todo: handle connect error + // @todo: handle connect error (accessResponse.data.status != 'success') + + //console.log("connect response", accessResponse.data) + + const accessToken = accessResponse.data.accessToken + this.contextAuth.accessToken = accessToken + return this.contextAuth + } + + // @todo: test if connection is valid? + + return this.contextAuth! + } + + private getAxios(storageContext: string, accessToken?: string) { + let config: any = { + headers: { + // @todo: Application-Name needs to become Storage-Context + "Application-Name": storageContext, + }, + }; + + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}` + } + + return Axios.create(config); + } + +} \ No newline at end of file diff --git a/packages/account-node/src/auto.ts b/packages/account-node/src/auto.ts index f6df8314..d95aed2e 100644 --- a/packages/account-node/src/auto.ts +++ b/packages/account-node/src/auto.ts @@ -1,11 +1,12 @@ import { Interfaces, StorageLink, DIDStorageConfig } from '@verida/storage-link' import { Keyring } from '@verida/keyring' -import { Account, AccountConfig, Config } from '@verida/account' +import { Account, AccountConfig, Config, VeridaDatabaseAuthTypeConfig, AuthContext, AuthTypeConfig } from '@verida/account' import { NodeAccountConfig } from './interfaces' import { DIDClient, Wallet } from '@verida/did-client' import EncryptionUtils from "@verida/encryption-utils" import { Interfaces as DIDDocumentInterfaces } from "@verida/did-document" +import VeridaDatabaseAuthType from "./authTypes/VeridaDatabase" /** * An Authenticator that automatically signs everything @@ -102,4 +103,16 @@ export default class AutoAccount extends Account { return this.didClient } + public async getAuthContext(contextName: string, authConfig: AuthTypeConfig, authType: string = "VeridaDatabase"): Promise { + if (authType == "VeridaDatabase") { + // @todo: Do we need to keep a cache of all previous auth contexts to handle refreshing + // and avoid re-authenticating every time? + + const auth = new VeridaDatabaseAuthType() + return auth.getAuthContext(this, contextName, authConfig) + } + + throw new Error(`Unknown auth context type (${authType}`) + } + } \ No newline at end of file diff --git a/packages/account-node/src/index.ts b/packages/account-node/src/index.ts index f8536a7f..f6b95892 100644 --- a/packages/account-node/src/index.ts +++ b/packages/account-node/src/index.ts @@ -1,7 +1,9 @@ import AutoAccount from "./auto" import LimitedAccount from "./limited" +import VeridaDatabaseAuthType from "./authTypes/VeridaDatabase" export { AutoAccount, + VeridaDatabaseAuthType, LimitedAccount } \ No newline at end of file diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index 40bc820c..8716a747 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -2,6 +2,7 @@ import { Keyring } from '@verida/keyring' import { Interfaces } from '@verida/storage-link' import { createJWT, ES256KSigner } from 'did-jwt' import { encodeBase64 } from "tweetnacl-util" +import { AuthContext, AuthTypeConfig } from './interfaces' const _ = require('lodash') @@ -104,4 +105,9 @@ export default class Account { public async disconnect(contextName?: string): Promise { throw new Error("Not implemented.") } + + public async getAuthContext(contextName: string, authConfig: AuthTypeConfig, authType: string = "VeridaDatabase"): Promise { + throw new Error("Not implemented.") + } + } \ No newline at end of file diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index 874d49b1..028b61c6 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -1,10 +1,15 @@ import Account from "./account" -import { AccountConfig, EnvironmentType } from "./interfaces" +import { AccountConfig, EnvironmentType, AuthContext, AuthType, AuthTypeConfig, VeridaDatabaseAuthContext, VeridaDatabaseAuthTypeConfig } from "./interfaces" import Config from "./config" export { Account, AccountConfig, EnvironmentType, + AuthContext, + AuthType, + AuthTypeConfig, + VeridaDatabaseAuthContext, + VeridaDatabaseAuthTypeConfig, Config } \ No newline at end of file diff --git a/packages/account/src/interfaces.ts b/packages/account/src/interfaces.ts index 9e4b1182..1c6834fe 100644 --- a/packages/account/src/interfaces.ts +++ b/packages/account/src/interfaces.ts @@ -1,4 +1,5 @@ import { Interfaces } from "@verida/storage-link" +import Account from "./account" export interface AccountConfig { defaultDatabaseServer: Interfaces.SecureContextEndpoint, @@ -11,4 +12,31 @@ export enum EnvironmentType { LOCAL = 'local', TESTNET = 'testnet', MAINNET = 'mainnet' +} + +export interface AuthContext { + publicSigningKey: string +} + +export interface AuthTypeConfig { +} + +export interface AuthType { + getAuthContext(account: Account, contextName: string, config: AuthTypeConfig): Promise +} + + +//// VeridaDatabase Authentication Interfaces + +export interface VeridaDatabaseAuthContext extends AuthContext { + refreshToken: string, + accessToken: string, + host: string +} + +export interface VeridaDatabaseAuthTypeConfig extends AuthTypeConfig { + serverUrl: string, + deviceId: string, + forceAccessToken: boolean + publicSigningKey: string } \ No newline at end of file diff --git a/packages/client-ts/src/context/context.ts b/packages/client-ts/src/context/context.ts index 92fb7276..ab68097f 100644 --- a/packages/client-ts/src/context/context.ts +++ b/packages/client-ts/src/context/context.ts @@ -154,7 +154,7 @@ class Context { const databaseEngine = new engine( this.contextName, this.dbRegistry, - contextConfig.services.databaseServer.endpointUri + contextConfig ); /** diff --git a/packages/client-ts/src/context/engines/base.ts b/packages/client-ts/src/context/engines/base.ts index 78397b98..51305c49 100644 --- a/packages/client-ts/src/context/engines/base.ts +++ b/packages/client-ts/src/context/engines/base.ts @@ -5,6 +5,7 @@ import Database from "../database"; import Datastore from "../datastore"; import DbRegistry from "../db-registry"; import ContextNotFoundError from "./ContextNotFoundError"; +import { Interfaces } from "@verida/storage-link"; /** * @category @@ -14,6 +15,7 @@ class BaseStorageEngine { protected storageContext: string; protected dbRegistry: DbRegistry; protected endpointUri: string; + protected contextConfig: Interfaces.SecureContextConfig; protected account?: Account; protected keyring?: Keyring; @@ -21,11 +23,12 @@ class BaseStorageEngine { constructor( storageContext: string, dbRegistry: DbRegistry, - endpointUri: string + contextConfig: Interfaces.SecureContextConfig, ) { this.storageContext = storageContext; this.dbRegistry = dbRegistry; - this.endpointUri = endpointUri; + this.endpointUri = contextConfig.services.databaseServer.endpointUri; + this.contextConfig = contextConfig } public async connectAccount(account: Account) { diff --git a/packages/client-ts/src/context/engines/verida/database/base-db.ts b/packages/client-ts/src/context/engines/verida/database/base-db.ts index f905a476..3e159507 100644 --- a/packages/client-ts/src/context/engines/verida/database/base-db.ts +++ b/packages/client-ts/src/context/engines/verida/database/base-db.ts @@ -423,6 +423,10 @@ class BaseDb extends EventEmitter implements Database { public async info(): Promise { throw new Error("Not implemented"); } + + public async disconnectDevice(deviceId: string="Test device"): Promise { + return await this.client.disconnectDevice(deviceId) + } } export default BaseDb; diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index c016095a..d279ceeb 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -1,5 +1,5 @@ import Axios from "axios"; -import { Account } from "@verida/account"; +import { Account, VeridaDatabaseAuthContext } from "@verida/account"; /** * Interface for RemoteClientAuthentication @@ -21,10 +21,12 @@ export class DatastoreServerClient { private contextAuth?: ContextAuth; private account?: Account; private deviceId: string + private signKey: string - constructor(storageContext: string, serverUrl: string, deviceId: string="Test device") { + constructor(storageContext: string, serverUrl: string, signKey: string, deviceId: string="Test device") { this.storageContext = storageContext; this.serverUrl = serverUrl; + this.signKey = signKey this.deviceId = deviceId; } @@ -39,101 +41,14 @@ export class DatastoreServerClient { throw new Error("Unable to connect. No account set.") } - // @todo: how are invalid access tokens going to produce an error? how to catch and then regenerate? - // - expired token stored in session when loading the app - // - token expires while using the app + const authContext = await this.account.getAuthContext(this.storageContext, { + serverUrl: this.serverUrl, + deviceId: this.deviceId, + publicSigningKey: this.signKey, + forceAccessToken + }) - // ------------------------- - - // We already have a context auth object, so reuse it unless - // requested to force create access token. - // This can happen if the access token has expired when being - // used and it can automatically be re-requested. - if (this.contextAuth && !forceAccessToken) { - return this.contextAuth - } - - const did = await this.account!.did() - - // No context auth or no refresh token, so generate it by signing a consent message - if (!this.contextAuth || !this.contextAuth.refreshToken) { - // @todo: get a new refresh token if getting close to expiring? - - let authJwt - try { - // Generate an auth token to start auth process - const authJwtResponse = await this.getAxios().post(this.serverUrl + "auth/generateAuthJwt",{ - did, - contextName: this.storageContext - }); - - // @todo: handle connection error - - //console.log("generateAuthJwt response", authJwtResponse.data) - authJwt = authJwtResponse.data.authJwt - } catch (err: any) { - throw new Error(`Context Authentication Error. ${err.message}`) - } - - let refreshResponse - try { - // Generate a refresh token by authenticating - const consentMessage = `Authenticate this application context: "${this.storageContext}"?\n\n${did.toLowerCase()}\n${authJwt.authRequestId}` - const signature = await this.account!.sign(consentMessage) - - refreshResponse = await this.getAxios().post(this.serverUrl + "auth/authenticate",{ - authJwt: authJwt.authJwt, - did, - contextName: this.storageContext, - signature, - deviceId: this.deviceId - }); - - // @todo: handle auth error (refreshResponse.data.status != 'success') - } catch (err: any) { - // @todo: handle connection error - console.log(err) - throw new Error(`Context Authentication Error. ${err.message}`) - } - - //console.log("authenticate response", refreshResponse.data) - - const refreshToken = refreshResponse.data.refreshToken - const host = refreshResponse.data.host - const accessToken = refreshResponse.data.accessToken - - this.contextAuth = { - refreshToken, - accessToken, - host - } - - //console.log(this.contextAuth!) - - return this.contextAuth - } - - // No access token, but have a refresh token, so generate access token - if (this.contextAuth && !this.contextAuth.accessToken) { - const accessResponse = await this.getAxios().post(this.serverUrl + "auth/connect",{ - refreshToken: this.contextAuth.refreshToken, - did, - contextName: this.storageContext - }); - - // @todo: handle connect error - // @todo: handle connect error (accessResponse.data.status != 'success') - - //console.log("connect response", accessResponse.data) - - const accessToken = accessResponse.data.accessToken - this.contextAuth.accessToken = accessToken - return this.contextAuth - } - - // @todo: test if connection is valid? - - return this.contextAuth! + return authContext } public async getPublicUser() { @@ -182,6 +97,35 @@ export class DatastoreServerClient { return Axios.create(config); } + + public async disconnectDevice(deviceId: string): Promise { + if (!this.account) { + throw new Error("Unable to disconnect device. No account connected.") + } + + const did = await this.account.did(); + + const consentMessage = `Invalidate device for this application context: "${this.storageContext}"?\n\n${did.toLowerCase()}\n${deviceId}` + const signature = await this.account.sign(consentMessage) + + try { + const response = await this.getAxios().post(`${this.serverUrl}auth/invalidateDeviceId`, { + did, + contextName: this.storageContext, + deviceId: deviceId, + signature + }); + + return response.data.status == 'success' + } catch (err: any) { + if (err.response && err.response.data) { + throw new Error(`Unable to disconnect device: ${JSON.stringify(err.response.data.data)}`) + } + else { + throw new Error(`Unable to disconnect device: ${err.message}`) + } + } + } } export default DatastoreServerClient; diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index 607ac4ef..c604a812 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -6,6 +6,7 @@ import { DatastoreServerClient, ContextAuth } from "./client"; import { Account } from "@verida/account"; import PublicDatabase from "./db-public"; import DbRegistry from "../../../db-registry"; +import { Interfaces } from "@verida/storage-link"; const _ = require("lodash"); @@ -25,12 +26,13 @@ class StorageEngineVerida extends BaseStorageEngine { constructor( storageContext: string, dbRegistry: DbRegistry, - endpointUri: string + contextConfig: Interfaces.SecureContextConfig, ) { - super(storageContext, dbRegistry, endpointUri); + super(storageContext, dbRegistry, contextConfig); this.client = new DatastoreServerClient( this.storageContext, - this.endpointUri + this.endpointUri, + contextConfig.publicKeys.signKey.publicKeyHex ); } @@ -71,7 +73,7 @@ class StorageEngineVerida extends BaseStorageEngine { throw new Error('Unable to connect to external storage node. No account connected.') } - const client = new DatastoreServerClient(this.storageContext, endpointUri); + const client = new DatastoreServerClient(this.storageContext, endpointUri, this.contextConfig.publicKeys.signKey.publicKeyHex); await client.setAccount(this.account!); const auth = await client.getContextAuth(); @@ -316,7 +318,8 @@ class StorageEngineVerida extends BaseStorageEngine { super.logout(); this.client = new DatastoreServerClient( this.storageContext, - this.endpointUri + this.endpointUri, + this.contextConfig.publicKeys.signKey.publicKeyHex ); } diff --git a/yarn.lock b/yarn.lock index 95730783..2fefb709 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1979,6 +1979,14 @@ axios@^0.23.0: dependencies: follow-redirects "^1.14.4" +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -2459,7 +2467,7 @@ columnify@^1.5.4: strip-ansi "^3.0.0" wcwidth "^1.0.0" -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -3423,6 +3431,11 @@ follow-redirects@^1.14.4: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== +follow-redirects@^1.14.9: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -3433,6 +3446,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" From f5caacdfcde7a0e7613e912fd6876c0e702281c1 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 25 Aug 2022 13:17:26 +0930 Subject: [PATCH 12/27] Support caching auth contexts --- packages/account-node/src/auto.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/account-node/src/auto.ts b/packages/account-node/src/auto.ts index d95aed2e..442e7cda 100644 --- a/packages/account-node/src/auto.ts +++ b/packages/account-node/src/auto.ts @@ -1,6 +1,6 @@ import { Interfaces, StorageLink, DIDStorageConfig } from '@verida/storage-link' import { Keyring } from '@verida/keyring' -import { Account, AccountConfig, Config, VeridaDatabaseAuthTypeConfig, AuthContext, AuthTypeConfig } from '@verida/account' +import { Account, AccountConfig, Config, AuthType, VeridaDatabaseAuthTypeConfig, AuthContext, AuthTypeConfig } from '@verida/account' import { NodeAccountConfig } from './interfaces' import { DIDClient, Wallet } from '@verida/did-client' @@ -18,6 +18,7 @@ export default class AutoAccount extends Account { private wallet: Wallet protected accountConfig: AccountConfig + protected contextAuths: Record = {} constructor(accountConfig: AccountConfig, autoConfig: NodeAccountConfig) { super() @@ -104,12 +105,13 @@ export default class AutoAccount extends Account { } public async getAuthContext(contextName: string, authConfig: AuthTypeConfig, authType: string = "VeridaDatabase"): Promise { - if (authType == "VeridaDatabase") { - // @todo: Do we need to keep a cache of all previous auth contexts to handle refreshing - // and avoid re-authenticating every time? + if (this.contextAuths[contextName]) { + return this.contextAuths[contextName].getAuthContext(this, contextName, authConfig) + } - const auth = new VeridaDatabaseAuthType() - return auth.getAuthContext(this, contextName, authConfig) + if (authType == "VeridaDatabase") { + this.contextAuths[contextName] = new VeridaDatabaseAuthType() + return this.contextAuths[contextName].getAuthContext(this, contextName, authConfig) } throw new Error(`Unknown auth context type (${authType}`) From b7fc5bc48358c41e52ba322be5c29fa3e6a1397c Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 25 Aug 2022 17:11:10 +0930 Subject: [PATCH 13/27] Refactor auth context to be implemented in account instance. Better separation of auth context and client operations. --- .../src/authTypes/VeridaDatabase.ts | 73 ++++++++++++++----- packages/account-node/src/auto.ts | 18 +++-- packages/account/src/account.ts | 2 +- packages/account/src/interfaces.ts | 34 ++++++--- packages/client-ts/src/context/context.ts | 13 +++- .../engines/verida/database/base-db.ts | 6 +- .../context/engines/verida/database/client.ts | 73 +++---------------- .../context/engines/verida/database/engine.ts | 39 +++++----- 8 files changed, 140 insertions(+), 118 deletions(-) diff --git a/packages/account-node/src/authTypes/VeridaDatabase.ts b/packages/account-node/src/authTypes/VeridaDatabase.ts index da84e72c..8656dc0d 100644 --- a/packages/account-node/src/authTypes/VeridaDatabase.ts +++ b/packages/account-node/src/authTypes/VeridaDatabase.ts @@ -1,18 +1,27 @@ import Axios from "axios"; import AutoAccount from "../auto"; -import { Account, VeridaDatabaseAuthContext, AuthType, VeridaDatabaseAuthTypeConfig } from "@verida/account"; +import { Interfaces } from '@verida/storage-link' +import { Account, VeridaDatabaseAuthContext, AuthType, AuthTypeConfig, VeridaDatabaseAuthTypeConfig } from "@verida/account"; +export default class VeridaDatabaseAuthType extends AuthType { + protected contextAuth?: VeridaDatabaseAuthContext + protected account: AutoAccount -export default class VeridaDatabaseAuthType implements AuthType { + public constructor(account: Account, contextName: string, serviceEndpoint: Interfaces.SecureContextEndpoint, signKey: Interfaces.SecureContextPublicKey) { + super(account, contextName, serviceEndpoint, signKey) + this.account = account + } + + public async getAuthContext(config?: VeridaDatabaseAuthTypeConfig): Promise { + const serverUrl = config && config.endpointUri ? config.endpointUri : this.serviceEndpoint.endpointUri + const deviceId = config && config.deviceId ? config.deviceId : "Test Device" - private contextAuth?: VeridaDatabaseAuthContext + let forceAccessToken = false - public async getAuthContext(account: AutoAccount, contextName: string, config: VeridaDatabaseAuthTypeConfig): Promise { - const serverUrl = config.serverUrl - const deviceId = config.serverUrl - const forceAccessToken = config.forceAccessToken - const publicSigningKey = config.publicSigningKey + if (config) { + forceAccessToken = config.forceAccessToken ? config.forceAccessToken : true + } // @todo: how are invalid access tokens going to produce an error? how to catch and then regenerate? // - expired token stored in session when loading the app @@ -25,11 +34,10 @@ export default class VeridaDatabaseAuthType implements AuthType { // This can happen if the access token has expired when being // used and it can automatically be re-requested. if (this.contextAuth && !forceAccessToken) { - //console.log('getContextAuth(): exists, returning') return this.contextAuth } - const did = await account!.did() + const did = await this.account!.did() // No context auth or no refresh token, so generate it by signing a consent message if (!this.contextAuth || !this.contextAuth.refreshToken) { @@ -39,9 +47,9 @@ export default class VeridaDatabaseAuthType implements AuthType { let authJwt try { // Generate an auth token to start auth process - const authJwtResponse = await this.getAxios(contextName).post(serverUrl + "auth/generateAuthJwt",{ + const authJwtResponse = await this.getAxios(this.contextName).post(serverUrl + "auth/generateAuthJwt",{ did, - contextName + contextName: this.contextName }) authJwt = authJwtResponse.data.authJwt @@ -52,13 +60,13 @@ export default class VeridaDatabaseAuthType implements AuthType { let refreshResponse try { // Generate a refresh token by authenticating - const consentMessage = `Authenticate this application context: "${contextName}"?\n\n${did.toLowerCase()}\n${authJwt.authRequestId}` - const signature = await account!.sign(consentMessage) + const consentMessage = `Authenticate this application context: "${this.contextName}"?\n\n${did.toLowerCase()}\n${authJwt.authRequestId}` + const signature = await this.account.sign(consentMessage) - refreshResponse = await this.getAxios(contextName).post(serverUrl + "auth/authenticate",{ + refreshResponse = await this.getAxios(this.contextName).post(serverUrl + "auth/authenticate",{ authJwt: authJwt.authJwt, did, - contextName, + contextName: this.contextName, signature, deviceId: deviceId }); @@ -78,7 +86,7 @@ export default class VeridaDatabaseAuthType implements AuthType { refreshToken, accessToken, host, - publicSigningKey + publicSigningKey: this.signKey } //console.log(this.contextAuth!) @@ -90,10 +98,10 @@ export default class VeridaDatabaseAuthType implements AuthType { if (this.contextAuth && !this.contextAuth.accessToken) { //console.log('getContextAuth(): no access token, but refresh token, so generating access token') - const accessResponse = await this.getAxios(contextName).post(serverUrl + "auth/connect",{ + const accessResponse = await this.getAxios(this.contextName).post(serverUrl + "auth/connect",{ refreshToken: this.contextAuth.refreshToken, did, - contextName: contextName + contextName: this.contextName }); // @todo: handle connect error @@ -111,6 +119,33 @@ export default class VeridaDatabaseAuthType implements AuthType { return this.contextAuth! } + public async disconnectDevice(deviceId: string): Promise { + const contextAuth = await this.getAuthContext() + + const did = await this.account.did(); + + const consentMessage = `Invalidate device for this application context: "${this.contextName}"?\n\n${did.toLowerCase()}\n${deviceId}` + const signature = await this.account.sign(consentMessage) + + try { + const response = await this.getAxios(this.contextName).post(`${contextAuth.host}auth/invalidateDeviceId`, { + did, + contextName: this.contextName, + deviceId: deviceId, + signature + }); + + return response.data.status == 'success' + } catch (err: any) { + if (err.response && err.response.data) { + throw new Error(`Unable to disconnect device: ${JSON.stringify(err.response.data.data)}`) + } + else { + throw new Error(`Unable to disconnect device: ${err.message}`) + } + } + } + private getAxios(storageContext: string, accessToken?: string) { let config: any = { headers: { diff --git a/packages/account-node/src/auto.ts b/packages/account-node/src/auto.ts index 442e7cda..b31a389e 100644 --- a/packages/account-node/src/auto.ts +++ b/packages/account-node/src/auto.ts @@ -104,17 +104,23 @@ export default class AutoAccount extends Account { return this.didClient } - public async getAuthContext(contextName: string, authConfig: AuthTypeConfig, authType: string = "VeridaDatabase"): Promise { + public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig?: AuthTypeConfig, authType = "database"): Promise { + // Use existing context auth instance if it exists if (this.contextAuths[contextName]) { - return this.contextAuths[contextName].getAuthContext(this, contextName, authConfig) + return this.contextAuths[contextName].getAuthContext( authConfig) } - if (authType == "VeridaDatabase") { - this.contextAuths[contextName] = new VeridaDatabaseAuthType() - return this.contextAuths[contextName].getAuthContext(this, contextName, authConfig) + const signKey = contextConfig.publicKeys.signKey + + // @todo: Currently hard code database server, need to support other service types in the future + const serviceEndpoint = contextConfig.services.databaseServer + + if (serviceEndpoint.type == "VeridaDatabase") { + this.contextAuths[contextName] = new VeridaDatabaseAuthType(this, contextName, serviceEndpoint, signKey) + return this.contextAuths[contextName].getAuthContext( authConfig) } - throw new Error(`Unknown auth context type (${authType}`) + throw new Error(`Unknown auth context type (${authType})`) } } \ No newline at end of file diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index 8716a747..68268ce9 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -106,7 +106,7 @@ export default class Account { throw new Error("Not implemented.") } - public async getAuthContext(contextName: string, authConfig: AuthTypeConfig, authType: string = "VeridaDatabase"): Promise { + public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig?: AuthTypeConfig, authType: string = "database"): Promise { throw new Error("Not implemented.") } diff --git a/packages/account/src/interfaces.ts b/packages/account/src/interfaces.ts index 1c6834fe..bea69a77 100644 --- a/packages/account/src/interfaces.ts +++ b/packages/account/src/interfaces.ts @@ -15,28 +15,42 @@ export enum EnvironmentType { } export interface AuthContext { - publicSigningKey: string + publicSigningKey: Interfaces.SecureContextPublicKey } export interface AuthTypeConfig { } -export interface AuthType { - getAuthContext(account: Account, contextName: string, config: AuthTypeConfig): Promise -} +export class AuthType { + + protected contextAuth?: AuthContext + protected account: Account + protected contextName: string + protected serviceEndpoint: Interfaces.SecureContextEndpoint + protected signKey: Interfaces.SecureContextPublicKey + public constructor(account: Account, contextName: string, serviceEndpoint: Interfaces.SecureContextEndpoint, signKey: Interfaces.SecureContextPublicKey) { + this.account = account + this.contextName = contextName + this.serviceEndpoint = serviceEndpoint + this.signKey = signKey + } + + getAuthContext(config: AuthTypeConfig): Promise { + throw new Error("Not implemented") + } +} //// VeridaDatabase Authentication Interfaces export interface VeridaDatabaseAuthContext extends AuthContext { - refreshToken: string, - accessToken: string, + refreshToken?: string, + accessToken?: string, host: string } export interface VeridaDatabaseAuthTypeConfig extends AuthTypeConfig { - serverUrl: string, - deviceId: string, - forceAccessToken: boolean - publicSigningKey: string + deviceId?: string, + endpointUri?: string, + forceAccessToken?: boolean } \ No newline at end of file diff --git a/packages/client-ts/src/context/context.ts b/packages/client-ts/src/context/context.ts index ab68097f..1d4e3be6 100644 --- a/packages/client-ts/src/context/context.ts +++ b/packages/client-ts/src/context/context.ts @@ -1,4 +1,4 @@ -import { Account } from "@verida/account"; +import { Account, AuthContext, AuthType, AuthTypeConfig } from "@verida/account"; import { Interfaces } from "@verida/storage-link"; import BaseStorageEngine from "./engines/base"; @@ -413,6 +413,17 @@ class Context { public getDbRegistry(): DbRegistry { return this.dbRegistry; } + + public async getAuthContext(authConfig?: AuthTypeConfig, authType?: string): Promise { + if (!this.account) { + throw new Error("No authenticated user"); + } + + const did = await this.account!.did() + const contextConfig = await this.getContextConfig(did, false) + + return this.account!.getAuthContext(this.contextName, contextConfig, authConfig, authType) + } } export default Context; diff --git a/packages/client-ts/src/context/engines/verida/database/base-db.ts b/packages/client-ts/src/context/engines/verida/database/base-db.ts index 3e159507..02727b3b 100644 --- a/packages/client-ts/src/context/engines/verida/database/base-db.ts +++ b/packages/client-ts/src/context/engines/verida/database/base-db.ts @@ -425,7 +425,11 @@ class BaseDb extends EventEmitter implements Database { } public async disconnectDevice(deviceId: string="Test device"): Promise { - return await this.client.disconnectDevice(deviceId) + if (!this.account) { + throw new Error("Unable to disconnect device. No account connected.") + } + + return await this.account.disconnectDevice(deviceId) } } diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index d279ceeb..2ddc14bf 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -15,44 +15,23 @@ export interface ContextAuth { * Modules */ export class DatastoreServerClient { - private serverUrl: string; + private authContext?: VeridaDatabaseAuthContext private storageContext: string; - private contextAuth?: ContextAuth; - private account?: Account; - private deviceId: string - private signKey: string + private serviceEndpoint: Account; - constructor(storageContext: string, serverUrl: string, signKey: string, deviceId: string="Test device") { + constructor(storageContext: string, serviceEndpoint: string, authContext?: VeridaDatabaseAuthContext) { + this.authContext = authContext this.storageContext = storageContext; - this.serverUrl = serverUrl; - this.signKey = signKey - this.deviceId = deviceId; + this.serviceEndpoint = serviceEndpoint } - public async setAccount(account: Account) { - this.account = account; - let did = await account.did(); - did = did.toLowerCase() - } - - public async getContextAuth(forceAccessToken=false): Promise { - if (!this.account) { - throw new Error("Unable to connect. No account set.") - } - - const authContext = await this.account.getAuthContext(this.storageContext, { - serverUrl: this.serverUrl, - deviceId: this.deviceId, - publicSigningKey: this.signKey, - forceAccessToken - }) - - return authContext + public async setAuthContext(authContext: VeridaDatabaseAuthContext) { + this.authContext = authContext } public async getPublicUser() { - return this.getAxios().get(this.serverUrl + "auth/public"); + return this.getAxios().get(this.serviceEndpoint + "auth/public"); } public async createDatabase( @@ -60,9 +39,7 @@ export class DatastoreServerClient { databaseName: string, config: any = {} ) { - const contextAuth = await this.getContextAuth() - - return this.getAxios(contextAuth.accessToken).post(this.serverUrl + "user/createDatabase", { + return this.getAxios(this.authContext.accessToken).post(this.serviceEndpoint + "user/createDatabase", { did: did, databaseName: databaseName, options: config, @@ -74,9 +51,7 @@ export class DatastoreServerClient { databaseName: string, config: any = {} ) { - const contextAuth = await this.getContextAuth() - - return this.getAxios(contextAuth.accessToken).post(this.serverUrl + "user/updateDatabase", { + return this.getAxios(this.authContext.accessToken).post(this.serviceEndpoint + "user/updateDatabase", { did: did, databaseName: databaseName, options: config, @@ -98,34 +73,6 @@ export class DatastoreServerClient { return Axios.create(config); } - public async disconnectDevice(deviceId: string): Promise { - if (!this.account) { - throw new Error("Unable to disconnect device. No account connected.") - } - - const did = await this.account.did(); - - const consentMessage = `Invalidate device for this application context: "${this.storageContext}"?\n\n${did.toLowerCase()}\n${deviceId}` - const signature = await this.account.sign(consentMessage) - - try { - const response = await this.getAxios().post(`${this.serverUrl}auth/invalidateDeviceId`, { - did, - contextName: this.storageContext, - deviceId: deviceId, - signature - }); - - return response.data.status == 'success' - } catch (err: any) { - if (err.response && err.response.data) { - throw new Error(`Unable to disconnect device: ${JSON.stringify(err.response.data.data)}`) - } - else { - throw new Error(`Unable to disconnect device: ${err.message}`) - } - } - } } export default DatastoreServerClient; diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index c604a812..186bfc81 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -2,11 +2,12 @@ import BaseStorageEngine from "../../base"; import EncryptedDatabase from "./db-encrypted"; import Database from "../../../database"; import { DatabaseOpenConfig } from "../../../interfaces"; -import { DatastoreServerClient, ContextAuth } from "./client"; +import { DatastoreServerClient } from "./client"; import { Account } from "@verida/account"; import PublicDatabase from "./db-public"; import DbRegistry from "../../../db-registry"; import { Interfaces } from "@verida/storage-link"; +import { VeridaDatabaseAuthContext } from "@verida/account" const _ = require("lodash"); @@ -17,12 +18,11 @@ const _ = require("lodash"); class StorageEngineVerida extends BaseStorageEngine { private client: DatastoreServerClient; - private publicCredentials: any; // @todo - + private publicCredentials: any; // @todo fix typing private accountDid?: string; - private auth?: ContextAuth + private auth?: VeridaDatabaseAuthContext - // @todo: dbmanager + // @todo: specify device id // deviceId: string="Test device" constructor( storageContext: string, dbRegistry: DbRegistry, @@ -31,8 +31,7 @@ class StorageEngineVerida extends BaseStorageEngine { super(storageContext, dbRegistry, contextConfig); this.client = new DatastoreServerClient( this.storageContext, - this.endpointUri, - contextConfig.publicKeys.signKey.publicKeyHex + contextConfig.services.databaseServer.endpointUri ); } @@ -40,7 +39,10 @@ class StorageEngineVerida extends BaseStorageEngine { try { await super.connectAccount(account); - await this.client.setAccount(account); + const auth = await account.getAuthContext(this.storageContext, this.contextConfig) + this.auth = auth + await this.client.setAuthContext(this.auth) + this.accountDid = await this.account!.did(); } catch (err: any) { if (err.name == "ContextNotFoundError") { @@ -49,8 +51,6 @@ class StorageEngineVerida extends BaseStorageEngine { throw err } - - this.auth = await this.client.getContextAuth(); } /** @@ -68,16 +68,22 @@ class StorageEngineVerida extends BaseStorageEngine { * @param did * @returns {string} */ - protected async buildExternalAuth(endpointUri: string): Promise { + protected async buildExternalAuth(endpointUri: string): Promise { if (!this.account) { throw new Error('Unable to connect to external storage node. No account connected.') } - const client = new DatastoreServerClient(this.storageContext, endpointUri, this.contextConfig.publicKeys.signKey.publicKeyHex); + const auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { + endpointUri + }) + + return auth + + /*const client = new DatastoreServerClient(this.storageContext, this.contextConfig); await client.setAccount(this.account!); const auth = await client.getContextAuth(); - return auth + return auth*/ } /** @@ -258,8 +264,8 @@ class StorageEngineVerida extends BaseStorageEngine { if (!config.isOwner && this.account) { // need to build a complete dsn const auth = await this.buildExternalAuth(config.dsn!); - dsn = auth.host - token = auth.accessToken + dsn = auth.host; + token = auth.accessToken; } const storageContextKey = await this.keyring!.getStorageContextKey( @@ -318,8 +324,7 @@ class StorageEngineVerida extends BaseStorageEngine { super.logout(); this.client = new DatastoreServerClient( this.storageContext, - this.endpointUri, - this.contextConfig.publicKeys.signKey.publicKeyHex + this.contextConfig.services.databaseServer.endpointUri ); } From f92259961f12ee89343127c142341414732b6181 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 28 Aug 2022 14:12:54 +0930 Subject: [PATCH 14/27] Support managing auth contexts and disconnecting devices. Support Auth Context Account type to help with testing. --- .../src/authTypes/VeridaDatabase.ts | 30 ++++++------- packages/account-node/src/authcontext.ts | 44 +++++++++++++++++++ packages/account-node/src/auto.ts | 18 ++++++-- packages/account-node/src/index.ts | 4 +- packages/account-node/src/limited.ts | 2 +- packages/account/src/account.ts | 8 +++- packages/account/src/interfaces.ts | 12 ++++- 7 files changed, 93 insertions(+), 25 deletions(-) create mode 100644 packages/account-node/src/authcontext.ts diff --git a/packages/account-node/src/authTypes/VeridaDatabase.ts b/packages/account-node/src/authTypes/VeridaDatabase.ts index 8656dc0d..7c19f89b 100644 --- a/packages/account-node/src/authTypes/VeridaDatabase.ts +++ b/packages/account-node/src/authTypes/VeridaDatabase.ts @@ -13,27 +13,22 @@ export default class VeridaDatabaseAuthType extends AuthType { this.account = account } - public async getAuthContext(config?: VeridaDatabaseAuthTypeConfig): Promise { + public async getAuthContext(config: VeridaDatabaseAuthTypeConfig = { + deviceId: "Test device", + force: false + }): Promise { const serverUrl = config && config.endpointUri ? config.endpointUri : this.serviceEndpoint.endpointUri - const deviceId = config && config.deviceId ? config.deviceId : "Test Device" - let forceAccessToken = false - - if (config) { - forceAccessToken = config.forceAccessToken ? config.forceAccessToken : true + // If we have an invalid access token, clear it + if (this.contextAuth && config.invalidAccessToken) { + this.contextAuth.accessToken = undefined } - // @todo: how are invalid access tokens going to produce an error? how to catch and then regenerate? - // - expired token stored in session when loading the app - // - token expires while using the app - - // ------------------------- - // We already have a context auth object, so reuse it unless - // requested to force create access token. + // requested to force create or have a missing access token. // This can happen if the access token has expired when being // used and it can automatically be re-requested. - if (this.contextAuth && !forceAccessToken) { + if (this.contextAuth && !config.force && this.contextAuth.accessToken) { return this.contextAuth } @@ -68,7 +63,7 @@ export default class VeridaDatabaseAuthType extends AuthType { did, contextName: this.contextName, signature, - deviceId: deviceId + deviceId: config.deviceId }); //console.log('refresh response', refreshResponse.data) @@ -86,6 +81,7 @@ export default class VeridaDatabaseAuthType extends AuthType { refreshToken, accessToken, host, + endpointUri: serverUrl, publicSigningKey: this.signKey } @@ -119,7 +115,7 @@ export default class VeridaDatabaseAuthType extends AuthType { return this.contextAuth! } - public async disconnectDevice(deviceId: string): Promise { + public async disconnectDevice(deviceId: string="Test device"): Promise { const contextAuth = await this.getAuthContext() const did = await this.account.did(); @@ -128,7 +124,7 @@ export default class VeridaDatabaseAuthType extends AuthType { const signature = await this.account.sign(consentMessage) try { - const response = await this.getAxios(this.contextName).post(`${contextAuth.host}auth/invalidateDeviceId`, { + const response = await this.getAxios(this.contextName).post(`${contextAuth.endpointUri}auth/invalidateDeviceId`, { did, contextName: this.contextName, deviceId: deviceId, diff --git a/packages/account-node/src/authcontext.ts b/packages/account-node/src/authcontext.ts new file mode 100644 index 00000000..7a3b8273 --- /dev/null +++ b/packages/account-node/src/authcontext.ts @@ -0,0 +1,44 @@ +import { AccountConfig, AuthContext, AuthTypeConfig, VeridaDatabaseAuthContext } from '@verida/account' +import { NodeAccountConfig } from './interfaces' +import LimitedAccount from './limited' +import { Interfaces } from '@verida/storage-link' +import VeridaDatabaseAuthType from "./authTypes/VeridaDatabase" + +/** + * A NodeJs account that only signs messages for a limited list of contexts. + * + * Used for testing. + */ +export default class AuthContextAccount extends LimitedAccount { + + /** + * This will need to be refactored when more db engines are supported. + * + * We are assuming we are dealing with a Verida Database Auth Context and then injecting + * a known context object into the in memory database. + * + * This is used for testing, by setting invalid access / request tokens in unit tests + * + * @param accountConfig + * @param autoConfig + * @param signingContext + * @param authContext + */ + constructor(accountConfig: AccountConfig, autoConfig: NodeAccountConfig, signingContext: string, authContext: VeridaDatabaseAuthContext) { + const signingContexts = [signingContext] + super(accountConfig, autoConfig, signingContexts) + + this.contextAuths[signingContext] = new VeridaDatabaseAuthType(this, signingContext, { + endpointUri: authContext.endpointUri, + type: 'VeridaDatabase' + }, authContext.publicSigningKey!) + + this.contextAuths[signingContext].setAuthContext(authContext) + + } + + public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig: AuthTypeConfig, authType = "database"): Promise { + return super.getAuthContext(contextName, contextConfig, authConfig, authType) + } + +} \ No newline at end of file diff --git a/packages/account-node/src/auto.ts b/packages/account-node/src/auto.ts index b31a389e..dc7975d7 100644 --- a/packages/account-node/src/auto.ts +++ b/packages/account-node/src/auto.ts @@ -104,10 +104,12 @@ export default class AutoAccount extends Account { return this.didClient } - public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig?: AuthTypeConfig, authType = "database"): Promise { + public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig: VeridaDatabaseAuthTypeConfig = { + force: false + }, authType = "database"): Promise { // Use existing context auth instance if it exists - if (this.contextAuths[contextName]) { - return this.contextAuths[contextName].getAuthContext( authConfig) + if (this.contextAuths[contextName] && !authConfig.force) { + return this.contextAuths[contextName].getAuthContext(authConfig) } const signKey = contextConfig.publicKeys.signKey @@ -117,10 +119,18 @@ export default class AutoAccount extends Account { if (serviceEndpoint.type == "VeridaDatabase") { this.contextAuths[contextName] = new VeridaDatabaseAuthType(this, contextName, serviceEndpoint, signKey) - return this.contextAuths[contextName].getAuthContext( authConfig) + return this.contextAuths[contextName].getAuthContext(authConfig) } throw new Error(`Unknown auth context type (${authType})`) } + public async disconnectDevice(contextName: string, deviceId: string="Test device"): Promise { + if (!this.contextAuths[contextName]) { + throw new Error(`Context not connected ${contextName}`) + } + + return this.contextAuths[contextName].disconnectDevice(deviceId) + } + } \ No newline at end of file diff --git a/packages/account-node/src/index.ts b/packages/account-node/src/index.ts index f6b95892..ed5335ca 100644 --- a/packages/account-node/src/index.ts +++ b/packages/account-node/src/index.ts @@ -1,9 +1,11 @@ import AutoAccount from "./auto" import LimitedAccount from "./limited" +import AuthContextAccount from "./authcontext" import VeridaDatabaseAuthType from "./authTypes/VeridaDatabase" export { AutoAccount, VeridaDatabaseAuthType, - LimitedAccount + LimitedAccount, + AuthContextAccount } \ No newline at end of file diff --git a/packages/account-node/src/limited.ts b/packages/account-node/src/limited.ts index fee8d9d2..40865448 100644 --- a/packages/account-node/src/limited.ts +++ b/packages/account-node/src/limited.ts @@ -13,7 +13,7 @@ export default class LimitedAccount extends AutoAccount { private signingContexts: string[] - constructor(accountConfig: AccountConfig, autoConfig: NodeAccountConfig, signingContexts = []) { + constructor(accountConfig: AccountConfig, autoConfig: NodeAccountConfig, signingContexts: string[] = []) { super(accountConfig, autoConfig) this.signingContexts = signingContexts } diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index 68268ce9..147adef3 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -106,7 +106,13 @@ export default class Account { throw new Error("Not implemented.") } - public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig?: AuthTypeConfig, authType: string = "database"): Promise { + public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig: AuthTypeConfig = { + force: false + }, authType: string = "database"): Promise { + throw new Error("Not implemented.") + } + + public async disconnectDevice(contextName: string, deviceId: string="Test device"): Promise { throw new Error("Not implemented.") } diff --git a/packages/account/src/interfaces.ts b/packages/account/src/interfaces.ts index bea69a77..663aaffd 100644 --- a/packages/account/src/interfaces.ts +++ b/packages/account/src/interfaces.ts @@ -19,6 +19,7 @@ export interface AuthContext { } export interface AuthTypeConfig { + force: boolean } export class AuthType { @@ -39,6 +40,14 @@ export class AuthType { getAuthContext(config: AuthTypeConfig): Promise { throw new Error("Not implemented") } + + setAuthContext(contextAuth: AuthContext) { + this.contextAuth = contextAuth + } + + disconnectDevice(deviceId: string="Test device"): Promise { + throw new Error("Not implemented") + } } //// VeridaDatabase Authentication Interfaces @@ -46,11 +55,12 @@ export class AuthType { export interface VeridaDatabaseAuthContext extends AuthContext { refreshToken?: string, accessToken?: string, + endpointUri: string, host: string } export interface VeridaDatabaseAuthTypeConfig extends AuthTypeConfig { deviceId?: string, endpointUri?: string, - forceAccessToken?: boolean + invalidAccessToken?: boolean } \ No newline at end of file From fb028f76d19291be8f96df7a8b3cad218339ab69 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 28 Aug 2022 14:14:33 +0930 Subject: [PATCH 15/27] Refactor to support authentication retry when access / refresh token expires. --- .../client-ts/src/context/engines/base.ts | 4 + .../engines/verida/database/base-db.ts | 24 ++- .../engines/verida/database/db-encrypted.ts | 38 +++-- .../engines/verida/database/db-public.ts | 26 ++- .../context/engines/verida/database/engine.ts | 29 +++- .../test/verida/database.auth.tests.ts | 149 ++++++++++++++++++ 6 files changed, 244 insertions(+), 26 deletions(-) create mode 100644 packages/client-ts/test/verida/database.auth.tests.ts diff --git a/packages/client-ts/src/context/engines/base.ts b/packages/client-ts/src/context/engines/base.ts index 51305c49..56ccfea0 100644 --- a/packages/client-ts/src/context/engines/base.ts +++ b/packages/client-ts/src/context/engines/base.ts @@ -41,6 +41,10 @@ class BaseStorageEngine { } } + public getDbRegistry() { + return this.dbRegistry + } + public async openDatabase( databaseName: string, config: DatabaseOpenConfig diff --git a/packages/client-ts/src/context/engines/verida/database/base-db.ts b/packages/client-ts/src/context/engines/verida/database/base-db.ts index 02727b3b..04697934 100644 --- a/packages/client-ts/src/context/engines/verida/database/base-db.ts +++ b/packages/client-ts/src/context/engines/verida/database/base-db.ts @@ -11,6 +11,7 @@ import { Context } from "../../../.."; import { DbRegistryEntry } from "../../../db-registry"; import EncryptionUtils from "@verida/encryption-utils"; import { RecordSignature } from "../../../utils" +import StorageEngineVerida from "./engine" /** * @category @@ -22,6 +23,7 @@ class BaseDb extends EventEmitter implements Database { protected dsn: string; protected token?: string; protected storageContext: string; + protected engine: StorageEngineVerida protected permissions?: PermissionsConfig; protected isOwner?: boolean; @@ -37,7 +39,7 @@ class BaseDb extends EventEmitter implements Database { // PouchDb instance for this database protected db?: any; - constructor(config: VeridaDatabaseConfig) { + constructor(config: VeridaDatabaseConfig, engine: StorageEngineVerida) { super(); this.client = config.client; this.databaseName = config.databaseName; @@ -45,6 +47,7 @@ class BaseDb extends EventEmitter implements Database { this.dsn = config.dsn; this.token = config.token; this.storageContext = config.storageContext; + this.engine = engine this.isOwner = config.isOwner; this.signContext = config.signContext; @@ -73,6 +76,10 @@ class BaseDb extends EventEmitter implements Database { this.db = null; } + public getEngine() { + return this.engine + } + // DID + context name + DB Name + readPerm + writePerm private buildDatabaseHash() { let text = [ @@ -420,17 +427,18 @@ class BaseDb extends EventEmitter implements Database { } } - public async info(): Promise { - throw new Error("Not implemented"); + public getAccessToken() { + return this.token } - public async disconnectDevice(deviceId: string="Test device"): Promise { - if (!this.account) { - throw new Error("Unable to disconnect device. No account connected.") - } + public async setAccessToken(token: string): Promise { + this.token = token + } - return await this.account.disconnectDevice(deviceId) + public async info(): Promise { + throw new Error("Not implemented"); } + } export default BaseDb; diff --git a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts index 7a903120..963ee7b0 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts @@ -1,6 +1,7 @@ import { VeridaDatabaseConfig } from "./interfaces"; import BaseDb from "./base-db"; -import DbRegistry, { DbRegistryEntry } from "../../../db-registry"; +import { DbRegistryEntry } from "../../../db-registry"; +import StorageEngineVerida from "./engine" import EncryptionUtils from "@verida/encryption-utils"; import * as PouchDBCryptLib from "pouchdb"; @@ -28,9 +29,7 @@ PouchDBCrypt.plugin(CryptoPouch); class EncryptedDatabase extends BaseDb { protected encryptionKey: Buffer; protected password?: string; - protected token: string - private dbRegistry: DbRegistry; private _sync: any; private _localDbEncrypted: any; private _localDb: any; @@ -48,10 +47,9 @@ class EncryptedDatabase extends BaseDb { * @param {*} permissions */ //constructor(dbHumanName: string, dbName: string, dataserver: any, encryptionKey: string | Buffer, remoteDsn: string, did: string, permissions: PermissionsConfig) { - constructor(config: VeridaDatabaseConfig, dbRegistry: DbRegistry) { - super(config); + constructor(config: VeridaDatabaseConfig, engine: StorageEngineVerida) { + super(config, engine); - this.dbRegistry = dbRegistry; this.encryptionKey = config.encryptionKey!; this.token = config.token!; @@ -85,12 +83,30 @@ class EncryptedDatabase extends BaseDb { // Setting to 1,000 -- Any higher and it takes too long on mobile devices }); - const authToken = this.token + const instance = this this._remoteDbEncrypted = new PouchDB(`${this.dsn}/${this.databaseHash}`, { skip_setup: true, - fetch: function(url: string, opts: any) { - opts.headers.set('Authorization', `Bearer ${authToken}`) - return PouchDB.fetch(url, opts) + fetch: async function(url: string, opts: any) { + opts.headers.set('Authorization', `Bearer ${instance.getAccessToken()}`) + const result = await PouchDB.fetch(url, opts) + if (result.status == 401) { + // Unauthorized, most likely due to an invalid access token + // Fetch new credentials and try again + await instance.getEngine().reAuth(instance) + + opts.headers.set('Authorization', `Bearer ${instance.getAccessToken()}`) + const result = await PouchDB.fetch(url, opts) + + if (result.status == 401) { + throw new Error(`Permission denied to access server: ${this.dsn}`) + } + + // Return an authorized result + return result + } + + // Return an authorized result + return result } }); @@ -270,7 +286,7 @@ class EncryptedDatabase extends BaseDb { this.client.updateDatabase(this.did, this.databaseName, options); if (this.config.saveDatabase !== false) { - await this.dbRegistry.saveDb(this); + await this.engine.getDbRegistry().saveDb(this); } } catch (err: any) { throw new Error("User doesn't exist or unable to create user database"); diff --git a/packages/client-ts/src/context/engines/verida/database/db-public.ts b/packages/client-ts/src/context/engines/verida/database/db-public.ts index 62ab7724..86ab12d8 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-public.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-public.ts @@ -28,10 +28,28 @@ class PublicDatabase extends BaseDb { } if (this.token) { - const authToken = this.token - dbConfig['fetch'] = function(url: string, opts: any) { - opts.headers.set('Authorization', `Bearer ${authToken}`) - return PouchDB.fetch(url, opts) + const instance = this + dbConfig['fetch'] = async function(url: string, opts: any) { + opts.headers.set('Authorization', `Bearer ${instance.getAccessToken()}`) + const result = await PouchDB.fetch(url, opts) + if (result.status == 401) { + // Unauthorized, most likely due to an invalid access token + // Fetch new credentials and try again + await instance.getEngine().reAuth(instance) + + opts.headers.set('Authorization', `Bearer ${instance.getAccessToken()}`) + const result = await PouchDB.fetch(url, opts) + + if (result.status == 401) { + throw new Error(`Permission denied to access server: ${this.dsn}`) + } + + // Return an authorized result + return result + } + + // Return an authorized result + return result } } diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index 186bfc81..050eb48c 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -8,6 +8,7 @@ import PublicDatabase from "./db-public"; import DbRegistry from "../../../db-registry"; import { Interfaces } from "@verida/storage-link"; import { VeridaDatabaseAuthContext } from "@verida/account" +import BaseDb from "./base-db"; const _ = require("lodash"); @@ -109,6 +110,8 @@ class StorageEngineVerida extends BaseStorageEngine { options ); + const instance = this + const contextName = config.contextName ? config.contextName : this.storageContext // Default to user's account did if not specified @@ -202,7 +205,7 @@ class StorageEngineVerida extends BaseStorageEngine { isOwner: config.isOwner, saveDatabase: config.saveDatabase, }, - this.dbRegistry + this ); await db.init(); @@ -231,7 +234,7 @@ class StorageEngineVerida extends BaseStorageEngine { client: this.client, isOwner: config.isOwner, saveDatabase: config.saveDatabase, - }); + }, this); await db.init(); return db; @@ -290,7 +293,7 @@ class StorageEngineVerida extends BaseStorageEngine { isOwner: config.isOwner, saveDatabase: config.saveDatabase, }, - this.dbRegistry + this ); try { @@ -320,6 +323,26 @@ class StorageEngineVerida extends BaseStorageEngine { }*/ } + + /** + * Re-authenticate this storage engine and update the credentials + * for the database. + */ + public async reAuth(db: BaseDb) { + if (!this.account) { + // No account connected, so can't reconnect database + const info = await db.info() + throw new Error(`No account connected. Access token expired, but unable to regenerate for database ${info.databaseName}`) + } + + const auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { + invalidAccessToken: true + }) + this.auth = auth + await this.client.setAuthContext(this.auth) + await db.setAccessToken(this.auth.accessToken) + } + public logout() { super.logout(); this.client = new DatastoreServerClient( diff --git a/packages/client-ts/test/verida/database.auth.tests.ts b/packages/client-ts/test/verida/database.auth.tests.ts new file mode 100644 index 00000000..2ceb70a5 --- /dev/null +++ b/packages/client-ts/test/verida/database.auth.tests.ts @@ -0,0 +1,149 @@ +'use strict' +const assert = require('assert') + +import { Client } from '../../src/index' +import { AutoAccount, AuthContextAccount } from '@verida/account-node' +import CONFIG from '../config' + +const DB_NAME_OWNER = 'OwnerTestDb_1' +const DB_NAME_OWNER_2 = 'OwnerTestDb_2' + +const VALID_CONTEXT = 'Verida Testing: Authentication' +const INVALID_CONTEXT = 'Verida Testing: Authentication - Invalid' + + +/** + * + * + */ +describe('Verida auth tests', () => { + let context, did + let invalidContext, invalidDid + + const account = new AutoAccount(CONFIG.DEFAULT_ENDPOINTS, { + privateKey: CONFIG.VDA_PRIVATE_KEY, + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + + const network = new Client({ + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + + const network2 = new Client({ + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + + const invalidAccount = new AutoAccount(CONFIG.INVALID_ENDPOINTS, { + privateKey: CONFIG.VDA_PRIVATE_KEY, + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + + const invalidNetwork = new Client({ + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + + describe('Handle authentication errors', function() { + this.timeout(200000) + + // Handle errors where the storage node API is down, so unable to authenticate + it('can handle authentication connection error', async function() { + invalidDid = await invalidAccount.did() + await invalidNetwork.connect(invalidAccount) + invalidContext = await invalidNetwork.openContext(INVALID_CONTEXT, true) + + const promise = new Promise((resolve, rejects) => { + invalidContext.openDatabase(DB_NAME_OWNER).then(rejects, resolve) + }) + const result = await promise + + const expectedMessage = `Unable to connect to storage node (http://localhost:6000/): connect ECONNREFUSED 127.0.0.1:6000` + assert.deepEqual(result, new Error(expectedMessage)) + }) + }) + + describe('Handle token expiry', function() { + this.timeout(200000) + + it('can handle accessToken expiry for an encrypted database', async function() { + // Create a working connection + did = await account.did() + await network.connect(account) + context = await network.openContext(VALID_CONTEXT, true) + + // 1/ get the context auth + const authContext = await context.getAuthContext() + + // Manually set to an invalid access token so the refreshToken is needed to re-authenticate + authContext.accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVIUTlLTFprbE9UVERjaU1TOXFOY1Nzb0hBUUJSbUxDbEQ3d2pDUnBsSXNSUE45emZMdHBSM0JlM3d0cXRVVHlFakVPcVhpS2NZeG5zZHBWWTlWRWE3TmUwNUtsUElPUTQxSlpJQktHWW1NYjE2ajRkWkZxVThlek1VZFR1SnRFYmNKUlY0M09JWXE4OTF0ZXY0TDhKY2xPdzBZV1BweTFoMmpKTHkzblNJNVZsZ3RaVG1HVmI5cXlDODRrVnd1NGsySzVvN1VuOFkyUmNWbGpEa2lZRXpaanBxbzlkc0l4Mkh0UGNQcE9TVDhVM05WU29TWjFibzNBdU5CRjkxbE4iLCJkaWQiOiJkaWQ6dmRhOjB4ZTcwZTYyODQyNzg4MGFiMDFiZTU1YTM0OWYxY2VmMDQ3OWIyMWJmOCIsInN1YiI6InZmNWYxOWYyYzcyYTFmNDllYjcwMDIxNWMwNGVhMGM4NjVmMjc4ZTA4MGNkMDU5Njc4NjVhMmE5MTA0YWUzNjUzIiwiY29udGV4dE5hbWUiOiJWZXJpZGEgVGVzdGluZzogQXV0aGVudGljYXRpb24iLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjYxNDE5ODQ2LCJleHAiOjE2NjE0MTk5MDZ9.tHugqMHM_udE-eV0u2_2oCjzDSiljfK8DVieK-GcXcU' + + // 2/ initialise a new testing account that has a single context with no access token + const account2 = new AuthContextAccount(CONFIG.DEFAULT_ENDPOINTS, { + privateKey: CONFIG.VDA_PRIVATE_KEY, + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }, + VALID_CONTEXT, + authContext) + + // 3/ initiate a new context using the context auth + await network2.connect(account2) + const context2 = await network2.openContext(VALID_CONTEXT) + + // 4/ Create a new context which will use the refresh token to create a new access token + const db2 = await context2?.openDatabase(DB_NAME_OWNER, { + saveDatabase: false + }) + + assert.ok(db2, 'Have successfully opened a database') + + const rows = await db2!.getMany() + assert.ok(rows && rows.length, 'Have valid results returned') + }) + + // todo: test with public database, not encrypted database + + /*it('can handle refreshToken expiry', async function() { + // Create a working connection + did = await account.did() + await network.connect(account) + context = await network.openContext(VALID_CONTEXT, true) + const db = await context.openDatabase(DB_NAME_OWNER) + + // 1/ get the context auth + const authContext = await context.getAuthContext() + console.log(authContext) + + // Manually invalidate the access token so the refreshToken is needed to re-authenticate + authContext.accessToken = '' + + // 2/ initialise a new testing account that has no ability to fetch auth contexts + // and just uses the context specified in the constructor + const account2 = new AuthContextAccount(CONFIG.DEFAULT_ENDPOINTS, { + privateKey: CONFIG.VDA_PRIVATE_KEY, + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }, + [VALID_CONTEXT], + authContext) + + // 3/ initiate a new context using the context auth + await network2.connect(account2) + const context2 = await network2.openContext(VALID_CONTEXT) + console.log('got new context') + + // 4/ Manually invalidate the refresh token on the storage node + const disconnect = await account.disconnectDevice(VALID_CONTEXT) + assert.ok(disconnect, 'Device disconnected') + + // 5/ Create a new context which will attempt to use the refresh token + // which has been deleted, which should fail + const db2 = await context2?.openDatabase(DB_NAME_OWNER) + await db2?.getDb() + })*/ + }) +}) \ No newline at end of file From 6f30f399c7111c8d5b2a6e09ef7184d067be08d5 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 29 Aug 2022 07:25:33 +0930 Subject: [PATCH 16/27] Support getAuthContext in vault-account (untested) --- packages/account-web-vault/src/vault-account.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index 8b2e7592..bce7472c 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -215,4 +215,16 @@ export default class VaultAccount extends Account { store.remove(VERIDA_AUTH_CONTEXT) } + public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig: AuthTypeConfig = { + force: false + }, authType: string = "database"): Promise { + if (this.contextCache[contextName]) { + return this.contextCache[contextName].contextAuth + } + + // @todo: Do we only do this with force = true? + // force intent was "force to get a new accessToken", not "force if no context" + await this.connectContext(contextName) + } + } \ No newline at end of file From cb94b2a0622524443c4f9e58a07e01d8dda7494f Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 29 Aug 2022 13:32:10 +0930 Subject: [PATCH 17/27] Fix typescript issues. --- packages/client-ts/src/context/datastore.ts | 1 - .../src/context/engines/verida/database/client.ts | 6 +++--- .../src/context/engines/verida/database/db-encrypted.ts | 5 ++--- .../src/context/engines/verida/database/engine.ts | 8 ++++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/client-ts/src/context/datastore.ts b/packages/client-ts/src/context/datastore.ts index bd8936c7..8b033496 100644 --- a/packages/client-ts/src/context/datastore.ts +++ b/packages/client-ts/src/context/datastore.ts @@ -2,7 +2,6 @@ const _ = require("lodash"); import { DatabaseOpenConfig, DatastoreOpenConfig } from "./interfaces"; import Context from "./context"; import Schema from "./schema"; -import { DIDDocument } from "@verida/did-document"; /** * A datastore wrapper around a given database and schema. diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index 2ddc14bf..1fd6033e 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -18,7 +18,7 @@ export class DatastoreServerClient { private authContext?: VeridaDatabaseAuthContext private storageContext: string; - private serviceEndpoint: Account; + private serviceEndpoint: string; constructor(storageContext: string, serviceEndpoint: string, authContext?: VeridaDatabaseAuthContext) { this.authContext = authContext @@ -39,7 +39,7 @@ export class DatastoreServerClient { databaseName: string, config: any = {} ) { - return this.getAxios(this.authContext.accessToken).post(this.serviceEndpoint + "user/createDatabase", { + return this.getAxios(this.authContext!.accessToken).post(this.serviceEndpoint + "user/createDatabase", { did: did, databaseName: databaseName, options: config, @@ -51,7 +51,7 @@ export class DatastoreServerClient { databaseName: string, config: any = {} ) { - return this.getAxios(this.authContext.accessToken).post(this.serviceEndpoint + "user/updateDatabase", { + return this.getAxios(this.authContext!.accessToken).post(this.serviceEndpoint + "user/updateDatabase", { did: did, databaseName: databaseName, options: config, diff --git a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts index 963ee7b0..7ab19d63 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts @@ -20,8 +20,6 @@ PouchDBCrypt.plugin(PouchDBFind); PouchDB.plugin(PouchDBFind); PouchDBCrypt.plugin(CryptoPouch); -//db = new EncryptedDatabase(databaseName, did, this.dsn!, encryptionKey, config.permissions) - /** * @category * Modules @@ -46,7 +44,6 @@ class EncryptedDatabase extends BaseDb { * @param {*} did * @param {*} permissions */ - //constructor(dbHumanName: string, dbName: string, dataserver: any, encryptionKey: string | Buffer, remoteDsn: string, did: string, permissions: PermissionsConfig) { constructor(config: VeridaDatabaseConfig, engine: StorageEngineVerida) { super(config, engine); @@ -83,6 +80,7 @@ class EncryptedDatabase extends BaseDb { // Setting to 1,000 -- Any higher and it takes too long on mobile devices }); + /* @ts-ignore */ const instance = this this._remoteDbEncrypted = new PouchDB(`${this.dsn}/${this.databaseHash}`, { skip_setup: true, @@ -133,6 +131,7 @@ class EncryptedDatabase extends BaseDb { const databaseName = this.databaseName; const dsn = this.dsn; + /* @ts-ignore */ const instance = this; // Do a once off sync to ensure the local database pulls all data from remote server diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index 050eb48c..4a947ba6 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -7,7 +7,7 @@ import { Account } from "@verida/account"; import PublicDatabase from "./db-public"; import DbRegistry from "../../../db-registry"; import { Interfaces } from "@verida/storage-link"; -import { VeridaDatabaseAuthContext } from "@verida/account" +import { VeridaDatabaseAuthContext, VeridaDatabaseAuthTypeConfig } from "@verida/account" import BaseDb from "./base-db"; const _ = require("lodash"); @@ -74,7 +74,7 @@ class StorageEngineVerida extends BaseStorageEngine { throw new Error('Unable to connect to external storage node. No account connected.') } - const auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { + const auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { endpointUri }) @@ -335,12 +335,12 @@ class StorageEngineVerida extends BaseStorageEngine { throw new Error(`No account connected. Access token expired, but unable to regenerate for database ${info.databaseName}`) } - const auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { + const auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { invalidAccessToken: true }) this.auth = auth await this.client.setAuthContext(this.auth) - await db.setAccessToken(this.auth.accessToken) + await db.setAccessToken(this.auth!.accessToken!) } public logout() { From 92010a797819378309fd8408ef21d0e9428c1291 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 29 Aug 2022 16:13:00 +0930 Subject: [PATCH 18/27] Cache opened databases to avoid opening multiple and creating lots of open sockets. --- packages/client-ts/src/context/context.ts | 36 +++++++++++++------ packages/client-ts/src/context/interfaces.ts | 5 +++ .../client-ts/test/storage.context.tests.ts | 22 ++++++++++++ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/client-ts/src/context/context.ts b/packages/client-ts/src/context/context.ts index 1d4e3be6..40da6d22 100644 --- a/packages/client-ts/src/context/context.ts +++ b/packages/client-ts/src/context/context.ts @@ -58,6 +58,8 @@ class Context { private databaseEngines: DatabaseEngines = {}; private dbRegistry: DbRegistry; + private databaseCache: Record> = {} + /** * Instantiate a new context. * @@ -291,21 +293,33 @@ class Context { config.did = accountDid; } - const databaseEngine = await this.getDatabaseEngine( - config.did, - config.createContext! - ); + const cacheKey = `${config.did}/${databaseName}` - if (!config.signingContext) { - config.signingContext = this; + if (this.databaseCache[cacheKey] && !config.ignoreCache) { + return this.databaseCache[cacheKey] } - const database = await databaseEngine.openDatabase(databaseName, config); - if (config.saveDatabase !== false) { - await this.dbRegistry.saveDb(database, false); - } + const instance = this + this.databaseCache[cacheKey] = new Promise(async (resolve, rejects) => { + const databaseEngine = await instance.getDatabaseEngine( + config.did!, + config.createContext! + ); + + if (!config.signingContext) { + config.signingContext = instance; + } + + const database = await databaseEngine.openDatabase(databaseName, config); + if (config.saveDatabase !== false) { + await instance.dbRegistry.saveDb(database, false); + } + + instance.databaseCache[cacheKey] = database; + resolve(database); + }) - return database; + return this.databaseCache[cacheKey] } /** diff --git a/packages/client-ts/src/context/interfaces.ts b/packages/client-ts/src/context/interfaces.ts index ab24665f..f02261d4 100644 --- a/packages/client-ts/src/context/interfaces.ts +++ b/packages/client-ts/src/context/interfaces.ts @@ -64,6 +64,11 @@ export interface DatabaseOpenConfig { * Optionally specify the context used to sign data */ signingContext?: Context; + + /** + * Ignore any cached instance already created + */ + ignoreCache?: boolean } // @todo: Same as DatabaseOpenConfig diff --git a/packages/client-ts/test/storage.context.tests.ts b/packages/client-ts/test/storage.context.tests.ts index 9b70ff0f..8fec6d19 100644 --- a/packages/client-ts/test/storage.context.tests.ts +++ b/packages/client-ts/test/storage.context.tests.ts @@ -8,6 +8,8 @@ import { DIDDocument } from '@verida/did-document' import CONFIG from './config' const TEST_DB_NAME = 'TestDb_1' +const TEST_DB_NAME_2 = 'TestDb_2' +const TEST_DB_NAME_3 = 'TestDb_3' /** * @@ -57,6 +59,26 @@ describe('Storage context tests', () => { assert.ok(data.length && data.length > 0, 'Array returned with at least one row') assert.ok(data[0].hello == 'world', 'First result has expected value') }) + + it('can open same database at once and both return the same cache entry', async () => { + const database1 = context.openDatabase(TEST_DB_NAME_2) + const database2 = context.openDatabase(TEST_DB_NAME_2) + + await Promise.all([database1, database2]).then(([db1, db2]) => { + assert.ok(db1 === db2, 'Returned databases are the same') + }) + }) + + it('can respect ignore cache when opening a database', async () => { + const database1 = context.openDatabase(TEST_DB_NAME_3) + const database2 = context.openDatabase(TEST_DB_NAME_3, { + ignoreCache: true + }) + + await Promise.all([database1, database2]).then(([db1, db2]) => { + assert.ok(db1 !== db2, 'Returned databases are not the same') + }) + }) }) }) \ No newline at end of file From a41203e582955d7c307ef7f72f053b76212b06c0 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 30 Aug 2022 19:18:39 +0930 Subject: [PATCH 19/27] Support account-web-vault handling invalid access token or refresh token. --- .../account-web-vault/src/vault-account.ts | 72 ++++++++++++++++--- packages/account/src/index.ts | 3 +- packages/account/src/interfaces.ts | 7 ++ packages/client-ts/src/context/context.ts | 2 +- packages/client-ts/src/context/database.ts | 1 + .../engines/verida/database/base-db.ts | 4 ++ .../engines/verida/database/db-encrypted.ts | 5 +- .../context/engines/verida/database/engine.ts | 23 +++++- 8 files changed, 99 insertions(+), 18 deletions(-) diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index bce7472c..b38d3dc2 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -1,7 +1,9 @@ -import { Account } from '@verida/account' +import { Account, VeridaDatabaseAuthContext, AuthTypeConfig, AuthContext, VeridaDatabaseAuthTypeConfig, ContextAuthorizationError } from '@verida/account' import { Interfaces } from '@verida/storage-link' import { Keyring } from '@verida/keyring' import VaultModalLogin from './vault-modal-login' +import Axios from "axios"; + const querystring = require('querystring') const _ = require('lodash') const store = require('store') @@ -71,12 +73,14 @@ export default class VaultAccount extends Account { this.config = config } - public async connectContext(contextName: string) { + public async connectContext(contextName: string, ignoreSession: boolean = false) { const vaultAccount = this - const contextConfig = await this.loadFromSession(contextName) - if (contextConfig) { - return contextConfig + if (!ignoreSession) { + const contextConfig = await this.loadFromSession(contextName) + if (contextConfig) { + return contextConfig + } } const promise = new Promise((resolve, reject) => { @@ -113,7 +117,7 @@ export default class VaultAccount extends Account { // First, attempt to Load from query parameters if specified const token = getAuthTokenFromQueryParams() if (token && token.context == contextName) { - this.addContext(token.context, token.contextConfig, new Keyring(token.signature)) + this.addContext(token.context, token.contextConfig, new Keyring(token.signature), token.contextAuth) this.setDid(token.did) if (typeof(this.config!.callback) === "function") { @@ -158,7 +162,7 @@ export default class VaultAccount extends Account { return this.contextCache[contextName].keyring } - public addContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, keyring: Keyring, contextAuth: any) { + public addContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, keyring: Keyring, contextAuth: VeridaDatabaseAuthContext) { this.contextCache[contextName] = { keyring, contextConfig, @@ -218,13 +222,59 @@ export default class VaultAccount extends Account { public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig: AuthTypeConfig = { force: false }, authType: string = "database"): Promise { - if (this.contextCache[contextName]) { + if (authConfig.force || !this.contextCache[contextName]) { + // Don't have an existing context in the cache or we need to force refresh + await this.connectContext(contextName, true) + } + + const serviceEndpoint = contextConfig.services.databaseServer + if (serviceEndpoint.type == "VeridaDatabase") { + // If we have an invalid access token (detected by the internal libraries) + // then attempt to re-authenticate using the refreshToken + if (( authConfig).invalidAccessToken) { + const did = await this.did() + + try { + const accessResponse = await this.getAxios(contextName).post(serviceEndpoint.endpointUri + "auth/connect",{ + refreshToken: this.contextCache[contextName].contextAuth.refreshToken, + did, + contextName: contextName + }); + + const accessToken = accessResponse.data.accessToken + this.contextCache[contextName].contextAuth.accessToken = accessToken + return this.contextCache[contextName].contextAuth + } catch (err: any) { + // Refresh token is invalid, so raise an exception that will be caught within the protocol + // and force the sign in to be restarted + if (err.message == 'Request failed with status code 400') { + throw new ContextAuthorizationError("Expired refresh token") + } else { + throw err + } + } + } + } + + if (this.contextCache[contextName] && this.contextCache[contextName].contextAuth) { return this.contextCache[contextName].contextAuth } - // @todo: Do we only do this with force = true? - // force intent was "force to get a new accessToken", not "force if no context" - await this.connectContext(contextName) + throw new Error(`Unknown auth context type (${authType})`) } + private getAxios(storageContext: string, accessToken?: string) { + let config: any = { + headers: { + // @todo: Application-Name needs to become Storage-Context + "Application-Name": storageContext, + }, + }; + + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}` + } + + return Axios.create(config); + } } \ No newline at end of file diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index 028b61c6..801af6ad 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -1,5 +1,5 @@ import Account from "./account" -import { AccountConfig, EnvironmentType, AuthContext, AuthType, AuthTypeConfig, VeridaDatabaseAuthContext, VeridaDatabaseAuthTypeConfig } from "./interfaces" +import { AccountConfig, EnvironmentType, AuthContext, AuthType, AuthTypeConfig, VeridaDatabaseAuthContext, VeridaDatabaseAuthTypeConfig, ContextAuthorizationError } from "./interfaces" import Config from "./config" export { @@ -11,5 +11,6 @@ export { AuthTypeConfig, VeridaDatabaseAuthContext, VeridaDatabaseAuthTypeConfig, + ContextAuthorizationError, Config } \ No newline at end of file diff --git a/packages/account/src/interfaces.ts b/packages/account/src/interfaces.ts index 663aaffd..79223990 100644 --- a/packages/account/src/interfaces.ts +++ b/packages/account/src/interfaces.ts @@ -63,4 +63,11 @@ export interface VeridaDatabaseAuthTypeConfig extends AuthTypeConfig { deviceId?: string, endpointUri?: string, invalidAccessToken?: boolean +} + +export class ContextAuthorizationError extends Error { + constructor(message: string) { + super(message) + this.name = "ContextAuthorizationError" + } } \ No newline at end of file diff --git a/packages/client-ts/src/context/context.ts b/packages/client-ts/src/context/context.ts index 40da6d22..7bc32f2a 100644 --- a/packages/client-ts/src/context/context.ts +++ b/packages/client-ts/src/context/context.ts @@ -1,4 +1,4 @@ -import { Account, AuthContext, AuthType, AuthTypeConfig } from "@verida/account"; +import { Account, AuthContext, AuthTypeConfig } from "@verida/account"; import { Interfaces } from "@verida/storage-link"; import BaseStorageEngine from "./engines/base"; diff --git a/packages/client-ts/src/context/database.ts b/packages/client-ts/src/context/database.ts index 430ec81c..25301d0d 100644 --- a/packages/client-ts/src/context/database.ts +++ b/packages/client-ts/src/context/database.ts @@ -16,5 +16,6 @@ import { DbRegistryEntry } from "./db-registry"; init(): Promise info(): Promise registryEntry(): Promise + close(): Promise } \ No newline at end of file diff --git a/packages/client-ts/src/context/engines/verida/database/base-db.ts b/packages/client-ts/src/context/engines/verida/database/base-db.ts index 04697934..e249ab31 100644 --- a/packages/client-ts/src/context/engines/verida/database/base-db.ts +++ b/packages/client-ts/src/context/engines/verida/database/base-db.ts @@ -438,6 +438,10 @@ class BaseDb extends EventEmitter implements Database { public async info(): Promise { throw new Error("Not implemented"); } + + public async close(): Promise { + throw new Error("Not implemented"); + } } diff --git a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts index 7ab19d63..3fcf9b79 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts @@ -91,12 +91,13 @@ class EncryptedDatabase extends BaseDb { // Unauthorized, most likely due to an invalid access token // Fetch new credentials and try again await instance.getEngine().reAuth(instance) - opts.headers.set('Authorization', `Bearer ${instance.getAccessToken()}`) const result = await PouchDB.fetch(url, opts) if (result.status == 401) { - throw new Error(`Permission denied to access server: ${this.dsn}`) + // Failed again, so the refresh token is likely also invalid an wasn't + // able to be re-authenticated + throw new Error(`Permission denied to access server: ${instance.dsn}`) } // Return an authorized result diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index 4a947ba6..daec2ed6 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -327,6 +327,8 @@ class StorageEngineVerida extends BaseStorageEngine { /** * Re-authenticate this storage engine and update the credentials * for the database. + * + * This is called by the internal fetch() methods when they detect an invalid access token */ public async reAuth(db: BaseDb) { if (!this.account) { @@ -335,9 +337,24 @@ class StorageEngineVerida extends BaseStorageEngine { throw new Error(`No account connected. Access token expired, but unable to regenerate for database ${info.databaseName}`) } - const auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { - invalidAccessToken: true - }) + let auth + try { + // Attempt to re-authenticate using the refresh token and ignoring the access token (its invalid) + auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { + invalidAccessToken: true + }) + + } catch (err: any) { + if (err.name == 'ContextAuthorizationError') { + // The refresh token is invalid + // Force a new connection, this will cause a new single sign in popup if in a web environment + // and using account-web-vault + auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { + force: true + }) + } + } + this.auth = auth await this.client.setAuthContext(this.auth) await db.setAccessToken(this.auth!.accessToken!) From 4347dff620a8cf822f11230244ec9589d94d35a7 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 30 Aug 2022 19:58:40 +0930 Subject: [PATCH 20/27] Fix error handling with promises --- .../src/authTypes/VeridaDatabase.ts | 4 +-- packages/client-ts/src/context/context.ts | 34 +++++++++++-------- .../test/verida/database.auth.tests.ts | 21 ++++++++---- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/account-node/src/authTypes/VeridaDatabase.ts b/packages/account-node/src/authTypes/VeridaDatabase.ts index 7c19f89b..f92184f2 100644 --- a/packages/account-node/src/authTypes/VeridaDatabase.ts +++ b/packages/account-node/src/authTypes/VeridaDatabase.ts @@ -1,7 +1,7 @@ import Axios from "axios"; import AutoAccount from "../auto"; import { Interfaces } from '@verida/storage-link' -import { Account, VeridaDatabaseAuthContext, AuthType, AuthTypeConfig, VeridaDatabaseAuthTypeConfig } from "@verida/account"; +import { Account, VeridaDatabaseAuthContext, AuthType, VeridaDatabaseAuthTypeConfig, ContextAuthorizationError } from "@verida/account"; export default class VeridaDatabaseAuthType extends AuthType { @@ -68,7 +68,7 @@ export default class VeridaDatabaseAuthType extends AuthType { //console.log('refresh response', refreshResponse.data) } catch (err: any) { - throw new Error(`Unable to authenticate with storage node (${serverUrl}): ${err.message}`) + throw new ContextAuthorizationError("Expired refresh token") } //console.log("authenticate response", refreshResponse.data) diff --git a/packages/client-ts/src/context/context.ts b/packages/client-ts/src/context/context.ts index 7bc32f2a..42bafb03 100644 --- a/packages/client-ts/src/context/context.ts +++ b/packages/client-ts/src/context/context.ts @@ -301,22 +301,26 @@ class Context { const instance = this this.databaseCache[cacheKey] = new Promise(async (resolve, rejects) => { - const databaseEngine = await instance.getDatabaseEngine( - config.did!, - config.createContext! - ); - - if (!config.signingContext) { - config.signingContext = instance; - } - - const database = await databaseEngine.openDatabase(databaseName, config); - if (config.saveDatabase !== false) { - await instance.dbRegistry.saveDb(database, false); + try { + const databaseEngine = await instance.getDatabaseEngine( + config.did!, + config.createContext! + ); + + if (!config.signingContext) { + config.signingContext = instance; + } + + const database = await databaseEngine.openDatabase(databaseName, config); + if (config.saveDatabase !== false) { + await instance.dbRegistry.saveDb(database, false); + } + + instance.databaseCache[cacheKey] = database; + resolve(database); + } catch (err: any) { + rejects(err) } - - instance.databaseCache[cacheKey] = database; - resolve(database); }) return this.databaseCache[cacheKey] diff --git a/packages/client-ts/test/verida/database.auth.tests.ts b/packages/client-ts/test/verida/database.auth.tests.ts index 2ceb70a5..3b765a26 100644 --- a/packages/client-ts/test/verida/database.auth.tests.ts +++ b/packages/client-ts/test/verida/database.auth.tests.ts @@ -48,7 +48,7 @@ describe('Verida auth tests', () => { }) describe('Handle authentication errors', function() { - this.timeout(200000) + this.timeout(10000) // Handle errors where the storage node API is down, so unable to authenticate it('can handle authentication connection error', async function() { @@ -56,18 +56,23 @@ describe('Verida auth tests', () => { await invalidNetwork.connect(invalidAccount) invalidContext = await invalidNetwork.openContext(INVALID_CONTEXT, true) - const promise = new Promise((resolve, rejects) => { - invalidContext.openDatabase(DB_NAME_OWNER).then(rejects, resolve) + const promise = new Promise(async (resolve, rejects) => { + try { + await invalidContext.openDatabase(DB_NAME_OWNER) + } catch (err) { + // Expect a connection error + resolve(err) + } }) - const result = await promise + const result = await promise const expectedMessage = `Unable to connect to storage node (http://localhost:6000/): connect ECONNREFUSED 127.0.0.1:6000` assert.deepEqual(result, new Error(expectedMessage)) }) }) describe('Handle token expiry', function() { - this.timeout(200000) + this.timeout(2000) it('can handle accessToken expiry for an encrypted database', async function() { // Create a working connection @@ -105,7 +110,11 @@ describe('Verida auth tests', () => { assert.ok(rows && rows.length, 'Have valid results returned') }) - // todo: test with public database, not encrypted database + // @todo: test with public database, not encrypted database + + // @todo: test with refreshtoken expired + + // @todo: implement and test with account-web-vault (how?) /*it('can handle refreshToken expiry', async function() { // Create a working connection From 32a932b63dfa8f73384fa8346054293a17192d3e Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 30 Aug 2022 20:15:23 +0930 Subject: [PATCH 21/27] Fix refresh token expiry with account-node --- .../src/authTypes/VeridaDatabase.ts | 31 ++++--- .../context/engines/verida/database/engine.ts | 3 +- .../test/verida/database.auth.tests.ts | 80 ++++++++++--------- 3 files changed, 61 insertions(+), 53 deletions(-) diff --git a/packages/account-node/src/authTypes/VeridaDatabase.ts b/packages/account-node/src/authTypes/VeridaDatabase.ts index f92184f2..b4bbd2d9 100644 --- a/packages/account-node/src/authTypes/VeridaDatabase.ts +++ b/packages/account-node/src/authTypes/VeridaDatabase.ts @@ -94,20 +94,25 @@ export default class VeridaDatabaseAuthType extends AuthType { if (this.contextAuth && !this.contextAuth.accessToken) { //console.log('getContextAuth(): no access token, but refresh token, so generating access token') - const accessResponse = await this.getAxios(this.contextName).post(serverUrl + "auth/connect",{ - refreshToken: this.contextAuth.refreshToken, - did, - contextName: this.contextName - }); - - // @todo: handle connect error - // @todo: handle connect error (accessResponse.data.status != 'success') - - //console.log("connect response", accessResponse.data) + try { + const accessResponse = await this.getAxios(this.contextName).post(serverUrl + "auth/connect",{ + refreshToken: this.contextAuth.refreshToken, + did, + contextName: this.contextName + }); - const accessToken = accessResponse.data.accessToken - this.contextAuth.accessToken = accessToken - return this.contextAuth + const accessToken = accessResponse.data.accessToken + this.contextAuth.accessToken = accessToken + return this.contextAuth + } catch (err: any) { + // Refresh token is invalid, so raise an exception that will be caught within the protocol + // and force the sign in to be restarted + if (err.message == 'Request failed with status code 400') { + throw new ContextAuthorizationError("Expired refresh token") + } else { + throw err + } + } } // @todo: test if connection is valid? diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index daec2ed6..22462fa9 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -343,7 +343,6 @@ class StorageEngineVerida extends BaseStorageEngine { auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { invalidAccessToken: true }) - } catch (err: any) { if (err.name == 'ContextAuthorizationError') { // The refresh token is invalid @@ -352,6 +351,8 @@ class StorageEngineVerida extends BaseStorageEngine { auth = await this.account!.getAuthContext(this.storageContext, this.contextConfig, { force: true }) + } else { + throw err } } diff --git a/packages/client-ts/test/verida/database.auth.tests.ts b/packages/client-ts/test/verida/database.auth.tests.ts index 3b765a26..96457637 100644 --- a/packages/client-ts/test/verida/database.auth.tests.ts +++ b/packages/client-ts/test/verida/database.auth.tests.ts @@ -17,7 +17,6 @@ const INVALID_CONTEXT = 'Verida Testing: Authentication - Invalid' * */ describe('Verida auth tests', () => { - let context, did let invalidContext, invalidDid const account = new AutoAccount(CONFIG.DEFAULT_ENDPOINTS, { @@ -26,16 +25,6 @@ describe('Verida auth tests', () => { environment: CONFIG.ENVIRONMENT }) - const network = new Client({ - didServerUrl: CONFIG.DID_SERVER_URL, - environment: CONFIG.ENVIRONMENT - }) - - const network2 = new Client({ - didServerUrl: CONFIG.DID_SERVER_URL, - environment: CONFIG.ENVIRONMENT - }) - const invalidAccount = new AutoAccount(CONFIG.INVALID_ENDPOINTS, { privateKey: CONFIG.VDA_PRIVATE_KEY, didServerUrl: CONFIG.DID_SERVER_URL, @@ -76,9 +65,18 @@ describe('Verida auth tests', () => { it('can handle accessToken expiry for an encrypted database', async function() { // Create a working connection - did = await account.did() + const network = new Client({ + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + + const network2 = new Client({ + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + await network.connect(account) - context = await network.openContext(VALID_CONTEXT, true) + const context = await network.openContext(VALID_CONTEXT, true) // 1/ get the context auth const authContext = await context.getAuthContext() @@ -86,7 +84,7 @@ describe('Verida auth tests', () => { // Manually set to an invalid access token so the refreshToken is needed to re-authenticate authContext.accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVIUTlLTFprbE9UVERjaU1TOXFOY1Nzb0hBUUJSbUxDbEQ3d2pDUnBsSXNSUE45emZMdHBSM0JlM3d0cXRVVHlFakVPcVhpS2NZeG5zZHBWWTlWRWE3TmUwNUtsUElPUTQxSlpJQktHWW1NYjE2ajRkWkZxVThlek1VZFR1SnRFYmNKUlY0M09JWXE4OTF0ZXY0TDhKY2xPdzBZV1BweTFoMmpKTHkzblNJNVZsZ3RaVG1HVmI5cXlDODRrVnd1NGsySzVvN1VuOFkyUmNWbGpEa2lZRXpaanBxbzlkc0l4Mkh0UGNQcE9TVDhVM05WU29TWjFibzNBdU5CRjkxbE4iLCJkaWQiOiJkaWQ6dmRhOjB4ZTcwZTYyODQyNzg4MGFiMDFiZTU1YTM0OWYxY2VmMDQ3OWIyMWJmOCIsInN1YiI6InZmNWYxOWYyYzcyYTFmNDllYjcwMDIxNWMwNGVhMGM4NjVmMjc4ZTA4MGNkMDU5Njc4NjVhMmE5MTA0YWUzNjUzIiwiY29udGV4dE5hbWUiOiJWZXJpZGEgVGVzdGluZzogQXV0aGVudGljYXRpb24iLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjYxNDE5ODQ2LCJleHAiOjE2NjE0MTk5MDZ9.tHugqMHM_udE-eV0u2_2oCjzDSiljfK8DVieK-GcXcU' - // 2/ initialise a new testing account that has a single context with no access token + // 2/ initialise a new testing account that has a single context with an invalid access token const account2 = new AuthContextAccount(CONFIG.DEFAULT_ENDPOINTS, { privateKey: CONFIG.VDA_PRIVATE_KEY, didServerUrl: CONFIG.DID_SERVER_URL, @@ -110,49 +108,53 @@ describe('Verida auth tests', () => { assert.ok(rows && rows.length, 'Have valid results returned') }) - // @todo: test with public database, not encrypted database - - // @todo: test with refreshtoken expired - - // @todo: implement and test with account-web-vault (how?) - - /*it('can handle refreshToken expiry', async function() { + it('can handle refreshToken expiry for an encrypted database', async function() { // Create a working connection - did = await account.did() + const network = new Client({ + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + + const network2 = new Client({ + didServerUrl: CONFIG.DID_SERVER_URL, + environment: CONFIG.ENVIRONMENT + }) + await network.connect(account) - context = await network.openContext(VALID_CONTEXT, true) - const db = await context.openDatabase(DB_NAME_OWNER) + const context = await network.openContext(VALID_CONTEXT, true) // 1/ get the context auth const authContext = await context.getAuthContext() - console.log(authContext) - // Manually invalidate the access token so the refreshToken is needed to re-authenticate - authContext.accessToken = '' + // Manually set to an invalid access token so the refreshToken is needed to re-authenticate + authContext.accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVIUTlLTFprbE9UVERjaU1TOXFOY1Nzb0hBUUJSbUxDbEQ3d2pDUnBsSXNSUE45emZMdHBSM0JlM3d0cXRVVHlFakVPcVhpS2NZeG5zZHBWWTlWRWE3TmUwNUtsUElPUTQxSlpJQktHWW1NYjE2ajRkWkZxVThlek1VZFR1SnRFYmNKUlY0M09JWXE4OTF0ZXY0TDhKY2xPdzBZV1BweTFoMmpKTHkzblNJNVZsZ3RaVG1HVmI5cXlDODRrVnd1NGsySzVvN1VuOFkyUmNWbGpEa2lZRXpaanBxbzlkc0l4Mkh0UGNQcE9TVDhVM05WU29TWjFibzNBdU5CRjkxbE4iLCJkaWQiOiJkaWQ6dmRhOjB4ZTcwZTYyODQyNzg4MGFiMDFiZTU1YTM0OWYxY2VmMDQ3OWIyMWJmOCIsInN1YiI6InZmNWYxOWYyYzcyYTFmNDllYjcwMDIxNWMwNGVhMGM4NjVmMjc4ZTA4MGNkMDU5Njc4NjVhMmE5MTA0YWUzNjUzIiwiY29udGV4dE5hbWUiOiJWZXJpZGEgVGVzdGluZzogQXV0aGVudGljYXRpb24iLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjYxNDE5ODQ2LCJleHAiOjE2NjE0MTk5MDZ9.tHugqMHM_udE-eV0u2_2oCjzDSiljfK8DVieK-GcXcU' + authContext.refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVIUTlLTFprbE9UVERjaU1TOXFOY1Nzb0hBUUJSbUxDbEQ3d2pDUnBsSXNSUE45emZMdHBSM0JlM3d0cXRVVHlFakVPcVhpS2NZeG5zZHBWWTlWRWE3TmUwNUtsUElPUTQxSlpJQktHWW1NYjE2ajRkWkZxVThlek1VZFR1SnRFYmNKUlY0M09JWXE4OTF0ZXY0TDhKY2xPdzBZV1BweTFoMmpKTHkzblNJNVZsZ3RaVG1HVmI5cXlDODRrVnd1NGsySzVvN1VuOFkyUmNWbGpEa2lZRXpaanBxbzlkc0l4Mkh0UGNQcE9TVDhVM05WU29TWjFibzNBdU5CRjkxbE4iLCJkaWQiOiJkaWQ6dmRhOjB4ZTcwZTYyODQyNzg4MGFiMDFiZTU1YTM0OWYxY2VmMDQ3OWIyMWJmOCIsInN1YiI6InZmNWYxOWYyYzcyYTFmNDllYjcwMDIxNWMwNGVhMGM4NjVmMjc4ZTA4MGNkMDU5Njc4NjVhMmE5MTA0YWUzNjUzIiwiY29udGV4dE5hbWUiOiJWZXJpZGEgVGVzdGluZzogQXV0aGVudGljYXRpb24iLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjYxNDE5ODQ2LCJleHAiOjE2NjE0MTk5MDZ9.tHugqMHM_udE-eV0u2_2oCjzDSiljfK8DVieK-GcXcU' - // 2/ initialise a new testing account that has no ability to fetch auth contexts - // and just uses the context specified in the constructor + // 2/ initialise a new testing account that has a single context with an invalid access and refresh token const account2 = new AuthContextAccount(CONFIG.DEFAULT_ENDPOINTS, { privateKey: CONFIG.VDA_PRIVATE_KEY, didServerUrl: CONFIG.DID_SERVER_URL, environment: CONFIG.ENVIRONMENT }, - [VALID_CONTEXT], + VALID_CONTEXT, authContext) // 3/ initiate a new context using the context auth await network2.connect(account2) const context2 = await network2.openContext(VALID_CONTEXT) - console.log('got new context') - // 4/ Manually invalidate the refresh token on the storage node - const disconnect = await account.disconnectDevice(VALID_CONTEXT) - assert.ok(disconnect, 'Device disconnected') + // 4/ Create a new context which should re-authenticate the refresh token + const db2 = await context2?.openDatabase(DB_NAME_OWNER, { + saveDatabase: false + }) - // 5/ Create a new context which will attempt to use the refresh token - // which has been deleted, which should fail - const db2 = await context2?.openDatabase(DB_NAME_OWNER) - await db2?.getDb() - })*/ + assert.ok(db2, 'Have successfully opened a database') + + const rows = await db2!.getMany() + assert.ok(rows && rows.length, 'Have valid results returned') + }) + + // @todo: test with public database, not encrypted database + }) }) \ No newline at end of file From e327e711d54438a97f752313a50a203f626c434f Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 31 Aug 2022 10:07:57 +0930 Subject: [PATCH 22/27] Support deviceId and wallet connect params when logging in. --- packages/account-web-vault/src/interfaces.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/account-web-vault/src/interfaces.ts b/packages/account-web-vault/src/interfaces.ts index 2ccd8992..2d761c3d 100644 --- a/packages/account-web-vault/src/interfaces.ts +++ b/packages/account-web-vault/src/interfaces.ts @@ -1,8 +1,16 @@ import { Interfaces } from '@verida/storage-link' +export interface WalletConnectConfig { + version: string, + uri: string, + chainId: string +} + export interface VaultAccountRequest { logoUrl?: string, // Optional URL that will be displayed as part of the login process openUrl?: string, // Optional URL that will be opened on the user's mobile device once the user is logged in + deviceId?: string, // Optional Device ID to associate with the authentication request + walletConnect?: WalletConnectConfig // Optional configuration for WalletConnect } export interface VaultAccountConfig { From 542249167f74ee27e2934fb919b167a614c06596 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 31 Aug 2022 10:08:15 +0930 Subject: [PATCH 23/27] Validate access and refresh tokens when auto-logging in. --- packages/account-web-vault/package.json | 1 + .../account-web-vault/src/vault-account.ts | 77 +++++++++++++----- yarn.lock | 80 +++++++++++++++++++ 3 files changed, 138 insertions(+), 20 deletions(-) diff --git a/packages/account-web-vault/package.json b/packages/account-web-vault/package.json index 9b975af7..b7628e8f 100644 --- a/packages/account-web-vault/package.json +++ b/packages/account-web-vault/package.json @@ -18,6 +18,7 @@ "@verida/account": "^1.1.9", "@verida/encryption-utils": "^1.1.3", "@verida/keyring": "^1.1.3", + "jsonwebtoken": "^8.5.1", "lodash": "^4.17.21", "qrcode-with-logos": "^1.0.3", "store": "^2.0.12" diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index b38d3dc2..5f63ee86 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -3,6 +3,7 @@ import { Interfaces } from '@verida/storage-link' import { Keyring } from '@verida/keyring' import VaultModalLogin from './vault-modal-login' import Axios from "axios"; +const jwt = require('jsonwebtoken'); const querystring = require('querystring') const _ = require('lodash') @@ -113,27 +114,60 @@ export default class VaultAccount extends Account { return promise } + /** + * Verify we have valid JWT's and non-expired accessToken and refreshToken + * + * @param contextAuth + * @returns + */ + public contextAuthIsValid(contextAuth: VeridaDatabaseAuthContext): boolean { + if (!contextAuth.accessToken || !contextAuth.refreshToken) { + return false + } + + // verify tokens are valid JWT's + const decodedAccessToken = jwt.decode(contextAuth.accessToken!) + if (!decodedAccessToken) { + return false + } + + const decodedRefreshToken = jwt.decode(contextAuth.refreshToken!) + if (!decodedRefreshToken) { + return false + } + + // verify tokens haven't expired + const now = Math.floor(Date.now() / 1000) + if (decodedAccessToken.exp < now || decodedRefreshToken.exp < now) { + return false + } + + return true + } + public async loadFromSession(contextName: string): Promise { // First, attempt to Load from query parameters if specified const token = getAuthTokenFromQueryParams() if (token && token.context == contextName) { - this.addContext(token.context, token.contextConfig, new Keyring(token.signature), token.contextAuth) - this.setDid(token.did) + if (this.contextAuthIsValid(token.contextAuth)) { + this.addContext(token.context, token.contextConfig, new Keyring(token.signature), token.contextAuth) + this.setDid(token.did) - if (typeof(this.config!.callback) === "function") { - this.config!.callback(token) - } + if (typeof(this.config!.callback) === "function") { + this.config!.callback(token) + } - // Store the session from the query params so future page loads will be authenticated - let storedSessions = store.get(VERIDA_AUTH_CONTEXT) - if (!storedSessions) { - storedSessions = {} - } + // Store the session from the query params so future page loads will be authenticated + let storedSessions = store.get(VERIDA_AUTH_CONTEXT) + if (!storedSessions) { + storedSessions = {} + } - storedSessions[contextName] = token - store.set(VERIDA_AUTH_CONTEXT, storedSessions) - - return token.contextConfig + storedSessions[contextName] = token + store.set(VERIDA_AUTH_CONTEXT, storedSessions) + + return token.contextConfig + } } const storedSessions = store.get(VERIDA_AUTH_CONTEXT) @@ -144,14 +178,17 @@ export default class VaultAccount extends Account { const response = storedSessions[contextName] - this.setDid(response.did) - this.addContext(response.context, response.contextConfig, new Keyring(response.signature), response.contextAuth) + if (this.contextAuthIsValid(response.contextAuth)) { + this.setDid(response.did) - if (typeof(this.config!.callback) === "function") { - this.config!.callback(response) - } + this.addContext(response.context, response.contextConfig, new Keyring(response.signature), response.contextAuth) + + if (typeof(this.config!.callback) === "function") { + this.config!.callback(response) + } - return response.contextConfig + return response.contextConfig + } } public async keyring(contextName: string): Promise { diff --git a/yarn.lock b/yarn.lock index 2fefb709..422ca3ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2129,6 +2129,11 @@ buffer-alloc@^1.2.0: buffer-alloc-unsafe "^1.1.0" buffer-fill "^1.0.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -3020,6 +3025,13 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + elliptic@6.5.4, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -4528,6 +4540,22 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -4538,6 +4566,23 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -4757,11 +4802,46 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" From 3452358cf1cea791d95a63f889343beb25bde70c Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 1 Sep 2022 17:06:05 +0930 Subject: [PATCH 24/27] Update HTTP status code checks to match changes in storage-node. --- packages/account-node/src/authTypes/VeridaDatabase.ts | 2 +- packages/account-web-vault/src/vault-account.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/account-node/src/authTypes/VeridaDatabase.ts b/packages/account-node/src/authTypes/VeridaDatabase.ts index b4bbd2d9..56d46f3d 100644 --- a/packages/account-node/src/authTypes/VeridaDatabase.ts +++ b/packages/account-node/src/authTypes/VeridaDatabase.ts @@ -107,7 +107,7 @@ export default class VeridaDatabaseAuthType extends AuthType { } catch (err: any) { // Refresh token is invalid, so raise an exception that will be caught within the protocol // and force the sign in to be restarted - if (err.message == 'Request failed with status code 400') { + if (err.message == 'Request failed with status code 401') { throw new ContextAuthorizationError("Expired refresh token") } else { throw err diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index 5f63ee86..fa56277a 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -284,7 +284,7 @@ export default class VaultAccount extends Account { } catch (err: any) { // Refresh token is invalid, so raise an exception that will be caught within the protocol // and force the sign in to be restarted - if (err.message == 'Request failed with status code 400') { + if (err.message == 'Request failed with status code 401') { throw new ContextAuthorizationError("Expired refresh token") } else { throw err From 31c1dbb69e24ea35501a7e51def9631986fe4475 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 2 Sep 2022 10:08:09 +0930 Subject: [PATCH 25/27] Add INVALID_ENDPOINTS to testing config --- packages/client-ts/test/config.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/client-ts/test/config.ts b/packages/client-ts/test/config.ts index 92f9977a..04f49952 100644 --- a/packages/client-ts/test/config.ts +++ b/packages/client-ts/test/config.ts @@ -18,5 +18,15 @@ export default { type: 'VeridaMessage', endpointUri: 'https://db.testnet.verida.io:5002/' }, + }, + INVALID_ENDPOINTS: { // endpoints that resolve to non-existant storage node + defaultDatabaseServer: { + type: 'VeridaDatabase', + endpointUri: 'http://localhost:6000/' + }, + defaultMessageServer: { + type: 'VeridaMessage', + endpointUri: 'http://localhost:6000/' + }, } } \ No newline at end of file From a15e61eab1109a17f88fb43fceb43250350dcfda Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 2 Sep 2022 11:32:56 +0930 Subject: [PATCH 26/27] Replace deviceId with userAgent --- packages/account-web-vault/src/interfaces.ts | 2 +- packages/account-web-vault/src/vault-account.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/account-web-vault/src/interfaces.ts b/packages/account-web-vault/src/interfaces.ts index 2d761c3d..7e592107 100644 --- a/packages/account-web-vault/src/interfaces.ts +++ b/packages/account-web-vault/src/interfaces.ts @@ -9,7 +9,7 @@ export interface WalletConnectConfig { export interface VaultAccountRequest { logoUrl?: string, // Optional URL that will be displayed as part of the login process openUrl?: string, // Optional URL that will be opened on the user's mobile device once the user is logged in - deviceId?: string, // Optional Device ID to associate with the authentication request + userAgent?: string, // Optional User Agent that made the authentication request (makes it easier for a user to know which refresh token to disconnect) walletConnect?: WalletConnectConfig // Optional configuration for WalletConnect } diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index fa56277a..657b152e 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -72,6 +72,8 @@ export default class VaultAccount extends Account { constructor(config: VaultAccountConfig = {}) { super() this.config = config + this.config.request = this.config.request ? this.config.request : {} + this.config.request.userAgent = navigator.userAgent } public async connectContext(contextName: string, ignoreSession: boolean = false) { From cd371aaa9ca96c535a2b0adf4eb1304c97166bcd Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 2 Sep 2022 11:33:01 +0930 Subject: [PATCH 27/27] Point to live docs --- packages/account-web-vault/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account-web-vault/README.md b/packages/account-web-vault/README.md index 7469df72..a7b162ac 100644 --- a/packages/account-web-vault/README.md +++ b/packages/account-web-vault/README.md @@ -1,2 +1,2 @@ -@todo \ No newline at end of file +See https://developers.verida.io/docs/single-sign-on-sdk \ No newline at end of file