@oraichain/customauth
Version:
CustomAuth login with torus to get user private key
546 lines (488 loc) • 18.4 kB
text/typescript
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;