UNPKG

@oraichain/customauth

Version:

CustomAuth login with torus to get user private key

1,441 lines (1,422 loc) 55.2 kB
import _defineProperty from '@babel/runtime/helpers/defineProperty'; import { get, post, enableSentryTracing, generateJsonRPCObject } 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 { serializeError } from 'serialize-error'; import loglevel from 'loglevel'; import { EventEmitter } from 'events'; import jwtDecode from 'jwt-decode'; import { Some, allSettled } from '@oraichain/multifactors.js'; import { keccak256 } from 'web3-utils'; import { register } from '@chaitanyapotti/register-service-worker'; 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" }; const REDIRECT_PARAMS_STORAGE_METHOD = { LOCAL_STORAGE: "localStorage", SESSION_STORAGE: "sessionStorage", SERVER: "server" }; const TORUS_METHOD = { TRIGGER_LOGIN: "triggerLogin" }; var log = loglevel.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, email } = 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(email, 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 = ""; 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; } 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`); } }; const wait = s => new Promise(resolve => { setTimeout(resolve, s * 1000); }); function stringtifyError(error) { return JSON.stringify(serializeError(error)); } class PopupHandler extends EventEmitter { constructor(_ref) { let { url, target, features } = _ref; super(); 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, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { _defineProperty(this, "nonce", randomId()); this.clientId = clientId; 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, typeOfLogin: this.typeOfLogin, redirectToOpener: this.redirectToOpener || false })))); } handleLoginWindow(params) { const verifierWindow = new PopupHandler({ url: this.finalURL, features: params.popupFeatures }); 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) { 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")); }); }); } } class DiscordHandler extends AbstractLoginHandler { constructor(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "RESPONSE_TYPE", "token"); _defineProperty(this, "SCOPE", "identify email"); this.clientId = clientId; 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.discord.com/embed/avatars/${Number(discriminator) % 5}.png` : `https://cdn.discord.com/avatars/${id}/${avatar}.png?size=2048`; return { profileImage, name: `${name}#${discriminator}`, email, verifierId: id, typeOfLogin: this.typeOfLogin }; } } class FacebookHandler extends AbstractLoginHandler { constructor(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "RESPONSE_TYPE", "token"); _defineProperty(this, "SCOPE", "public_profile email"); this.clientId = clientId; 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 || "", verifierId: id, typeOfLogin: this.typeOfLogin }; } } class GoogleHandler extends AbstractLoginHandler { constructor(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "RESPONSE_TYPE", "token id_token"); _defineProperty(this, "SCOPE", "profile email openid"); _defineProperty(this, "PROMPT", "consent select_account"); this.clientId = clientId; 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, verifierId: email.toLowerCase(), typeOfLogin: this.typeOfLogin }; } } let JwtHandler$1 = class JwtHandler extends AbstractLoginHandler { constructor(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "SCOPE", "openid profile email"); _defineProperty(this, "RESPONSE_TYPE", "token id_token"); _defineProperty(this, "PROMPT", "login"); this.clientId = clientId; 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 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), typeOfLogin: this.typeOfLogin }; } catch (error) { // ignore loglevel.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: email, typeOfLogin: this.typeOfLogin }; } throw new Error("Access/id token not available"); } }; 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, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "SCOPE", "openid profile email"); _defineProperty(this, "RESPONSE_TYPE", "token id_token"); _defineProperty(this, "PROMPT", "login"); this.clientId = clientId; 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), 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), 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) { 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, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "RESPONSE_TYPE", "token"); _defineProperty(this, "SCOPE", "identity"); this.clientId = clientId; 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, verifierId: name.toLowerCase(), typeOfLogin: this.typeOfLogin }; } } class TwitchHandler extends AbstractLoginHandler { constructor(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState) { super(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); _defineProperty(this, "RESPONSE_TYPE", "token"); _defineProperty(this, "SCOPE", "user:read:email"); this.clientId = clientId; 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, typeOfLogin: this.typeOfLogin }; } } const createHandler = _ref => { let { clientId, redirect_uri, typeOfLogin, jwtParams, redirectToOpener, uxMode, customState } = _ref; if (!typeOfLogin || !clientId) { throw new Error("Invalid params"); } const { domain, login_hint } = jwtParams || {}; switch (typeOfLogin) { case LOGIN.GOOGLE: return new GoogleHandler(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.FACEBOOK: return new FacebookHandler(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.TWITCH: return new TwitchHandler(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.REDDIT: return new RedditHandler(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.DISCORD: return new DiscordHandler(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); case LOGIN.PASSWORDLESS: if (!domain || !login_hint) throw new Error("Invalid params"); return new JwtHandler(clientId, 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 (!domain) throw new Error("Invalid params"); return new JwtHandler$1(clientId, redirect_uri, typeOfLogin, uxMode, redirectToOpener, jwtParams, customState); 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")); } }); var Network; (function (Network) { Network["DEV"] = "development"; Network["TESTNET"] = "testnet"; Network["MAINNET"] = "mainnet"; Network["STAGING"] = "staging"; Network["PRODUCTION"] = "production"; })(Network || (Network = {})); const NetworkConfig = { [Network.DEV]: { rpc: "https://rpc.testnet.orai.io", lcd: "https://lcd.testnet.orai.io", chainId: "Oraichain-testnet", contract: "orai182z6mxeta4dgaxu6qyuu5fywc3p7cdyz2udphd0nz5vnqkrdhzrscu8zan" }, [Network.STAGING]: { rpc: "https://rpc.testnet.orai.io", lcd: "https://lcd.testnet.orai.io", chainId: "Oraichain-testnet", contract: "orai1j3ynfwl2gv7jujfhjqkwrgfwfsg2jth7fyv2g0ph9twc372ny7gqxhe5pj" }, [Network.TESTNET]: { rpc: "https://rpc.testnet.orai.io", lcd: "https://lcd.testnet.orai.io", chainId: "Oraichain-testnet", contract: "orai1v5hwd3w4dx3628suz3lrhd9hr8ktdgjytu95kfsa0vxxmxj42rtsxv4sdn" }, [Network.MAINNET]: { rpc: "https://rpc.orai.io", lcd: "https://lcd.orai.io", chainId: "Oraichain", contract: "orai1kvu7xclv2uvc5yl0mzgcux0cw40sjur2kksarva84376gq4qnnxqhk2hh5", loadBalancerEndpoint: "https://social-login.orai.io/jrpc" }, [Network.PRODUCTION]: { rpc: "https://rpc.orai.io", lcd: "https://lcd.orai.io", chainId: "Oraichain", contract: "orai1r7qwtfp7uc0jsemc8frnjgwc4gpspxnuhg7gjcv3slzul08gglds65tnrp", loadBalancerEndpoint: "https://executor-multifactor.orai.io/jrpc" } }; const metadataUrl = { [Network.DEV]: "http://127.0.0.1:5051", [Network.STAGING]: "https://metadata.social-login-staging.orai.io", [Network.TESTNET]: "https://metadata.social-login-testnet.orai.io", [Network.MAINNET]: "https://metadata-social-login.orai.io", [Network.PRODUCTION]: "https://metadata-social-login.orai.io" }; const getQueryUrl = (config, params) => `${config.lcd}/cosmwasm/wasm/v1/contract/${config.contract}/smart/${params}`; const query = async (config, input, requestInit, customOpts) => { const param = Buffer.from(JSON.stringify(input)).toString("base64"); const queryUrl = getQueryUrl(config, param); const resp = await get(queryUrl, requestInit, customOpts); return resp.data; }; var WEBSOCKET_CODE_ONCLOSE; (function (WEBSOCKET_CODE_ONCLOSE) { WEBSOCKET_CODE_ONCLOSE[WEBSOCKET_CODE_ONCLOSE["ERROR"] = 4000] = "ERROR"; WEBSOCKET_CODE_ONCLOSE[WEBSOCKET_CODE_ONCLOSE["SUCCESS"] = 3000] = "SUCCESS"; })(WEBSOCKET_CODE_ONCLOSE || (WEBSOCKET_CODE_ONCLOSE = {})); function fromRPCtoWebsocket(url) { const rpcURL = new URL(url); rpcURL.protocol = "wss"; rpcURL.pathname = "/websocket"; return rpcURL.toString(); } const conditionTransform = queryTags => { let condition = ""; Object.keys(queryTags).forEach(key => { condition += condition ? ` AND ${key} = '${queryTags[key]}'` : `AND ${key} = '${queryTags[key]}'`; }); return condition; }; const subscribeTx = async function (url, tags) { let timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 15000; const conditionString = conditionTransform(tags); return new Promise((resolve, reject) => { // Set up the timeout const ws = new WebSocket(url); const timer = setTimeout(() => { reject(new Error(`subscribe timed out after ${timeout} ms`)); ws.send(JSON.stringify({ jrpc: "2.0", method: "unsubscribe", id: "99", params: [] })); return ws.close(); }, timeout); ws.onopen = () => { ws.send(JSON.stringify({ jsonrpc: "2.0", method: "subscribe", params: [`tm.event = 'Tx' ${conditionString}`], id: "1" })); }; ws.onmessage = message => { const data = JSON.parse(message.data); if ("events" in data.result) { var _data$result; const events = data === null || data === void 0 || (_data$result = data.result) === null || _data$result === void 0 ? void 0 : _data$result.events; const keysFromTags = Object.keys(tags); const extractValueFromEvent = keysFromTags.reduce((acc, key) => { acc[key] = events[key]; return acc; }, {}); extractValueFromEvent.txHash = events["tx.hash"][0]; clearTimeout(timer); resolve(extractValueFromEvent); ws.send(JSON.stringify({ jrpc: "2.0", method: "unsubscribe", id: "99", params: [] })); return ws.close(WEBSOCKET_CODE_ONCLOSE.SUCCESS); } }; ws.onerror = errorEvent => { reject(new Error(`WebSocket error ${errorEvent.type}`)); ws.send(JSON.stringify({ jrpc: "2.0", method: "unsubscribe", id: "99", params: [] })); return ws.close(WEBSOCKET_CODE_ONCLOSE.ERROR); }; ws.onclose = event => { clearTimeout(timer); if (event.code !== WEBSOCKET_CODE_ONCLOSE.ERROR && event.code !== WEBSOCKET_CODE_ONCLOSE.SUCCESS) { return reject(new Error(`WebSocket closed unexpectedly with code ${event.code}`)); } }; }); }; const _excluded = ["access_token", "id_token"]; 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] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } class CustomAuth { constructor(_ref) { let { baseUrl, network = Network.MAINNET, enableLogging = true, redirectToOpener = false, redirectPathName = "redirect", uxMode = UX_MODE.POPUP, locationReplaceOnRedirect = false, popupFeatures, multifactors, networkConfig } = _ref; this.isInitialized = false; const baseUri = new URL(baseUrl); this.config = { baseUrl: padUrlString(baseUri), get redirect_uri() { return `${this.baseUrl}${redirectPathName}`; }, redirectToOpener, uxMode, locationReplaceOnRedirect, popupFeatures }; this.multifactors = multifactors; this.networkConfig = networkConfig ? _objectSpread(_objectSpread({}, NetworkConfig[network]), networkConfig) : NetworkConfig[network]; if (enableLogging) log.enableAll();else log.disableAll(); } async enableSentry(sentry) { const { members: nodes } = await this.getContractConfig(); const endpoints = nodes.map(node => node.end_point); enableSentryTracing(sentry, endpoints, ["/get", "/set"]); } async init() { let { skipSw = false, skipInit = false, skipPrefetch = false } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (skipInit) { this.isInitialized = true; return; } if (!skipSw) { const fetchSwResponse = await fetch(`${this.config.baseUrl}sw.js`, { cache: "reload" }); if (fetchSwResponse.ok) { try { await registerServiceWorker(this.config.baseUrl); this.isInitialized = true; return; } catch (error) { log.warn(error); } } else { throw new Error("Service worker is not being served. Please serve it"); } } if (!skipPrefetch) { // Skip the redirect check for firefox if (isFirefox()) { this.isInitialized = true; return; } await this.handlePrefetchRedirectUri(); return; } this.isInitialized = true; } async triggerLogin(args) { const { verifier, typeOfLogin } = args; const { userInfo, loginParams } = await this.getLoginParamsAndUserInfo(args); try { const multifactorsKey = await this.getMultifactorsKey(typeOfLogin, { verifier_id: userInfo === null || userInfo === void 0 ? void 0 : userInfo.verifierId, verifier }, loginParams.idToken || loginParams.accessToken, userInfo === null || userInfo === void 0 ? void 0 : userInfo.extraVerifierParams); return _objectSpread(_objectSpread({}, multifactorsKey), {}, { userInfo: _objectSpread(_objectSpread(_objectSpread({}, userInfo), loginParams), {}, { verifierId: userInfo === null || userInfo === void 0 ? void 0 : userInfo.verifierId }) }); } catch (error) { log.debug(error); throw new Error(`getMultifactorsKey::${stringtifyError(error)}`); } } async triggerLoginMobile(args) { const { verifier, typeOfLogin } = args; const { userInfo, loginParams } = await this.getLoginParamsAndUserInfo(args); try { const { sharesIndexes, shares, thresholdPublicKey } = await this.getMultifactorsKeyMobile(typeOfLogin, { verifier_id: userInfo === null || userInfo === void 0 ? void 0 : userInfo.verifierId, verifier }, loginParams.idToken || loginParams.accessToken, userInfo === null || userInfo === void 0 ? void 0 : userInfo.extraVerifierParams); return { sharesIndexes, shares, userInfo: _objectSpread(_objectSpread({}, userInfo), {}, { verifierId: userInfo === null || userInfo === void 0 ? void 0 : userInfo.verifierId }), thresholdPublicKey }; } catch (error) { throw new Error(`getMultifactorsKeyMobile::${stringtifyError(error)}`); } } /* istanbul ignore next @preserve */ async login(args) { const { typeOfLogin, clientId, jwtParams, hash, queryParameters, customState } = args; let { idToken, accessToken } = args; if (!this.isInitialized) { throw new Error("Not initialized yet"); } const loginHandler = createHandler({ typeOfLogin, clientId, redirect_uri: this.config.redirect_uri, redirectToOpener: this.config.redirectToOpener, jwtParams, uxMode: this.config.uxMode, customState }); let loginParams; if (hash && queryParameters) { const { error, hashParameters, instanceParameters } = handleRedirectParameters(hash, queryParameters); if (error) throw new Error(error); let rest; var _hashParameters = hashParameters; ({ access_token: accessToken, id_token: idToken } = _hashParameters); rest = _objectWithoutProperties(_hashParameters, _excluded); loginParams = _objectSpread(_objectSpread({ accessToken, idToken }, rest), {}, { state: instanceParameters }); } else { loginParams = await loginHandler.handleLoginWindow({ locationReplaceOnRedirect: this.config.locationReplaceOnRedirect, popupFeatures: this.config.popupFeatures }); } const userInfo = await loginHandler.getUserInfo(loginParams); return { loginParams, userInfo }; } async getMultifactorsKey(typeOfLogin, verifierParams, idToken, additionalParams) { const [{ found, verifierId }, { members: nodes }] = await Promise.all([this.lookUpVerifierId(verifierParams.verifier_id, verifierParams.verifier), this.getContractConfig()]); const endpoints = nodes.map(node => node.end_point); if (!found) { let nodeSignatures = await this.getKeyAssignCommitment(idToken, verifierId, verifierParams.verifier, endpoints); nodeSignatures = nodeSignatures.map(i => i.result); await this.assignKey(typeOfLogin, idToken, verifierId, verifierParams.verifier, endpoints, nodeSignatures); } const shares = await this.multifactors.retrieveShares({ typeOfLogin, endpoints, verifierParams: _objectSpread(_objectSpread({}, verifierParams), {}, { verifier_id: verifierId }), idToken, extraParams: additionalParams }); log.debug("multifactors-direct/getMultifactorsKey", { retrieveShares: shares }); return { privateKey: shares.privKey.toString() }; } async getMultifactorsKeyMobile(typeOfLogin, verifierParams, idToken, additionalParams) { const [{ found, verifierId }, { members: nodes }] = await Promise.all([this.lookUpVerifierId(verifierParams.verifier_id, verifierParams.verifier), this.getContractConfig()]); const endpoints = nodes.map(node => node.end_point); if (!found) { let nodeSignatures; nodeSignatures = await this.getKeyAssignCommitment(idToken, verifierId, verifierParams.verifier, endpoints); nodeSignatures = nodeSignatures.map(i => i && i.result); await this.assignKey(typeOfLogin, idToken, verifierId, verifierParams.verifier, endpoints, nodeSignatures); } return this.multifactors.retrieveSharesMobile({ typeOfLogin, endpoints, verifierParams: _objectSpread(_objectSpread({}, verifierParams), {}, { verifier_id: verifierId }), idToken, extraParams: additionalParams }); } async getKeyAssignCommitment(idToken, verifierId, verifier, endpoints) { const commitment = keccak256(idToken).slice(2); const promiseArr = []; for (let i = 0; i < endpoints.length; i += 1) { const p = post(endpoints[i], generateJsonRPCObject("AssignKeyCommitmentRequest", { tokencommitment: commitment, verifier_id: verifierId, verifier })).catch(err => { log.error("🚀 ~ file: login.ts:196 ~ CustomAuth ~ getKeyAssignCommitment ~ err:", err); log.error("AssignKeyCommitmentRequest", err); return undefined; }); promiseArr.push(p); } return Some(promiseArr, resultArr => { const completedRequests = resultArr.filter(x => { if (!x || typeof x !== "object") { return false; } if (x.error) { return false; } return true; }); if (completedRequests.length >= ~~(endpoints.length / 2) + 1) { return Promise.resolve(completedRequests); } return Promise.reject(new Error(`Insufficent nodeSignatures to assign key`)); }); } async assignKey(typeOfLogin, idToken, verifierId, verifier, endpoints, nodeSignatures) { const queryTag = { "wasm.verifier": verifier, "wasm.verify_id": verifierId, "wasm.action": "assign_key", "wasm._contract_address": this.networkConfig.contract }; const webSocketUrl = fromRPCtoWebsocket(this.networkConfig.rpc); const promiseSubcribe = subscribeTx(webSocketUrl, queryTag, 20000); const response = await post(this.networkConfig.loadBalancerEndpoint ? this.networkConfig.loadBalancerEndpoint : endpoints[3], generateJsonRPCObject("AssignKeyRequest", { typeOfLogin, idtoken: idToken, verifier_id: verifierId, verifier, nodesignatures: nodeSignatures })).catch(_error => undefined); if (!response || typeof response !== "object") { throw new Error("assign key fail"); } const attributes = await promiseSubcribe; return { txHash: attributes.txHash }; } async mapNewVerifierId(mapNewVerifierIdParams) { try { const [{ members: nodes }, { found, verifierId: newVerifierId }] = await Promise.all([this.getContractConfig(), this.lookUpVerifierId(mapNewVerifierIdParams.newVerifierId, mapNewVerifierIdParams.newVerifier)]); if (found) throw new Error("New verifier id is already exist in system"); const endpoints = nodes.map(node => node.end_point); const queryTag = { "wasm.verifier": mapNewVerifierIdParams.verifier, "wasm.verifier_id": keccak256(mapNewVerifierIdParams.verifierId), "wasm.new_verifier": mapNewVerifierIdParams.newVerifier, "wasm.new_verifier_id": newVerifierId, "wasm.action": "map_exist_verifier_id_to_another_permit", "wasm._contract_address": this.networkConfig.contract }; const webSocketUrl = fromRPCtoWebsocket(this.networkConfig.rpc); const promiseSubcribe = subscribeTx(webSocketUrl, queryTag, 25000); const nodeSignatures = await this.getMapNewVerifierIdCommitment(_objectSpread(_objectSpread({}, mapNewVerifierIdParams), {}, { newVerifierId: newVerifierId }), endpoints); if (nodeSignatures.length < ~~(endpoints.length / 2) + 1) { throw new Error("Insufficent nodeSignatures to mapping"); } const response = await post(this.networkConfig.loadBalancerEndpoint ? this.networkConfig.loadBalancerEndpoint : e