aladinnetwork-blockstack
Version:
The Aladin Javascript library for authentication, identity, and storage.
381 lines (357 loc) • 14.4 kB
text/typescript
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
}