-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/181 support updated auth storage node (#225)
* Better handling opening a context without an account. Update storage node consent message. * Support CouchDB token auth for public and encrypted databases. * Include token with user permissions * Add more progress * Adding context auth interface * Remove redundant contextAuth * Core authJWT refactor complete. Still need edge cases and fixing some tests. * Fix deprecated substr() reference * Update context name * Support manual injection of contxt name * Move getAuthContext into account. Refactor. * Support caching auth contexts * Refactor auth context to be implemented in account instance. Better separation of auth context and client operations. * Support managing auth contexts and disconnecting devices. Support Auth Context Account type to help with testing. * Refactor to support authentication retry when access / refresh token expires. * Support getAuthContext in vault-account (untested) * Fix typescript issues. * Cache opened databases to avoid opening multiple and creating lots of open sockets. * Support account-web-vault handling invalid access token or refresh token. * Fix error handling with promises * Fix refresh token expiry with account-node * Support deviceId and wallet connect params when logging in. * Validate access and refresh tokens when auto-logging in. * Update HTTP status code checks to match changes in storage-node. * Add INVALID_ENDPOINTS to testing config * Replace deviceId with userAgent * Point to live docs
- Loading branch information
Showing
31 changed files
with
1,058 additions
and
202 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.