UNPKG

@upbond/customauth

Version:

CustomAuth login with torus to get user private key

1,425 lines (1,403 loc) 68 kB
import _defineProperty from '@babel/runtime/helpers/defineProperty'; import { get, post } from '@toruslabs/http-helpers'; import deepmerge from 'lodash.merge'; import _objectWithoutProperties from '@babel/runtime/helpers/objectWithoutProperties'; import { BroadcastChannel } from '@toruslabs/broadcast-channel'; import Bowser from 'bowser'; import log$1 from 'loglevel'; import { EventEmitter } from 'events'; import jwtDecode from 'jwt-decode'; import { TORUS_NETWORK } from '@toruslabs/constants'; import { NodeDetailManager } from '@toruslabs/fetch-node-details'; import Torus, { keccak256 as keccak256$1 } from '@toruslabs/torus.js'; import { UserManager } from 'oidc-client'; import { register } from '@chaitanyapotti/register-service-worker'; import { getPublic, sign } from '@toruslabs/eccrypto'; import { keccak256, encryptData, decryptData } from '@toruslabs/metadata-helpers'; const LOGIN = { GOOGLE: "google", FACEBOOK: "facebook", REDDIT: "reddit", DISCORD: "discord", TWITCH: "twitch", APPLE: "apple", GITHUB: "github", LINKEDIN: "linkedin", TWITTER: "twitter", WEIBO: "weibo", LINE: "line", EMAIL_PASSWORD: "email_password", PASSWORDLESS: "passwordless", JWT: "jwt", WEBAUTHN: "webauthn" }; const AGGREGATE_VERIFIER = { SINGLE_VERIFIER_ID: "single_id_verifier" // AND_AGGREGATE_VERIFIER : "and_aggregate_verifier", // OR_AGGREGATE_VERIFIER : "or_aggregate_verifier", }; const UX_MODE = { POPUP: "popup", REDIRECT: "redirect" }; const REDIRECT_PARAMS_STORAGE_METHOD = { LOCAL_STORAGE: "localStorage", SESSION_STORAGE: "sessionStorage", SERVER: "server" }; const TORUS_METHOD = { TRIGGER_LOGIN: "triggerLogin", TRIGGER_AGGREGATE_LOGIN: "triggerAggregateLogin", TRIGGER_AGGREGATE_HYBRID_LOGIN: "triggerHybridAggregateLogin" }; const SENTRY_TXNS = { FETCH_NODE_DETAILS: "fetchNodeDetails", PUB_ADDRESS_LOOKUP: "pubAddressLookup", FETCH_SHARES: "fetchShares" }; var log = log$1.getLogger("customauth"); function eventToPromise(emitter) { return new Promise((resolve, reject) => { const handler = ev => { const { error = "", data } = ev; emitter.removeEventListener("message", handler); if (error) return reject(new Error(error)); return resolve(data); }; emitter.addEventListener("message", handler); }); } // These are the connection names used by auth0 const loginToConnectionMap = { [LOGIN.APPLE]: "apple", [LOGIN.GITHUB]: "github", [LOGIN.LINKEDIN]: "linkedin", [LOGIN.TWITTER]: "twitter", [LOGIN.WEIBO]: "weibo", [LOGIN.LINE]: "line", [LOGIN.EMAIL_PASSWORD]: "Username-Password-Authentication", [LOGIN.PASSWORDLESS]: "email" }; const padUrlString = url => url.href.endsWith("/") ? url.href : `${url.href}/`; /** * Returns a random number. Don't use for cryptographic purposes. * @returns a random number */ const randomId = () => Math.random().toString(36).slice(2); const broadcastChannelOptions = { // type: 'localstorage', // (optional) enforce a type, oneOf['native', 'idb', 'localstorage', 'node'] webWorkerSupport: false // (optional) set this to false if you know that your channel will never be used in a WebWorker (increases performance) }; function caseSensitiveField(field, isCaseSensitive) { return isCaseSensitive ? field : field.toLowerCase(); } const getVerifierId = function (userInfo, typeOfLogin, verifierIdField) { let isVerifierIdCaseSensitive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; const { name, sub } = userInfo; if (verifierIdField) return caseSensitiveField(userInfo[verifierIdField], isVerifierIdCaseSensitive); switch (typeOfLogin) { case LOGIN.PASSWORDLESS: case LOGIN.EMAIL_PASSWORD: return caseSensitiveField(name, isVerifierIdCaseSensitive); case LOGIN.WEIBO: case LOGIN.GITHUB: case LOGIN.TWITTER: case LOGIN.APPLE: case LOGIN.LINKEDIN: case LOGIN.LINE: case LOGIN.JWT: return caseSensitiveField(sub, isVerifierIdCaseSensitive); default: throw new Error("Invalid login type"); } }; const handleRedirectParameters = (hash, queryParameters) => { const hashParameters = hash.split("&").reduce((result, item) => { const [part0, part1] = item.split("="); result[part0] = part1; return result; }, {}); log.info(hashParameters, queryParameters); let instanceParameters = {}; let error = ""; hashParameters.state = localStorage.getItem("state"); if (Object.keys(hashParameters).length > 0 && hashParameters.state) { instanceParameters = JSON.parse(atob(decodeURIComponent(decodeURIComponent(hashParameters.state)))) || {}; error = hashParameters.error_description || hashParameters.error || error; } else if (Object.keys(queryParameters).length > 0 && queryParameters.state) { instanceParameters = JSON.parse(atob(decodeURIComponent(decodeURIComponent(queryParameters.state)))) || {}; if (queryParameters.error) error = queryParameters.error; } localStorage.removeItem("state"); return { error, instanceParameters, hashParameters }; }; function storageAvailable(type) { let storage; try { storage = window[type]; const x = "__storage_test__"; storage.setItem(x, x); storage.removeItem(x); return true; } catch (e) { return e && ( // everything except Firefox e.code === 22 || // Firefox e.code === 1014 || // test name field too, because code might not be present // everything except Firefox e.name === "QuotaExceededError" || // Firefox e.name === "NS_ERROR_DOM_QUOTA_REACHED") && // acknowledge QuotaExceededError only if there's something already stored storage && storage.length !== 0; } } function getPopupFeatures() { // Fixes dual-screen position Most browsers Firefox const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX; const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY; const w = 1200; const h = 700; const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : window.screen.width; const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : window.screen.height; const systemZoom = 1; // No reliable estimate const left = Math.abs((width - w) / 2 / systemZoom + dualScreenLeft); const top = Math.abs((height - h) / 2 / systemZoom + dualScreenTop); const features = `titlebar=0,toolbar=0,status=0,location=0,menubar=0,height=${h / systemZoom},width=${w / systemZoom},top=${top},left=${left}`; return features; } const isFirefox = () => { var _window; return ((_window = window) === null || _window === void 0 || (_window = _window.navigator) === null || _window === void 0 ? void 0 : _window.userAgent.toLowerCase().indexOf("firefox")) > -1 || false; }; function constructURL(params) { const { baseURL, query, hash } = params; const url = new URL(baseURL); if (query) { Object.keys(query).forEach(key => { url.searchParams.append(key, query[key]); }); } if (hash) { const h = new URL(constructURL({ baseURL, query: hash })).searchParams.toString(); url.hash = h; } return url.toString(); } function are3PCSupported() { var _navigator; const browserInfo = Bowser.parse(navigator.userAgent); log.info(JSON.stringify(browserInfo), "current browser info"); let thirdPartyCookieSupport = true; // brave if ((_navigator = navigator) !== null && _navigator !== void 0 && _navigator.brave) { thirdPartyCookieSupport = false; } // All webkit & gecko engine instances use itp (intelligent tracking prevention - // https://webkit.org/tracking-prevention/#intelligent-tracking-prevention-itp) if (browserInfo.engine.name === Bowser.ENGINE_MAP.WebKit || browserInfo.engine.name === Bowser.ENGINE_MAP.Gecko) { thirdPartyCookieSupport = false; } return thirdPartyCookieSupport; } const validateAndConstructUrl = domain => { try { const url = new URL(decodeURIComponent(domain)); return url; } catch (error) { throw new Error(`${(error === null || error === void 0 ? void 0 : error.message) || ""}, Note: Your jwt domain: (i.e ${domain}) must have http:// or https:// prefix`); } }; class PopupHandler extends EventEmitter { constructor(_ref) { let { url, target, features } = _ref; super(); _defineProperty(this, "url", void 0); _defineProperty(this, "target", void 0); _defineProperty(this, "features", void 0); _defineProperty(this, "window", void 0); _defineProperty(this, "windowTimer", void 0); _defineProperty(this, "iClosedWindow", void 0); this.url = url; this.target = target || "_blank"; this.features = features || getPopupFeatures(); this.window = undefined; this.windowTimer = undefined; this.iClosedWindow = false; this._setupTimer(); } _setupTimer() { this.windowTimer = Number(setInterval(() => { if (this.window && this.window.closed) { clearInterval(this.windowTimer); if (!this.iClosedWindow) { this.emit("close"); } this.iClosedWindow = false; this.window = undefined; } if (this.window === undefined) clearInterval(this.windowTimer); }, 500)); } open() { var _this$window; this.window = window.open(this.url.href, this.target, this.features); if ((_this$window = this.window) !== null && _this$window !== void 0 && _this$window.focus) this.window.focus(); return Promise.resolve(); } close() { this.iClosedWindow = true; if (this.window) this.window.close(); } redirect(locationReplaceOnRedirect) { if (locationReplaceOnRedirect) { window.location.replace(this.url.href); } else { window.location.href = this.url.href; } } } const _excluded$2 = ["access_token", "id_token"]; function ownKeys$2(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread$2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$2(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$2(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } class AbstractLoginHandler { // Not using object constructor because of this issue // https://github.com/microsoft/TypeScript/issues/5326 constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "nonce", randomId()); _defineProperty(this, "finalURL", void 0); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; } get state() { return encodeURIComponent(window.btoa(JSON.stringify(_objectSpread$2(_objectSpread$2({}, this.customState || {}), {}, { instanceId: this.nonce, verifier: this.verifier, typeOfLogin: this.typeOfLogin, redirectToOpener: this.redirectToOpener || false })))); } handleLoginWindow(params) { const verifierWindow = new PopupHandler({ url: this.finalURL, features: params.popupFeatures }); if (this.uxMode === UX_MODE.REDIRECT) { verifierWindow.redirect(params.locationReplaceOnRedirect); } else { return new Promise((resolve, reject) => { let bc; const handleData = async ev => { try { const { error, data } = ev; const _ref = data || {}, { instanceParams, hashParams: { access_token: accessToken, id_token: idToken } } = _ref, rest = _objectWithoutProperties(_ref.hashParams, _excluded$2); if (error) { log.error(ev); reject(new Error(`Error: ${error}. Info: ${JSON.stringify(ev.data || {})}`)); return; } if (ev.data && instanceParams.verifier === this.verifier) { log.info(ev.data); if (!this.redirectToOpener && bc) await bc.postMessage({ success: true }); resolve(_objectSpread$2(_objectSpread$2({ accessToken, idToken: idToken || "" }, rest), {}, { // State has to be last here otherwise it will be overwritten state: instanceParams })); } } catch (error) { log.error(error); reject(error); } }; if (!this.redirectToOpener) { bc = new BroadcastChannel(`redirect_channel_${this.nonce}`, broadcastChannelOptions); bc.addEventListener("message", async ev => { await handleData(ev); bc.close(); verifierWindow.close(); }); } else { const postMessageEventHandler = async postMessageEvent => { if (!postMessageEvent.data) return; const ev = postMessageEvent.data; if (ev.channel !== `redirect_channel_${this.nonce}`) return; window.removeEventListener("message", postMessageEventHandler); handleData(ev); verifierWindow.close(); }; window.addEventListener("message", postMessageEventHandler); } verifierWindow.open(); verifierWindow.once("close", () => { if (bc) bc.close(); reject(new Error("user closed popup")); }); }); } return null; } } class DiscordHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "RESPONSE_TYPE", "token"); _defineProperty(this, "SCOPE", "identify email"); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.setFinalUrl(); } setFinalUrl() { const finalUrl = new URL("https://discord.com/api/oauth2/authorize"); const clonedParams = JSON.parse(JSON.stringify(this.jwtParams || {})); const finalJwtParams = deepmerge({ state: this.state, response_type: this.RESPONSE_TYPE, client_id: this.clientId, redirect_uri: this.redirect_uri, scope: this.SCOPE }, clonedParams); Object.keys(finalJwtParams).forEach(key => { if (finalJwtParams[key]) finalUrl.searchParams.append(key, finalJwtParams[key]); }); this.finalURL = finalUrl; } async getUserInfo(params) { const { accessToken } = params; const userInfo = await get("https://discord.com/api/users/@me", { headers: { Authorization: `Bearer ${accessToken}` } }); const { id, avatar, email = "", username: name = "", discriminator = "" } = userInfo; const profileImage = avatar === null ? `https://cdn.discordapp.com/embed/avatars/${Number(discriminator) % 5}.png` : `https://cdn.discordapp.com/avatars/${id}/${avatar}.png?size=2048`; return { profileImage, name: `${name}#${discriminator}`, email, verifierId: id, verifier: this.verifier, typeOfLogin: this.typeOfLogin }; } } class FacebookHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "RESPONSE_TYPE", "token"); _defineProperty(this, "SCOPE", "public_profile email"); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.setFinalUrl(); } setFinalUrl() { const finalUrl = new URL("https://www.facebook.com/v15.0/dialog/oauth"); const clonedParams = JSON.parse(JSON.stringify(this.jwtParams || {})); const finalJwtParams = deepmerge({ state: this.state, response_type: this.RESPONSE_TYPE, client_id: this.clientId, redirect_uri: this.redirect_uri, scope: this.SCOPE }, clonedParams); Object.keys(finalJwtParams).forEach(key => { if (finalJwtParams[key]) finalUrl.searchParams.append(key, finalJwtParams[key]); }); this.finalURL = finalUrl; } async getUserInfo(params) { const { accessToken } = params; const userInfo = await get("https://graph.facebook.com/me?fields=name,email,picture.type(large)", { headers: { Authorization: `Bearer ${accessToken}` } }); const { name = "", id, picture, email = "" } = userInfo; return { email, name, profileImage: picture.data.url || "", verifier: this.verifier, verifierId: id, typeOfLogin: this.typeOfLogin }; } } class GoogleHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "RESPONSE_TYPE", "token id_token"); _defineProperty(this, "SCOPE", "profile email openid"); _defineProperty(this, "PROMPT", "consent select_account"); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.setFinalUrl(); } setFinalUrl() { const finalUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth"); const clonedParams = JSON.parse(JSON.stringify(this.jwtParams || {})); const finalJwtParams = deepmerge({ state: this.state, response_type: this.RESPONSE_TYPE, client_id: this.clientId, prompt: this.PROMPT, redirect_uri: this.redirect_uri, scope: this.SCOPE, nonce: this.nonce }, clonedParams); Object.keys(finalJwtParams).forEach(key => { if (finalJwtParams[key]) finalUrl.searchParams.append(key, finalJwtParams[key]); }); this.finalURL = finalUrl; } async getUserInfo(params) { const { accessToken } = params; const userInfo = await get("https://www.googleapis.com/userinfo/v2/me", { headers: { Authorization: `Bearer ${accessToken}` } }); const { picture: profileImage = "", email = "", name = "" } = userInfo; return { email, name, profileImage, verifier: this.verifier, verifierId: email.toLowerCase(), typeOfLogin: this.typeOfLogin }; } } let JwtHandler$1 = class JwtHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "SCOPE", "openid profile email"); _defineProperty(this, "RESPONSE_TYPE", "token id_token"); _defineProperty(this, "PROMPT", "login"); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.setFinalUrl(); } setFinalUrl() { localStorage.setItem("state", this.state); const { domain } = this.jwtParams; const finalUrl = validateAndConstructUrl(domain); finalUrl.pathname += finalUrl.pathname.endsWith("/") ? "authorize" : "/authorize"; const clonedParams = JSON.parse(JSON.stringify(this.jwtParams)); delete clonedParams.domain; const finalJwtParams = deepmerge({ state: this.state, response_type: this.RESPONSE_TYPE, client_id: this.clientId, prompt: this.PROMPT, redirect_uri: this.redirect_uri, scope: this.SCOPE, connection: loginToConnectionMap[this.typeOfLogin], nonce: this.nonce }, clonedParams); Object.keys(finalJwtParams).forEach(key => { if (finalJwtParams[key]) finalUrl.searchParams.append(key, finalJwtParams[key]); }); this.finalURL = finalUrl; } async getUserInfo(params) { const { idToken, accessToken } = params; const { domain, verifierIdField, isVerifierIdCaseSensitive, user_info_route = "userinfo" } = this.jwtParams; if (accessToken) { try { const domainUrl = new URL(domain); const userInfo = await get(`${padUrlString(domainUrl)}${user_info_route}`, { headers: { Authorization: `Bearer ${accessToken}` } }); const { picture, name, email } = userInfo; return { email, name, profileImage: picture, verifierId: getVerifierId(userInfo, this.typeOfLogin, verifierIdField, isVerifierIdCaseSensitive), verifier: this.verifier, typeOfLogin: this.typeOfLogin }; } catch (error) { // ignore log$1.warn(error, "Unable to get userinfo from endpoint"); } } if (idToken) { const decodedToken = jwtDecode(idToken); const { name, email, picture } = decodedToken; return { profileImage: picture, name, email, verifierId: getVerifierId(decodedToken, this.typeOfLogin, verifierIdField, isVerifierIdCaseSensitive), verifier: this.verifier, typeOfLogin: this.typeOfLogin }; } throw new Error("Access/id token not available"); } }; class MockLoginHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.setFinalUrl(); } setFinalUrl() { const clonedParams = JSON.parse(JSON.stringify(this.jwtParams)); delete clonedParams.domain; const finalJwtParams = deepmerge({ state: this.state, client_id: this.clientId, nonce: this.nonce }, clonedParams); this.finalURL = new URL(constructURL({ baseURL: this.redirect_uri, query: null, hash: finalJwtParams })); } async getUserInfo(params) { const { idToken, accessToken } = params; const { domain, verifierIdField, isVerifierIdCaseSensitive, user_info_route = "userinfo" } = this.jwtParams; if (accessToken) { try { const domainUrl = new URL(domain); const userInfo = await get(`${padUrlString(domainUrl)}${user_info_route}`, { headers: { Authorization: `Bearer ${accessToken}` } }); const { picture, name, email } = userInfo; return { email, name, profileImage: picture, verifierId: getVerifierId(userInfo, this.typeOfLogin, verifierIdField, isVerifierIdCaseSensitive), verifier: this.verifier, typeOfLogin: this.typeOfLogin }; } catch (error) { // ignore log$1.warn(error, "Unable to get userinfo from endpoint"); } } if (idToken) { const decodedToken = jwtDecode(idToken); const { name, email, picture } = decodedToken; return { profileImage: picture, name, email, verifierId: getVerifierId(decodedToken, this.typeOfLogin, verifierIdField, isVerifierIdCaseSensitive), verifier: this.verifier, typeOfLogin: this.typeOfLogin }; } throw new Error("Access/id token not available"); } handleLoginWindow(params) { const { id_token: idToken, access_token: accessToken } = this.jwtParams; const verifierWindow = new PopupHandler({ url: this.finalURL, features: params.popupFeatures }); if (this.uxMode === UX_MODE.REDIRECT) { verifierWindow.redirect(params.locationReplaceOnRedirect); } else { return Promise.resolve({ state: {}, idToken, accessToken }); } return null; } } const _excluded$1 = ["access_token", "id_token"]; function ownKeys$1(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread$1(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$1(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$1(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } class JwtHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "SCOPE", "openid profile email"); _defineProperty(this, "RESPONSE_TYPE", "token id_token"); _defineProperty(this, "PROMPT", "login"); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.setFinalUrl(); } setFinalUrl() { const { domain } = this.jwtParams; const domainUrl = validateAndConstructUrl(domain); domainUrl.pathname = "/passwordless/start"; this.finalURL = domainUrl; } async getUserInfo(params) { const { idToken, accessToken } = params; const { domain, verifierIdField, isVerifierIdCaseSensitive } = this.jwtParams; try { const domainUrl = new URL(domain); const userInfo = await get(`${padUrlString(domainUrl)}userinfo`, { headers: { Authorization: `Bearer ${accessToken}` } }); const { picture, name, email } = userInfo; return { email, name, profileImage: picture, verifierId: getVerifierId(userInfo, this.typeOfLogin, verifierIdField, isVerifierIdCaseSensitive), verifier: this.verifier, typeOfLogin: this.typeOfLogin }; } catch (error) { log.error(error); const decodedToken = jwtDecode(idToken); const { name, email, picture } = decodedToken; return { profileImage: picture, name, email, verifierId: getVerifierId(decodedToken, this.typeOfLogin, verifierIdField, isVerifierIdCaseSensitive), verifier: this.verifier, typeOfLogin: this.typeOfLogin }; } } handleLoginWindow() { return new Promise((resolve, reject) => { if (this.redirectToOpener) { reject(new Error("Cannot use redirect to opener for passwordless")); return; } const handleData = ev => { try { const { error, data } = ev; const _ref = data || {}, { instanceParams, hashParams: { access_token: accessToken, id_token: idToken } } = _ref, rest = _objectWithoutProperties(_ref.hashParams, _excluded$1); if (error) { log.error(ev.error); reject(new Error(error)); return; } if (ev.data && instanceParams.verifier === this.verifier) { log.info(ev.data); resolve(_objectSpread$1(_objectSpread$1({ accessToken, idToken: idToken || "" }, rest), {}, { state: instanceParams })); } } catch (error) { log.error(error); reject(error); } }; const bc = new BroadcastChannel(`redirect_channel_${this.nonce}`, broadcastChannelOptions); bc.addEventListener("message", async ev => { handleData(ev); bc.close(); }); try { const { connection = "email", login_hint } = this.jwtParams; const finalJwtParams = deepmerge({ client_id: this.clientId, connection, email: connection === "email" ? login_hint : undefined, phone_number: connection === "sms" ? login_hint : undefined, send: "link", authParams: { scope: this.SCOPE, state: this.state, response_type: this.RESPONSE_TYPE, redirect_uri: this.redirect_uri, nonce: this.nonce, prompt: this.PROMPT } }, { authParams: this.jwtParams }); // using stringify and parse to remove undefined params // This method is only resolved when the user clicks the email link post(this.finalURL.href, JSON.parse(JSON.stringify(finalJwtParams))).then(response => { log.info("posted", response); return undefined; }).catch(error => { log.error(error); reject(error); }); } catch (error) { log.error(error); reject(error); } }); } } class RedditHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "RESPONSE_TYPE", "token"); _defineProperty(this, "SCOPE", "identity"); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.setFinalUrl(); } setFinalUrl() { const finalUrl = new URL(`https://www.reddit.com/api/v1/authorize${window.innerWidth < 600 ? ".compact" : ""}`); const clonedParams = JSON.parse(JSON.stringify(this.jwtParams || {})); const finalJwtParams = deepmerge({ state: this.state, response_type: this.RESPONSE_TYPE, client_id: this.clientId, redirect_uri: this.redirect_uri, scope: this.SCOPE }, clonedParams); Object.keys(finalJwtParams).forEach(key => { if (finalJwtParams[key]) finalUrl.searchParams.append(key, finalJwtParams[key]); }); this.finalURL = finalUrl; } async getUserInfo(params) { const { accessToken } = params; const userInfo = await get("https://oauth.reddit.com/api/v1/me", { headers: { Authorization: `Bearer ${accessToken}` } }); const { icon_img: profileImage = "", name = "" } = userInfo; return { email: "", name, profileImage: profileImage.split("?").length > 0 ? profileImage.split("?")[0] : profileImage, verifier: this.verifier, verifierId: name.toLowerCase(), typeOfLogin: this.typeOfLogin }; } } class TwitchHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "RESPONSE_TYPE", "token"); _defineProperty(this, "SCOPE", "user:read:email"); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.setFinalUrl(); } setFinalUrl() { const finalUrl = new URL("https://id.twitch.tv/oauth2/authorize"); const clonedParams = JSON.parse(JSON.stringify(this.jwtParams || {})); const finalJwtParams = deepmerge({ state: this.state, response_type: this.RESPONSE_TYPE, client_id: this.clientId, redirect_uri: this.redirect_uri, scope: this.SCOPE, force_verify: true }, clonedParams); Object.keys(finalJwtParams).forEach(key => { if (finalJwtParams[key]) finalUrl.searchParams.append(key, finalJwtParams[key]); }); this.finalURL = finalUrl; } async getUserInfo(params) { const { accessToken } = params; const userInfo = await get("https://api.twitch.tv/helix/users", { headers: { Authorization: `Bearer ${accessToken}`, "Client-ID": this.clientId } }); const [{ profile_image_url: profileImage = "", display_name: name = "", email = "", id: verifierId }] = userInfo.data || []; return { profileImage, name, email, verifierId, verifier: this.verifier, typeOfLogin: this.typeOfLogin }; } } const WEBAUTHN_LOOKUP_SERVER = "https://api.webauthn.openlogin.com"; class WebAuthnHandler extends AbstractLoginHandler { constructor(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState, registerOnly) { super(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "clientId", void 0); _defineProperty(this, "verifier", void 0); _defineProperty(this, "redirect_uri", void 0); _defineProperty(this, "typeOfLogin", void 0); _defineProperty(this, "uxMode", void 0); _defineProperty(this, "redirectToOpener", void 0); _defineProperty(this, "jwtParams", void 0); _defineProperty(this, "customState", void 0); _defineProperty(this, "registerOnly", void 0); this.clientId = clientId; this.verifier = verifier; this.redirect_uri = redirect_uri; this.typeOfLogin = typeOfLogin; this.uxMode = uxMode; this.redirectToOpener = redirectToOpener; this.jwtParams = jwtParams; this.customState = customState; this.registerOnly = registerOnly; this.setFinalUrl(); } setFinalUrl() { const finalUrl = new URL("https://webauthn.openlogin.com"); const clonedParams = JSON.parse(JSON.stringify(this.jwtParams || {})); const finalJwtParams = deepmerge({ register_only: !!this.registerOnly, state: this.state, client_id: this.clientId, redirect_uri: this.redirect_uri }, clonedParams); Object.keys(finalJwtParams).forEach(key => { if (finalJwtParams[key]) finalUrl.searchParams.append(key, finalJwtParams[key]); }); this.finalURL = finalUrl; } async getUserInfo(parameters) { const { idToken, ref, extraParamsPassed, extraParams } = parameters; let verifierId; let signature; let clientDataJSON; let authenticatorData; let publicKey; let challenge; let rpOrigin; let credId; let transports; if (extraParamsPassed === "true") { log.debug("extraParamsPassed is true, using extraParams passed through hashParams"); try { ({ verifier_id: verifierId, signature, clientDataJSON, authenticatorData, publicKey, challenge, rpOrigin, credId, transports } = JSON.parse(atob(extraParams))); } catch (error) { log.warn("unable to parse extraParams", error); ({ verifier_id: verifierId, signature, clientDataJSON, authenticatorData, publicKey, challenge, rpOrigin, credId, transports } = await get(`${WEBAUTHN_LOOKUP_SERVER}/signature/fetch/${idToken}`)); } } else { log.debug("extraParamsPassed is false, using extraParams passed through bridge server"); ({ verifier_id: verifierId, signature, clientDataJSON, authenticatorData, publicKey, challenge, rpOrigin, credId, transports } = await get(`${WEBAUTHN_LOOKUP_SERVER}/signature/fetch/${idToken}`)); } if (signature !== idToken) { throw new Error("idtoken should be equal to signature"); } return { email: "", name: "WebAuthn Login", profileImage: "", verifier: this.verifier, verifierId, typeOfLogin: this.typeOfLogin, ref, registerOnly: this.registerOnly, extraVerifierParams: { signature, clientDataJSON, authenticatorData, publicKey, challenge, rpOrigin, credId, transports } }; } } const createHandler = _ref => { let { clientId, redirect_uri, typeOfLogin, verifier, jwtParams, redirectToOpener, uxMode, customState, registerOnly } = _ref; if (!verifier || !typeOfLogin || !clientId) { throw new Error("Invalid params"); } const { domain, login_hint, id_token, access_token } = jwtParams || {}; switch (typeOfLogin) { case LOGIN.GOOGLE: return new GoogleHandler(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.FACEBOOK: return new FacebookHandler(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.TWITCH: return new TwitchHandler(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.REDDIT: return new RedditHandler(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.DISCORD: return new DiscordHandler(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.PASSWORDLESS: if (!domain || !login_hint) throw new Error("Invalid params"); return new JwtHandler(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.APPLE: case LOGIN.GITHUB: case LOGIN.LINKEDIN: case LOGIN.TWITTER: case LOGIN.WEIBO: case LOGIN.LINE: case LOGIN.EMAIL_PASSWORD: case LOGIN.JWT: if (id_token || access_token) { return new MockLoginHandler(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); } if (!domain) throw new Error("Invalid params"); return new JwtHandler$1(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.WEBAUTHN: return new WebAuthnHandler(clientId, verifier, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState, registerOnly); default: throw new Error("Invalid login type"); } }; const registerServiceWorker = baseUrl => new Promise((resolve, reject) => { const swUrl = `${baseUrl}sw.js`; if ("serviceWorker" in window.navigator) { // if swIntegrity is not calculated register(swUrl, { ready() { log.info("App is being served from cache by a service worker.\n For more details, visit https://goo.gl/AFskqB"); resolve(undefined); }, registered() { log.info("Service worker has been registered."); resolve(undefined); }, cached() { log.info("Content has been cached for offline use."); resolve(undefined); }, updatefound() { log.info("New content is downloading."); }, updated() { log.info("New content is available; please refresh."); }, offline() { log.info("No internet connection found. App is running in offline mode."); reject(new Error("App is offline")); }, error(error) { log.error("Error during service worker registration:", error); reject(error); } }); } else { reject(new Error("Service workers are not supported")); } }); class SentryHandler { constructor(sentry) { _defineProperty(this, "sentry", null); this.sentry = sentry; } startTransaction(context) { if (this.sentry) { return this.sentry.startTransaction(context); } } finishTransaction(tx) { if (tx) { tx.finish(); } } } class StorageHelper { constructor(serverUrl) { _defineProperty(this, "currentStorageMethod", REDIRECT_PARAMS_STORAGE_METHOD.LOCAL_STORAGE); _defineProperty(this, "isInitialized", false); _defineProperty(this, "storageServerUrl", "https://broadcast-server.tor.us"); this.storageServerUrl = serverUrl; } init() { const support = are3PCSupported(); const localStorageAvailable = storageAvailable(REDIRECT_PARAMS_STORAGE_METHOD.LOCAL_STORAGE); if (support && localStorageAvailable) { // use local storage as default for storing stuff this.currentStorageMethod = REDIRECT_PARAMS_STORAGE_METHOD.LOCAL_STORAGE; } else { // use server store as default for storing stuff this.currentStorageMethod = REDIRECT_PARAMS_STORAGE_METHOD.SERVER; } this.isInitialized = true; } async storeLoginDetails(params, scope) { if (!this.isInitialized) throw new Error("StorageHelper is not initialized"); if (this.currentStorageMethod === REDIRECT_PARAMS_STORAGE_METHOD.SERVER) { const privKey = keccak256(Buffer.from(scope, "utf8")); const privKeyHex = privKey.toString("hex"); const publicKeyHex = getPublic(privKey).toString("hex"); const encData = await encryptData(privKeyHex, params); const signature = (await sign(privKey, keccak256(Buffer.from(encData, "utf8")))).toString("hex"); await post(`${this.storageServerUrl}/store/set`, { key: publicKeyHex, data: encData, signature }); } else { window.localStorage.setItem(`torus_login_${scope}`, JSON.stringify(params)); } } async retrieveLoginDetails(scope) { if (!this.isInitialized) throw new Error("StorageHelper is not initialized"); if (this.currentStorageMethod === REDIRECT_PARAMS_STORAGE_METHOD.SERVER) { const privKey = keccak256(Buffer.from(scope, "utf8")); const privKeyHex = privKey.toString("hex"); const publicKeyHex = getPublic(privKey).toString("hex"); try { const encData = await get(`${this.storageServerUrl}/store/get?key=${publicKeyHex}`); if (encData.message) { const loginDetails = await decryptData(privKeyHex, encData.message); return loginDetails; } } catch (error) { if (error.status === 404) { log.warn(error, "Session likely expired"); } else { throw error; } } } const loginDetails = window.localStorage.getItem(`torus_login_${scope}`); return JSON.parse(loginDetails || "{}"); } clearLoginDetailsStorage(scope) { if (!this.isInitialized) throw new Error("StorageHelper is not initialized"); if (this.currentStorageMethod === REDIRECT_PARAMS_STORAGE_METHOD.LOCAL_STORAGE) { window.localStorage.removeItem(`torus_login_${scope}`); } // No need to clear server details cause they auto expire and scope is never re-used for different login attempts } clearOrphanedLoginDetails() { if (!this.isInitialized) throw new Error("StorageHelper is not initialized"); if (this.currentStorageMethod === REDIRECT_PARAMS_STORAGE_METHOD.LOCAL_STORAGE) { const allStorageKeys = Object.keys(window.localStorage); allStorageKeys.forEach(key => { if (key.startsWith("torus_login_")) { window.localStorage.removeItem(key); } }); } // No need to clear server details cause they auto expire and scope is never re-used for different login attempts } } const _excluded = ["access_token", "id_token"], _excluded2 = ["access_token", "id_token"], _excluded3 = ["args", "method"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? ar