UNPKG

@oraichain/customauth

Version:

CustomAuth login with torus to get user private key

546 lines (488 loc) 18.4 kB
import Multifactors, { allSettled, JRPCResponse, KeyAssignCommitmentRequestResult, MapNewVeririerIdCommitmentRequestResult, RetrieveSharesResponse, Some, } from "@oraichain/multifactors.js"; import { enableSentryTracing, generateJsonRPCObject, post } from "@toruslabs/http-helpers"; import { keccak256 } from "web3-utils"; import createHandler from "./handlers/HandlerFactory"; import { CustomAuthArgs, ExtraParams, ILoginHandler, InitParams, LoginWindowResponse, MapNewVerifierParams, Sentry, SingleLoginParams, MultifactorsGenericObject, MultifactorsKey, MultifactorsLoginResponse, MultifactorsVerifierResponse, } from "./handlers/interfaces"; import { registerServiceWorker } from "./registerServiceWorker"; import { INetworkConfig, Member, Network, NetworkConfig, query } from "./utils/blockchain"; import { UX_MODE, UX_MODE_TYPE } from "./utils/enums"; import { handleRedirectParameters, isFirefox, padUrlString, stringtifyError } from "./utils/helpers"; import log from "./utils/loglevel"; import { fromRPCtoWebsocket, subscribeTx } from "./utils/ws"; class CustomAuth { isInitialized: boolean; config: { baseUrl: string; redirectToOpener: boolean; redirect_uri: string; uxMode: UX_MODE_TYPE; locationReplaceOnRedirect: boolean; popupFeatures: string; }; multifactors: Multifactors; networkConfig: INetworkConfig; constructor({ baseUrl, network = Network.MAINNET, enableLogging = true, redirectToOpener = false, redirectPathName = "redirect", uxMode = UX_MODE.POPUP, locationReplaceOnRedirect = false, popupFeatures, multifactors, networkConfig, }: CustomAuthArgs) { 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 ? { ...NetworkConfig[network], ...networkConfig } : NetworkConfig[network]; if (enableLogging) log.enableAll(); else log.disableAll(); } async enableSentry(sentry: Sentry) { const { members: nodes } = await this.getContractConfig(); const endpoints = nodes.map((node: Member) => node.end_point); enableSentryTracing(sentry, endpoints, ["/get", "/set"]); } async init({ skipSw = false, skipInit = false, skipPrefetch = false }: InitParams = {}): Promise<void> { 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: SingleLoginParams): Promise<MultifactorsLoginResponse> { const { verifier, typeOfLogin } = args; const { userInfo, loginParams } = await this.getLoginParamsAndUserInfo(args); try { const multifactorsKey = await this.getMultifactorsKey( typeOfLogin, { verifier_id: userInfo?.verifierId, verifier }, loginParams.idToken || loginParams.accessToken, userInfo?.extraVerifierParams ); return { ...multifactorsKey, userInfo: { ...userInfo, ...loginParams, verifierId: userInfo?.verifierId, }, }; } catch (error) { log.debug(error); throw new Error(`getMultifactorsKey::${stringtifyError(error)}`); } } async triggerLoginMobile( args: SingleLoginParams ): Promise<{ sharesIndexes: Buffer[]; shares: Buffer[]; userInfo?: MultifactorsVerifierResponse; thresholdPublicKey: string }> { const { verifier, typeOfLogin } = args; const { userInfo, loginParams } = await this.getLoginParamsAndUserInfo(args); try { const { sharesIndexes, shares, thresholdPublicKey } = await this.getMultifactorsKeyMobile( typeOfLogin, { verifier_id: userInfo?.verifierId, verifier }, loginParams.idToken || loginParams.accessToken, userInfo?.extraVerifierParams ); return { sharesIndexes, shares, userInfo: { ...userInfo, verifierId: userInfo?.verifierId, }, thresholdPublicKey, }; } catch (error) { throw new Error(`getMultifactorsKeyMobile::${stringtifyError(error)}`); } } /* istanbul ignore next @preserve */ async login(args: SingleLoginParams): Promise<{ loginParams: LoginWindowResponse; userInfo: MultifactorsVerifierResponse }> { const { typeOfLogin, clientId, jwtParams, hash, queryParameters, customState } = args; let { idToken, accessToken } = args; if (!this.isInitialized) { throw new Error("Not initialized yet"); } const loginHandler: ILoginHandler = createHandler({ typeOfLogin, clientId, redirect_uri: this.config.redirect_uri, redirectToOpener: this.config.redirectToOpener, jwtParams, uxMode: this.config.uxMode, customState, }); let loginParams: LoginWindowResponse; if (hash && queryParameters) { const { error, hashParameters, instanceParameters } = handleRedirectParameters(hash, queryParameters); if (error) throw new Error(error); let rest: Partial<MultifactorsGenericObject>; ({ access_token: accessToken, id_token: idToken, ...rest } = hashParameters); loginParams = { 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: string, verifierParams: { verifier_id: string; verifier: string }, idToken: string, additionalParams?: ExtraParams ): Promise<MultifactorsKey> { const [{ found, verifierId }, { members: nodes }] = await Promise.all([ this.lookUpVerifierId(verifierParams.verifier_id, verifierParams.verifier), this.getContractConfig(), ]); const endpoints = nodes.map((node: Member) => node.end_point); if (!found) { let nodeSignatures = await this.getKeyAssignCommitment(idToken, verifierId, verifierParams.verifier, endpoints); nodeSignatures = nodeSignatures.map((i: any) => i.result); await this.assignKey(typeOfLogin, idToken, verifierId, verifierParams.verifier, endpoints, nodeSignatures); } const shares = await this.multifactors.retrieveShares({ typeOfLogin, endpoints, verifierParams: { ...verifierParams, verifier_id: verifierId }, idToken, extraParams: additionalParams, }); log.debug("multifactors-direct/getMultifactorsKey", { retrieveShares: shares }); return { privateKey: shares.privKey.toString(), }; } async getMultifactorsKeyMobile( typeOfLogin: string, verifierParams: { verifier_id: string; verifier: string }, idToken: string, additionalParams?: ExtraParams ): Promise<RetrieveSharesResponse> { const [{ found, verifierId }, { members: nodes }] = await Promise.all([ this.lookUpVerifierId(verifierParams.verifier_id, verifierParams.verifier), this.getContractConfig(), ]); const endpoints = nodes.map((node: Member) => node.end_point); if (!found) { let nodeSignatures: any; nodeSignatures = await this.getKeyAssignCommitment(idToken, verifierId, verifierParams.verifier, endpoints); nodeSignatures = nodeSignatures.map((i: JRPCResponse<KeyAssignCommitmentRequestResult>) => i && i.result); await this.assignKey(typeOfLogin, idToken, verifierId, verifierParams.verifier, endpoints, nodeSignatures); } return this.multifactors.retrieveSharesMobile({ typeOfLogin, endpoints, verifierParams: { ...verifierParams, verifier_id: verifierId }, idToken, extraParams: additionalParams, }); } async getKeyAssignCommitment( idToken: string, verifierId: string, verifier: string, endpoints: string[] ): Promise<(void | JRPCResponse<KeyAssignCommitmentRequestResult>)[]> { const commitment = keccak256(idToken).slice(2); const promiseArr = []; for (let i = 0; i < endpoints.length; i += 1) { const p = post<JRPCResponse<KeyAssignCommitmentRequestResult>>( 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<void | JRPCResponse<KeyAssignCommitmentRequestResult>, (void | JRPCResponse<KeyAssignCommitmentRequestResult>)[]>( promiseArr, (resultArr: (void | JRPCResponse<KeyAssignCommitmentRequestResult>)[]) => { 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: string, idToken: string, verifierId: string, verifier: string, endpoints: string[], nodeSignatures: (void | JRPCResponse<KeyAssignCommitmentRequestResult>)[] ) { 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<JRPCResponse<{ status: string }>>( 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: MapNewVerifierParams) { 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: Member) => 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({ ...mapNewVerifierIdParams, newVerifierId: newVerifierId }, endpoints); if (nodeSignatures.length < ~~(endpoints.length / 2) + 1) { throw new Error("Insufficent nodeSignatures to mapping"); } const response = await post<JRPCResponse<{ status: string }>>( this.networkConfig.loadBalancerEndpoint ? this.networkConfig.loadBalancerEndpoint : endpoints[3], generateJsonRPCObject("MappingNewVerifierId", { signature: mapNewVerifierIdParams.signature, nodesignatures: nodeSignatures, verifier: mapNewVerifierIdParams.verifier, verifier_id: keccak256(mapNewVerifierIdParams.verifierId), new_verifier: mapNewVerifierIdParams.newVerifier, new_verifier_id: newVerifierId, }) ).catch((_error) => undefined); if (!response || typeof response !== "object") { throw new Error("Map new verifier id fail"); } const attributes = await promiseSubcribe; return { txHash: attributes.txHash }; } catch (error) { throw new Error(`Map new verifier id fail:${error.message}`); } } async getMapNewVerifierIdCommitment( mapNewVerifierIdParams: Readonly<MapNewVerifierParams>, endpoints: string[] ): Promise<MapNewVeririerIdCommitmentRequestResult[]> { const commitmentResponses = ( await Promise.allSettled<JRPCResponse<MapNewVeririerIdCommitmentRequestResult>>( endpoints.map((endpoint: string) => { return post<JRPCResponse<MapNewVeririerIdCommitmentRequestResult>>( endpoint, generateJsonRPCObject("MappingNewVerifierIdCommitment", { typeOfLogin: mapNewVerifierIdParams.typeOfLogin, new_verifier: mapNewVerifierIdParams.newVerifier, new_verifier_id: mapNewVerifierIdParams.newVerifierId, id_token: mapNewVerifierIdParams.idToken, }) ); }) ) ) .filter((x: PromiseSettledResult<JRPCResponse<MapNewVeririerIdCommitmentRequestResult>>) => x.status === "fulfilled") .map((x: PromiseFulfilledResult<JRPCResponse<MapNewVeririerIdCommitmentRequestResult>>) => x.value); const nodeSignatures = commitmentResponses .map((response: JRPCResponse<MapNewVeririerIdCommitmentRequestResult>) => { if (!response || typeof response !== "object") { return undefined; } if (response.error) { return undefined; } return response.result; }) .filter((x: MapNewVeririerIdCommitmentRequestResult | undefined) => x); return nodeSignatures; } async lookUpVerifierId(verifierId: string, verifier: string) { try { const [resp, resp2] = await allSettled([ query(this.networkConfig, { verifier_id_info: { verifier_id: verifierId, verifier, }, }), query(this.networkConfig, { verifier_id_info: { verifier_id: keccak256(verifierId), verifier, }, }), ]); if (resp.status === "fulfilled") { return { found: true, verifierId }; } else if (resp2.status === "fulfilled") { return { found: true, verifierId: keccak256(verifierId) }; } return { found: false, verifierId: keccak256(verifierId) }; } catch (error) { throw new Error("Look up verifier id fail"); } } async getContractConfig() { return query(this.networkConfig, { config: {}, }); } getPostboxKeyFrom1OutOf1(privKey: string, nonce: string): string { return this.multifactors.getPostboxKeyFrom1OutOf1(privKey, nonce); } async getLoginParamsAndUserInfo(args: SingleLoginParams) { const { idToken } = args; let loginParams: LoginWindowResponse; let userInfo: MultifactorsVerifierResponse; let payload: any; if (!idToken) { ({ loginParams, userInfo } = await this.login(args)); log.info({ loginParams, userInfo }); } else { loginParams = { idToken }; } try { payload = JSON.parse(Buffer.from(loginParams.idToken.split(".")[1], "base64").toString()) as MultifactorsGenericObject; } catch (error) { throw new Error("Invalid idToken:" + error.message); } userInfo = { verifierId: payload.email || payload.phone_number || payload.sub, typeOfLogin: args.typeOfLogin, email: payload.email, name: payload.name, profileImage: payload.profileImage, }; return { loginParams, userInfo }; } /* istanbul ignore next @preserve */ async handlePrefetchRedirectUri(): Promise<void> { if (!document) return Promise.resolve(); return new Promise((resolve, reject) => { const redirectHtml = document.createElement("link"); redirectHtml.href = this.config.redirect_uri; if (window.location.origin !== new URL(this.config.redirect_uri).origin) redirectHtml.crossOrigin = "anonymous"; redirectHtml.type = "text/html"; redirectHtml.rel = "prefetch"; const resolveFn = () => { this.isInitialized = true; resolve(); }; try { if (redirectHtml.relList && redirectHtml.relList.supports) { if (redirectHtml.relList.supports("prefetch")) { redirectHtml.onload = resolveFn; redirectHtml.onerror = () => { reject(new Error(`Please serve redirect.html present in serviceworker folder of this package on ${this.config.redirect_uri}`)); }; document.head.appendChild(redirectHtml); } else { // Link prefetch is not supported. pass through resolveFn(); } } else { // Link prefetch is not detectable. pass through resolveFn(); } } catch (err) { resolveFn(); } }); } } export default CustomAuth;