aladinnetwork-blockstack
Version:
The Aladin Javascript library for authentication, identity, and storage.
398 lines (359 loc) • 13.4 kB
text/typescript
import { AppConfig } from './appConfig'
import { SessionOptions } from './sessionData'
import {
LocalStorageStore,
SessionDataStore,
InstanceDataStore
} from './sessionStore'
import * as authApp from './authApp'
import * as authMessages from './authMessages'
import * as storage from '../storage'
import {
nextHour
} from '../utils'
import {
MissingParameterError,
InvalidStateError
} from '../errors'
import { Logger } from '../logger'
import { GaiaHubConfig, connectToGaiaHub } from '../storage/hub'
import { ALADIN_DEFAULT_GAIA_HUB_URL, AuthScope } from './authConstants'
/**
*
* Represents an instance of a signed in user for a particular app.
*
* A signed in user has access to two major pieces of information
* about the user, the user's private key for that app and the location
* of the user's gaia storage bucket for the app.
*
* A user can be signed in either directly through the interactive
* sign in process or by directly providing the app private key.
*
*
*/
export class UserSession {
appConfig: AppConfig
store: SessionDataStore
/**
* Creates a UserSession object
*
* @param options
*/
constructor(options?: {
appConfig?: AppConfig,
sessionStore?: SessionDataStore,
sessionOptions?: SessionOptions }) {
let runningInBrowser = true
if (typeof window === 'undefined' && typeof self === 'undefined') {
Logger.debug('UserSession: not running in browser')
runningInBrowser = false
}
if (options && options.appConfig) {
this.appConfig = options.appConfig
} else if (runningInBrowser) {
this.appConfig = new AppConfig()
} else {
throw new MissingParameterError('You need to specify options.appConfig')
}
if (options && options.sessionStore) {
this.store = options.sessionStore
} else if (runningInBrowser) {
if (options) {
this.store = new LocalStorageStore(options.sessionOptions)
} else {
this.store = new LocalStorageStore()
}
} else if (options) {
this.store = new InstanceDataStore(options.sessionOptions)
} else {
this.store = new InstanceDataStore()
}
}
/**
* Generates an authentication request and redirects the user to the Aladin
* browser to approve the sign in request.
*
* Please note that this requires that the web browser properly handles the
* `aladin:` URL protocol handler.
*
* Most applications should use this
* method for sign in unless they require more fine grained control over how the
* authentication request is generated. If your app falls into this category,
* use [[generateAndStoreTransitKey]], [[makeAuthRequest]],
* and [[redirectToSignInWithAuthRequest]] to build your own sign in process.
*
* @param redirectURI Location of your application.
* @param manifestURI Location of the manifest.json file
* @param scopes Permissions requested by the application. Possible values are
* `store_write` (default) or `publish_data`.
*
* @returns {void}
*/
redirectToSignIn(
redirectURI?: string,
manifestURI?: string,
scopes?: Array<AuthScope | string>
) {
const transitKey = this.generateAndStoreTransitKey()
const authRequest = this.makeAuthRequest(transitKey, redirectURI, manifestURI, scopes)
const authenticatorURL = this.appConfig && this.appConfig.authenticatorURL
return authApp.redirectToSignInWithAuthRequest(authRequest, authenticatorURL)
}
/**
* Redirects the user to the Aladin browser to approve the sign in request.
* To construct a request see the [[makeAuthRequest]] function.
*
* The user is redirected to the authenticator URL specified in the `AppConfig`
* if the `aladin:` protocol handler is not detected.
* Please note that the protocol handler detection does not work on all browsers.
*
* @param authRequest A request string built by the [[makeAuthRequest]] function
* @param aladinIDHost The ID of the Aladin Browser application.
*
*/
redirectToSignInWithAuthRequest(
authRequest?: string,
aladinIDHost?: string
) {
authRequest = authRequest || this.makeAuthRequest()
const authenticatorURL = aladinIDHost
|| (this.appConfig && this.appConfig.authenticatorURL)
return authApp.redirectToSignInWithAuthRequest(authRequest, authenticatorURL)
}
/**
* Generates an authentication request that can be sent to the Aladin
* browser for the user to approve sign in. This authentication request can
* then be used for sign in by passing it to the [[redirectToSignInWithAuthRequest]]
* method.
*
* *Note*: This method should only be used if you want to use a customized authentication
* flow. Typically, you'd use [[redirectToSignIn]] which is the default sign in method.
*
* @param transitKey A HEX encoded transit private key.
* @param redirectURI Location to redirect the user to after sign in approval.
* @param manifestURI Location of this app's manifest file.
* @param scopes The permissions this app is requesting. The default is `store_write`.
* @param appDomain The origin of the app.
* @param expiresAt The time at which this request is no longer valid.
* @param extraParams Any extra parameters to pass to the authenticator. Use this to
* pass options that aren't part of the Aladin authentication specification,
* but might be supported by special authenticators.
*
* @returns {String} the authentication request
*/
makeAuthRequest(
transitKey?: string,
redirectURI?: string,
manifestURI?: string,
scopes?: Array<AuthScope | string>,
appDomain?: string,
expiresAt: number = nextHour().getTime(),
extraParams: any = {}
): string {
const appConfig = this.appConfig
if (!appConfig) {
throw new InvalidStateError('Missing AppConfig')
}
transitKey = transitKey || this.generateAndStoreTransitKey()
redirectURI = redirectURI || appConfig.redirectURI()
manifestURI = manifestURI || appConfig.manifestURI()
scopes = scopes || appConfig.scopes
appDomain = appDomain || appConfig.appDomain
return authMessages.makeAuthRequest(
transitKey, redirectURI, manifestURI,
scopes, appDomain, expiresAt, extraParams)
}
/**
* Generates a ECDSA keypair to
* use as the ephemeral app transit private key
* and store in the session.
*
* @returns {String} the hex encoded private key
*
*/
generateAndStoreTransitKey(): string {
const sessionData = this.store.getSessionData()
const transitKey = authMessages.generateTransitKey()
sessionData.transitKey = transitKey
this.store.setSessionData(sessionData)
return transitKey
}
/**
* Retrieve the authentication token from the URL query.
*
* @returns {String} the authentication token if it exists otherwise `null`
*/
getAuthResponseToken(): string {
return authApp.getAuthResponseToken()
}
/**
* Check if there is a authentication request that hasn't been handled.
*
* @returns{Boolean} `true` if there is a pending sign in, otherwise `false`
*/
isSignInPending() {
return authApp.isSignInPending()
}
/**
* Check if a user is currently signed in.
*
* @returns {Boolean} `true` if the user is signed in, `false` if not.
*/
isUserSignedIn() {
return !!this.store.getSessionData().userData
}
/**
* Try to process any pending sign in request by returning a `Promise` that resolves
* to the user data object if the sign in succeeds.
*
* @param {String} authResponseToken - the signed authentication response token
* @returns {Promise} that resolves to the user data object if successful and rejects
* if handling the sign in request fails or there was no pending sign in request.
*/
handlePendingSignIn(authResponseToken: string = this.getAuthResponseToken()) {
const transitKey = this.store.getSessionData().transitKey
const nameLookupURL = this.store.getSessionData().coreNode
return authApp.handlePendingSignIn(nameLookupURL, authResponseToken, transitKey, this)
}
/**
* Retrieves the user data object. The user's profile is stored in the key [[Profile]].
*
* @returns {Object} User data object.
*/
loadUserData() {
const userData = this.store.getSessionData().userData
if (!userData) {
throw new InvalidStateError('No user data found. Did the user sign in?')
}
return userData
}
/**
* Sign the user out and optionally redirect to given location.
* @param redirectURL Location to redirect user to after sign out.
* Only used in environments with `window` available
*/
signUserOut(redirectURL?: string) {
authApp.signUserOut(redirectURL, this)
}
/**
* Encrypts the data provided with the app public key.
* @param {String|Buffer} content the data to encrypt
* @param {String} options.publicKey the hex string of the ECDSA public
* key to use for encryption. If not provided, will use user's appPrivateKey.
*
* @returns {String} Stringified ciphertext object
*/
encryptContent(
content: string | Buffer,
options?: {publicKey?: string}
) {
return storage.encryptContent(content, options, this)
}
/**
* Decrypts data encrypted with `encryptContent` with the
* transit private key.
* @param {String|Buffer} content - encrypted content.
* @param {String} options.privateKey - The hex string of the ECDSA private
* key to use for decryption. If not provided, will use user's appPrivateKey.
* @returns {String|Buffer} decrypted content.
*/
decryptContent(content: string, options?: {privateKey?: string}) {
return storage.decryptContent(content, options, this)
}
/**
* Stores the data provided in the app's data store to to the file specified.
* @param {String} path - the path to store the data in
* @param {String|Buffer} content - the data to store in the file
* @param options a [[PutFileOptions]] object
*
* @returns {Promise} that resolves if the operation succeed and rejects
* if it failed
*/
putFile(path: string, content: string | Buffer, options?: import('../storage').PutFileOptions) {
return storage.putFile(path, content, options, this)
}
/**
* Retrieves the specified file from the app's data store.
*
* @param {String} path - the path to the file to read
* @param {Object} options a [[GetFileOptions]] object
*
* @returns {Promise} that resolves to the raw data in the file
* or rejects with an error
*/
getFile(path: string, options?: import('../storage').GetFileOptions) {
return storage.getFile(path, options, this)
}
/**
* Get the URL for reading a file from an app's data store.
*
* @param {String} path - the path to the file to read
*
* @returns {Promise<string>} that resolves to the URL or rejects with an error
*/
getFileUrl(path: string, options?: import('../storage').GetFileUrlOptions): Promise<string> {
return storage.getFileUrl(path, options, this)
}
/**
* List the set of files in this application's Gaia storage bucket.
*
* @param {function} callback - a callback to invoke on each named file that
* returns `true` to continue the listing operation or `false` to end it
*
* @returns {Promise} that resolves to the number of files listed
*/
listFiles(callback: (name: string) => boolean): Promise<number> {
return storage.listFiles(callback, this)
}
/**
* Deletes the specified file from the app's data store.
* @param path - The path to the file to delete.
* @param options - Optional options object.
* @param options.wasSigned - Set to true if the file was originally signed
* in order for the corresponding signature file to also be deleted.
* @returns Resolves when the file has been removed or rejects with an error.
*/
public deleteFile(path: string, options?: { wasSigned?: boolean }) {
return storage.deleteFile(path, options, this)
}
/**
* @ignore
*/
getOrSetLocalGaiaHubConnection(): Promise<GaiaHubConfig> {
const sessionData = this.store.getSessionData()
const userData = sessionData.userData
if (!userData) {
throw new InvalidStateError('Missing userData')
}
const hubConfig = userData.gaiaHubConfig
if (hubConfig) {
return Promise.resolve(hubConfig)
}
return this.setLocalGaiaHubConnection()
}
/**
* These two functions are app-specific connections to gaia hub,
* they read the user data object for information on setting up
* a hub connection, and store the hub config to localstorage
* @private
* @returns {Promise} that resolves to the new gaia hub connection
*/
async setLocalGaiaHubConnection(): Promise<GaiaHubConfig> {
const userData = this.loadUserData()
if (!userData) {
throw new InvalidStateError('Missing userData')
}
if (!userData.hubUrl) {
userData.hubUrl = ALADIN_DEFAULT_GAIA_HUB_URL
}
const gaiaConfig = await connectToGaiaHub(
userData.hubUrl,
userData.appPrivateKey,
userData.gaiaAssociationToken)
userData.gaiaHubConfig = gaiaConfig
const sessionData = this.store.getSessionData()
sessionData.userData.gaiaHubConfig = gaiaConfig
this.store.setSessionData(sessionData)
return gaiaConfig
}
}