UNPKG

aladinnetwork-blockstack

Version:

The Aladin Javascript library for authentication, identity, and storage.

381 lines (357 loc) 14.4 kB
import queryString from 'query-string' // @ts-ignore: Could not find a declaration file for module import { decodeToken } from 'jsontokens' import { verifyAuthResponse } from './authVerification' import { isLaterVersion, hexStringToECPair, getGlobalObject, getGlobalObjects } from '../utils' import { fetchPrivate } from '../fetchUtil' import { getAddressFromDID } from '../dids' import { LoginFailedError } from '../errors' import { decryptPrivateKey, makeAuthRequest } from './authMessages' import { ALADIN_DEFAULT_GAIA_HUB_URL, DEFAULT_ALADIN_HOST, NAME_LOOKUP_PATH, AuthScope } from './authConstants' import { extractProfile } from '../profiles/profileTokens' import { UserSession } from './userSession' import { config } from '../config' import { Logger } from '../logger' import { GaiaHubConfig } from '../storage/hub' import { protocolEchoReplyDetection } from './protocolEchoDetection' import { launchCustomProtocol } from './protocolLaunch' const DEFAULT_PROFILE = { '@type': 'Person', '@context': 'http://schema.org' } /** * Returned from the [[UserSession.loadUserData]] function. */ export interface UserData { // public: the aladin ID (for example: stackerson.id or alice.aladin.id) username: string; // public: the email address for the user. only available if the `email` // scope is requested, and if the user has entered a valid email into // their profile. // // **Note**: Aladin does not require email validation // for users for privacy reasons and blah blah (something like this, idk) email?: string; // probably public: (a quick description of what this is, and a link to the // DID foundation and/or the aladin docs related to DID, idk) decentralizedID: string; // probably private: looks like it happens to be the btc address but idk // the value of establishing this as a supported field identityAddress: string; // probably public: this is an advanced feature, I think many app devs // using our more advanced encryption functions (as opposed to putFile/getFile), // are probably using this. seems useful to explain. appPrivateKey: string; // maybe public: possibly useful for advanced devs / webapps. I see an opportunity // to make a small plug about "user owned data" here, idk. hubUrl: string; // maybe private: this would be an advanced field for app devs to use. authResponseToken: string; // private: does not get sent to webapp at all. coreSessionToken?: string; // private: does not get sent to webapp at all. gaiaAssociationToken?: string; // public: this is the proper `Person` schema json for the user. // This is the data that gets used when the `new aladin.Person(profile)` class is used. profile: any; // private: does not get sent to webapp at all. gaiaHubConfig?: GaiaHubConfig; } /** * @deprecated * #### v19 Use [[UserSession.isUserSignedIn]] instead. * * Check if a user is currently signed in. * @return {Boolean} `true` if the user is signed in, `false` if not. */ export function isUserSignedIn() { console.warn('DEPRECATION WARNING: The static isUserSignedIn() function will be deprecated in ' + 'the next major release of aladin.js. Create an instance of UserSession and call the ' + 'instance method isUserSignedIn().') const userSession = new UserSession() return userSession.isUserSignedIn() } /** * * * @deprecated * #### v19 Use [[UserSession.isUserSignedIn]] instead. * * 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 `makeAuthRequest` and `redirectToSignInWithAuthRequest` to build your own sign in process. * * @param {String} [redirectURI=`${window.location.origin}/`] * The location to which the identity provider will redirect the user after * the user approves sign in. * @param {String} [manifestURI=`${window.location.origin}/manifest.json`] * Location of the manifest file. * @param {Array} [scopes=DEFAULT_SCOPE] Defaults to requesting write access to * this app's data store. * An array of strings indicating which permissions this app is requesting. * @return {void} */ export function redirectToSignIn(redirectURI?: string, manifestURI?: string, scopes?: Array<AuthScope | string>) { console.warn('DEPRECATION WARNING: The static redirectToSignIn() function will be deprecated in the ' + 'next major release of aladin.js. Create an instance of UserSession and call the ' + 'instance method redirectToSignIn().') const authRequest = makeAuthRequest(null, redirectURI, manifestURI, scopes) redirectToSignInWithAuthRequest(authRequest) } /** * @deprecated * #### v19 Use [[UserSession.isSignInPending]] instead. * * Check if there is a authentication request that hasn't been handled. * * Also checks for a protocol echo reply (which if detected then the page * will be automatically redirected after this call). * * @return {Boolean} `true` if there is a pending sign in, otherwise `false` */ export function isSignInPending() { try { const isProtocolEcho = protocolEchoReplyDetection() if (isProtocolEcho) { Logger.info('protocolEchoReply detected from isSignInPending call, the page is about to redirect.') return true } } catch (error) { Logger.error(`Error checking for protocol echo reply isSignInPending: ${error}`) } return !!getAuthResponseToken() } /** * @deprecated * #### v19 Use [[UserSession.getAuthResponseToken]] instead. * * Retrieve the authentication token from the URL query * @return {String} the authentication token if it exists otherwise `null` */ export function getAuthResponseToken(): string { const search = getGlobalObject( 'location', { throwIfUnavailable: true, usageDesc: 'getAuthResponseToken' } ).search const queryDict = queryString.parse(search) return queryDict.authResponse ? <string>queryDict.authResponse : '' } /** * @deprecated * #### v19 Use [[UserSession.loadUserData]] instead. * * Retrieves the user data object. The user's profile is stored in the key `profile`. * @return {Object} User data object. */ export function loadUserData() { console.warn('DEPRECATION WARNING: The static loadUserData() function will be deprecated in the ' + 'next major release of aladin.js. Create an instance of UserSession and call the ' + 'instance method loadUserData().') const userSession = new UserSession() return userSession.loadUserData() } /** * @deprecated * #### v19 Use [[UserSession.signUserOut]] instead. * * 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 */ export function signUserOut(redirectURL?: string, caller?: UserSession) { const userSession = caller || new UserSession() userSession.store.deleteSessionData() if (redirectURL) { getGlobalObject( 'location', { throwIfUnavailable: true, usageDesc: 'signUserOut' } ).href = redirectURL } } /** * @deprecated * #### v19 Use [[UserSession.redirectToSignInWithAuthRequest]] instead. * * Redirects the user to the Aladin browser to approve the sign in request * given. * * The user is redirected to the `aladinIDHost` if the `aladin:` * protocol handler is not detected. Please note that the protocol handler detection * does not work on all browsers. * @param {String} authRequest - the authentication request generated by `makeAuthRequest` * @param {String} aladinIDHost - the URL to redirect the user to if the aladin * protocol handler is not detected * @return {void} */ export function redirectToSignInWithAuthRequest( authRequest?: string, aladinIDHost: string = DEFAULT_ALADIN_HOST, ) { authRequest = authRequest || makeAuthRequest() const httpsURI = `${aladinIDHost}?authRequest=${authRequest}` const { navigator, location } = getGlobalObjects( ['navigator', 'location'], { throwIfUnavailable: true, usageDesc: 'redirectToSignInWithAuthRequest' } ) // If they're on a mobile OS, always redirect them to HTTPS site if (/Android|webOS|iPhone|iPad|iPod|Opera Mini/i.test(navigator.userAgent)) { Logger.info('detected mobile OS, sending to https') location.href = httpsURI return } function successCallback() { Logger.info('protocol handler detected') // The detection function should open the link for us } function failCallback() { Logger.warn('protocol handler not detected') location.href = httpsURI } launchCustomProtocol(authRequest, successCallback, failCallback) } /** * @deprecated * #### v19 Use [[UserSession.handlePendingSignIn]] instead. * * 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} nameLookupURL - the endpoint against which to verify public * keys match claimed username * @param {String} authResponseToken - the signed authentication response token * @param {String} transitKey - the transit private key that corresponds to the transit public key * that was provided in the authentication request * @return {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. */ export async function handlePendingSignIn( nameLookupURL: string = '', authResponseToken: string = getAuthResponseToken(), transitKey?: string, caller?: UserSession ): Promise<UserData> { try { const isProtocolEcho = protocolEchoReplyDetection() if (isProtocolEcho) { const msg = 'handlePendingSignIn called while protocolEchoReply was detected, and ' + 'the page is about to redirect. This function will resolve with an error after ' + 'several seconds, if the page was not redirected for some reason.' Logger.info(msg) return new Promise<UserData>((_resolve, reject) => { setTimeout(() => { Logger.error('Page should have redirected by now. handlePendingSignIn will now throw.') reject(msg) }, 3000) }) } } catch (error) { Logger.error(`Error checking for protocol echo reply handlePendingSignIn: ${error}`) } if (!caller) { caller = new UserSession() } if (!transitKey) { transitKey = caller.store.getSessionData().transitKey } if (!nameLookupURL) { const tokenPayload = decodeToken(authResponseToken).payload if (isLaterVersion(tokenPayload.version, '1.3.0') && tokenPayload.aladinAPIUrl !== null && tokenPayload.aladinAPIUrl !== undefined) { // override globally Logger.info(`Overriding ${config.network.aladinAPIUrl} ` + `with ${tokenPayload.aladinAPIUrl}`) config.network.aladinAPIUrl = tokenPayload.aladinAPIUrl } nameLookupURL = `${config.network.aladinAPIUrl}${NAME_LOOKUP_PATH}` } const isValid = await verifyAuthResponse(authResponseToken, nameLookupURL) if (!isValid) { throw new LoginFailedError('Invalid authentication response.') } const tokenPayload = decodeToken(authResponseToken).payload // TODO: real version handling let appPrivateKey = tokenPayload.private_key let coreSessionToken = tokenPayload.core_token if (isLaterVersion(tokenPayload.version, '1.1.0')) { if (transitKey !== undefined && transitKey != null) { if (tokenPayload.private_key !== undefined && tokenPayload.private_key !== null) { try { appPrivateKey = decryptPrivateKey(transitKey, tokenPayload.private_key) } catch (e) { Logger.warn('Failed decryption of appPrivateKey, will try to use as given') try { hexStringToECPair(tokenPayload.private_key) } catch (ecPairError) { throw new LoginFailedError('Failed decrypting appPrivateKey. Usually means' + ' that the transit key has changed during login.') } } } if (coreSessionToken !== undefined && coreSessionToken !== null) { try { coreSessionToken = decryptPrivateKey(transitKey, coreSessionToken) } catch (e) { Logger.info('Failed decryption of coreSessionToken, will try to use as given') } } } else { throw new LoginFailedError('Authenticating with protocol > 1.1.0 requires transit' + ' key, and none found.') } } let hubUrl = ALADIN_DEFAULT_GAIA_HUB_URL let gaiaAssociationToken if (isLaterVersion(tokenPayload.version, '1.2.0') && tokenPayload.hubUrl !== null && tokenPayload.hubUrl !== undefined) { hubUrl = tokenPayload.hubUrl } if (isLaterVersion(tokenPayload.version, '1.3.0') && tokenPayload.associationToken !== null && tokenPayload.associationToken !== undefined) { gaiaAssociationToken = tokenPayload.associationToken } const userData: UserData = { username: tokenPayload.username, profile: tokenPayload.profile, email: tokenPayload.email, decentralizedID: tokenPayload.iss, identityAddress: getAddressFromDID(tokenPayload.iss), appPrivateKey, coreSessionToken, authResponseToken, hubUrl, gaiaAssociationToken } const profileURL = tokenPayload.profile_url if (!userData.profile && profileURL) { const response = await fetchPrivate(profileURL) if (!response.ok) { // return blank profile if we fail to fetch userData.profile = Object.assign({}, DEFAULT_PROFILE) } else { const responseText = await response.text() const wrappedProfile = JSON.parse(responseText) const profile = extractProfile(wrappedProfile[0].token) userData.profile = profile } } else { userData.profile = tokenPayload.profile } const sessionData = caller.store.getSessionData() sessionData.userData = userData caller.store.setSessionData(sessionData) return userData }