Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/181 support updated auth storage node #225

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c927e57
Better handling opening a context without an account. Update storage …
Apr 3, 2022
f21bc97
Merge branch 'main' into feature/181-support-updated-auth-storage-node
Apr 27, 2022
d44b283
Support CouchDB token auth for public and encrypted databases.
Apr 27, 2022
2d7db2c
Include token with user permissions
Apr 27, 2022
c0adfbc
Add more progress
Jun 15, 2022
2293771
Adding context auth interface
Jul 16, 2022
3dd5ce0
Remove redundant contextAuth
Aug 23, 2022
7b86d9d
Core authJWT refactor complete. Still need edge cases and fixing some…
Aug 23, 2022
92f4e2c
Fix deprecated substr() reference
Aug 23, 2022
5766a2a
Update context name
Aug 23, 2022
6e71b6a
Merge branch 'main' into feature/181-support-updated-auth-storage-node
Aug 23, 2022
ca612e8
Support manual injection of contxt name
Aug 23, 2022
fc4c82b
Move getAuthContext into account. Refactor.
Aug 25, 2022
f5caacd
Support caching auth contexts
Aug 25, 2022
b7fc5bc
Refactor auth context to be implemented in account instance. Better s…
Aug 25, 2022
f922599
Support managing auth contexts and disconnecting devices. Support Aut…
Aug 28, 2022
fb028f7
Refactor to support authentication retry when access / refresh token …
Aug 28, 2022
6f30f39
Support getAuthContext in vault-account (untested)
Aug 28, 2022
cb94b2a
Fix typescript issues.
Aug 29, 2022
92010a7
Cache opened databases to avoid opening multiple and creating lots of…
Aug 29, 2022
a41203e
Support account-web-vault handling invalid access token or refresh to…
Aug 30, 2022
4347dff
Fix error handling with promises
Aug 30, 2022
32a932b
Fix refresh token expiry with account-node
Aug 30, 2022
e327e71
Support deviceId and wallet connect params when logging in.
Aug 31, 2022
5422491
Validate access and refresh tokens when auto-logging in.
Aug 31, 2022
3452358
Update HTTP status code checks to match changes in storage-node.
Sep 1, 2022
31c1dbb
Add INVALID_ENDPOINTS to testing config
Sep 2, 2022
a15e61e
Replace deviceId with userAgent
Sep 2, 2022
cd371aa
Point to live docs
Sep 2, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/account-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
165 changes: 165 additions & 0 deletions packages/account-node/src/authTypes/VeridaDatabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import Axios from "axios";
import AutoAccount from "../auto";
import { Interfaces } from '@verida/storage-link'
import { Account, VeridaDatabaseAuthContext, AuthType, VeridaDatabaseAuthTypeConfig, ContextAuthorizationError } from "@verida/account";

export default class VeridaDatabaseAuthType extends AuthType {

protected contextAuth?: VeridaDatabaseAuthContext
protected account: AutoAccount

public constructor(account: Account, contextName: string, serviceEndpoint: Interfaces.SecureContextEndpoint, signKey: Interfaces.SecureContextPublicKey) {
super(account, contextName, serviceEndpoint, signKey)
this.account = <AutoAccount> account
}

public async getAuthContext(config: VeridaDatabaseAuthTypeConfig = {
deviceId: "Test device",
force: false
}): Promise<VeridaDatabaseAuthContext> {
const serverUrl = config && config.endpointUri ? config.endpointUri : this.serviceEndpoint.endpointUri

// If we have an invalid access token, clear it
if (this.contextAuth && config.invalidAccessToken) {
this.contextAuth.accessToken = undefined
}

// We already have a context auth object, so reuse it unless
// 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 && !config.force && this.contextAuth.accessToken) {
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) {
//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(this.contextName).post(serverUrl + "auth/generateAuthJwt",{
did,
contextName: this.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: "${this.contextName}"?\n\n${did.toLowerCase()}\n${authJwt.authRequestId}`
const signature = await this.account.sign(consentMessage)

refreshResponse = await this.getAxios(this.contextName).post(serverUrl + "auth/authenticate",{
authJwt: authJwt.authJwt,
did,
contextName: this.contextName,
signature,
deviceId: config.deviceId
});

//console.log('refresh response', refreshResponse.data)
} catch (err: any) {
throw new ContextAuthorizationError("Expired refresh token")
}

//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,
endpointUri: serverUrl,
publicSigningKey: this.signKey
}

//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')

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
} 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 401') {
throw new ContextAuthorizationError("Expired refresh token")
} else {
throw err
}
}
}

// @todo: test if connection is valid?

return this.contextAuth!
}

public async disconnectDevice(deviceId: string="Test device"): Promise<boolean> {
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.endpointUri}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: {
// @todo: Application-Name needs to become Storage-Context
"Application-Name": storageContext,
},
};

if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`
}

return Axios.create(config);
}

}
44 changes: 44 additions & 0 deletions packages/account-node/src/authcontext.ts
Original file line number Diff line number Diff line change
@@ -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<AuthContext> {
return super.getAuthContext(contextName, contextConfig, authConfig, authType)
}

}
33 changes: 32 additions & 1 deletion packages/account-node/src/auto.ts
Original file line number Diff line number Diff line change
@@ -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, AuthType, 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
Expand All @@ -17,6 +18,7 @@ export default class AutoAccount extends Account {

private wallet: Wallet
protected accountConfig: AccountConfig
protected contextAuths: Record<string, AuthType> = {}

constructor(accountConfig: AccountConfig, autoConfig: NodeAccountConfig) {
super()
Expand Down Expand Up @@ -102,4 +104,33 @@ export default class AutoAccount extends Account {
return this.didClient
}

public async getAuthContext(contextName: string, contextConfig: Interfaces.SecureContextConfig, authConfig: VeridaDatabaseAuthTypeConfig = {
force: false
}, authType = "database"): Promise<AuthContext> {
// Use existing context auth instance if it exists
if (this.contextAuths[contextName] && !authConfig.force) {
return this.contextAuths[contextName].getAuthContext(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})`)
}

public async disconnectDevice(contextName: string, deviceId: string="Test device"): Promise<boolean> {
if (!this.contextAuths[contextName]) {
throw new Error(`Context not connected ${contextName}`)
}

return this.contextAuths[contextName].disconnectDevice(deviceId)
}

}
6 changes: 5 additions & 1 deletion packages/account-node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import AutoAccount from "./auto"
import LimitedAccount from "./limited"
import AuthContextAccount from "./authcontext"
import VeridaDatabaseAuthType from "./authTypes/VeridaDatabase"

export {
AutoAccount,
LimitedAccount
VeridaDatabaseAuthType,
LimitedAccount,
AuthContextAccount
}
2 changes: 1 addition & 1 deletion packages/account-node/src/limited.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion packages/account-web-vault/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@

@todo
See https://developers.verida.io/docs/single-sign-on-sdk
1 change: 1 addition & 0 deletions packages/account-web-vault/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions packages/account-web-vault/src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -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
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
}

export interface VaultAccountConfig {
Expand Down
Loading