diff --git a/package-lock.json b/package-lock.json index b6ce6da..71aa778 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,13 @@ "license": "ISC", "dependencies": { "@toruslabs/constants": "^15.0.0", + "@toruslabs/fetch-node-details": "^15.0.0", "@toruslabs/http-helpers": "^8.1.1", "@toruslabs/session-manager": "^4.0.2", + "@toruslabs/torus.js": "^16.0.0", "@web3auth/auth": "^10.5.0", "buffer": "^6.0.3", + "jwt-decode": "^4.0.0", "lodash.clonedeep": "^4.5.0", "lodash.merge": "^4.6.2", "lodash.unionby": "^4.8.0", @@ -12212,7 +12215,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index b91b7fd..1272587 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,13 @@ ], "dependencies": { "@toruslabs/constants": "^15.0.0", + "@toruslabs/fetch-node-details": "^15.0.0", "@toruslabs/http-helpers": "^8.1.1", "@toruslabs/session-manager": "^4.0.2", + "@toruslabs/torus.js": "^16.0.0", "@web3auth/auth": "^10.5.0", "buffer": "^6.0.3", + "jwt-decode": "^4.0.0", "lodash.clonedeep": "^4.5.0", "lodash.merge": "^4.6.2", "lodash.unionby": "^4.8.0", diff --git a/src/Web3Auth.ts b/src/Web3Auth.ts index f5d3aea..4929a51 100644 --- a/src/Web3Auth.ts +++ b/src/Web3Auth.ts @@ -1,8 +1,12 @@ +import { NodeDetailManager } from "@toruslabs/fetch-node-details"; import { SessionManager } from "@toruslabs/session-manager"; +import { keccak256, Torus, TorusKey, VerifierParams } from "@toruslabs/torus.js"; import { AUTH_ACTIONS, + AUTH_CONNECTION, AuthConnectionConfig, type AuthSessionConfig, + AuthUserInfo, type BaseLoginParams, BUILD_ENV, jsonToBase64, @@ -11,6 +15,7 @@ import { type WEB3AUTH_NETWORK_TYPE, } from "@web3auth/auth"; import { type IProvider } from "@web3auth/base"; +import { jwtDecode, JwtPayload } from "jwt-decode"; import clonedeep from "lodash.clonedeep"; import merge from "lodash.merge"; import unionBy from "lodash.unionby"; @@ -19,10 +24,11 @@ import URI from "urijs"; import { CHAIN_NAMESPACES } from "./constants"; import { InitializationError, LoginError, RequestError } from "./errors"; -import KeyStore from "./session/KeyStore"; +import KeyStore, { KEYSTORE_KEYS } from "./session/KeyStore"; import { EncryptedStorage } from "./types/IEncryptedStorage"; import { SecureStore } from "./types/IExpoSecureStore"; import { + AggregateVerifierParams, AuthSessionData, ChainConfig, IWeb3Auth, @@ -31,6 +37,7 @@ import { SdkLoginParams, SmartAccountConfig, State, + SubVerifierInfo, WalletLoginParams, } from "./types/interface"; import { IWebBrowser } from "./types/IWebBrowser"; @@ -59,6 +66,10 @@ class Web3Auth implements IWeb3Auth { private projectConfig?: ProjectConfigResponse; + private fetchNodeDetails: NodeDetailManager; + + private torusUtils: Torus; + constructor(webBrowser: IWebBrowser, storage: SecureStore | EncryptedStorage, options: SdkInitParams) { if (!options.clientId) throw InitializationError.invalidParams("clientId is required"); if (!options.privateKeyProvider) { @@ -125,6 +136,13 @@ class Web3Auth implements IWeb3Auth { this.webBrowser = webBrowser; this.keyStore = new KeyStore(storage); this.privateKeyProvider = options.privateKeyProvider; + this.fetchNodeDetails = new NodeDetailManager({ network: this.options.network }); + this.torusUtils = new Torus({ + network: this.options.network, + clientId: this.options.clientId, + serverTimeOffset: 0, + enableOneKey: true, + }); if (options.accountAbstractionProvider) { this.accountAbstractionProvider = options.accountAbstractionProvider; } @@ -165,11 +183,6 @@ class Web3Auth implements IWeb3Auth { ) { throw InitializationError.invalidParams("provider should have chainConfig and should be initialized with chainId and chainNamespace"); } - this.sessionManager = new SessionManager({ - sessionServerBaseUrl: this.options.storageServerUrl, - sessionTime: this.options.sessionTime, - sessionNamespace: this.options.sessionNamespace, - }); try { this.projectConfig = await fetchProjectConfig(this.options.clientId, this.options.network, this.options.buildEnv); @@ -193,8 +206,18 @@ class Web3Auth implements IWeb3Auth { if (typeof key_export_enabled === "boolean") { this.privateKeyProvider.setKeyExportFlag(key_export_enabled); } - const sessionId = await this.keyStore.get("sessionId"); + + const isSFAValue = await this.keyStore.get(KEYSTORE_KEYS.IS_SFA); + const isSFA = isSFAValue === "true"; + + this.sessionManager = new SessionManager({ + sessionServerBaseUrl: this.options.storageServerUrl, + sessionTime: this.options.sessionTime, + sessionNamespace: isSFA ? "sfa" : undefined, + sessionId, + }); + if (sessionId) { this.sessionManager.sessionId = sessionId; const data = await this.authorizeSession(); @@ -209,76 +232,13 @@ class Web3Auth implements IWeb3Auth { } } else { await this.keyStore.remove("sessionId"); + await this.keyStore.remove(KEYSTORE_KEYS.IS_SFA); this.updateState({}); } } this.ready = true; } - async login(loginParams: SdkLoginParams): Promise { - if (!this.ready) throw InitializationError.notInitialized("Please call init first."); - if (!this.options.redirectUrl) throw InitializationError.invalidParams("redirectUrl is required"); - if (!loginParams.authConnection) throw InitializationError.invalidParams("authConnection is required"); - - // check for share - if (this.options.authConnectionConfig) { - const authConnectionConfigItem = this.options.authConnectionConfig[0]; - if (authConnectionConfigItem) { - const share = await this.keyStore.get(authConnectionConfigItem.authConnectionId); - if (share) { - loginParams.dappShare = share; - } - } - } - - const dataObject: AuthSessionConfig = { - actionType: AUTH_ACTIONS.LOGIN, - options: this.options, - params: loginParams, - }; - - const result = await this.authHandler(`${this.baseUrl}/start`, dataObject); - - if (result.type !== "success" || !result.url) { - log.error(`[Web3Auth] login flow failed with error type ${result.type}`); - throw LoginError.loginFailed(`login flow failed with error type ${result.type}`); - } - - const { sessionId, sessionNamespace, error } = getHashQueryParams(result.url); - if (error || !sessionId) { - throw LoginError.loginFailed(error || "SessionId is missing"); - } - - if (sessionId) { - await this.keyStore.set("sessionId", sessionId); - this.sessionManager.sessionId = sessionId; - this.sessionManager.sessionNamespace = sessionNamespace || ""; - } - - const sessionData = await this.authorizeSession(); - - if (!sessionData || Object.keys(sessionData).length === 0) { - throw LoginError.loginFailed("Session data is missing"); - } - - if (sessionData.userInfo?.dappShare.length > 0) { - const verifier = sessionData.userInfo?.groupedAuthConnectionId || sessionData.userInfo?.authConnectionId; - await this.keyStore.set(verifier, sessionData.userInfo?.dappShare); - } - - this.updateState(sessionData); - - const finalPrivKey = this.getFinalPrivKey(); - if (!finalPrivKey) throw LoginError.loginFailed("final private key not found"); - await this.privateKeyProvider.setupProvider(finalPrivKey); - // setup aa provider after private key provider is setup - if (this.accountAbstractionProvider) { - await this.accountAbstractionProvider.setupProvider(this.privateKeyProvider); - } - - return this.provider; - } - async logout(): Promise { if (!this.sessionManager.sessionId) { throw LoginError.userNotLoggedIn(); @@ -287,9 +247,11 @@ class Web3Auth implements IWeb3Auth { await this.sessionManager.invalidateSession(); await this.keyStore.remove("sessionId"); + await this.keyStore.remove(KEYSTORE_KEYS.IS_SFA); if (currentUserInfo.authConnectionId && currentUserInfo.dappShare.length > 0) { - await this.keyStore.remove(currentUserInfo.authConnectionId); + const verifier = currentUserInfo.groupedAuthConnectionId || currentUserInfo.authConnectionId; + await this.keyStore.remove(verifier); } this.updateState({ @@ -363,10 +325,14 @@ class Web3Auth implements IWeb3Auth { await this.createLoginSession(loginId, dataObject); const { sessionId } = this.sessionManager; + const isSFAValue = await this.keyStore.get(KEYSTORE_KEYS.IS_SFA); + const isSFA = isSFAValue === "true"; + const configParams: WalletLoginParams = { loginId, sessionId, platform: "react-native", + sessionNamespace: isSFA ? "sfa" : undefined, }; const loginUrl = constructURL({ @@ -538,6 +504,218 @@ class Web3Auth implements IWeb3Auth { throw LoginError.userNotLoggedIn(); } + public async connectTo(loginParams: SdkLoginParams): Promise { + const isSFA = !!loginParams.idToken; + + // recreate session manager with sfa option + this.sessionManager = new SessionManager({ + sessionServerBaseUrl: this.options.storageServerUrl, + sessionTime: this.options.sessionTime, + sessionNamespace: isSFA ? "sfa" : undefined, + }); + + if (isSFA) { + await this.keyStore.set(KEYSTORE_KEYS.IS_SFA, "true"); + if (loginParams.groupedAuthConnectionId) { + const aggregateLoginParams: SdkLoginParams = { + authConnection: AUTH_CONNECTION.CUSTOM, + authConnectionId: loginParams.groupedAuthConnectionId, + idToken: loginParams.idToken, + }; + const subVerifierInfoArray: SubVerifierInfo[] = [ + { + verifier: loginParams.authConnectionId, + idToken: loginParams.idToken, + }, + ]; + await this.connect(aggregateLoginParams, subVerifierInfoArray); + } else { + await this.connect(loginParams); + } + } else { + await this.login(loginParams); + } + + const finalPrivKey = this.getFinalPrivKey(); + if (!finalPrivKey) throw LoginError.loginFailed("final private key not found"); + await this.privateKeyProvider.setupProvider(finalPrivKey); + // setup aa provider after private key provider is setup + if (this.accountAbstractionProvider) { + await this.accountAbstractionProvider.setupProvider(this.privateKeyProvider); + } + + return this.provider; + } + + /** + * Handle user PNP login. + * @param loginParams - The login parameters. + * @returns The connected user information. + */ + private async login(loginParams: SdkLoginParams) { + if (!this.ready) throw InitializationError.notInitialized("Please call init first."); + if (!this.options.redirectUrl) throw InitializationError.invalidParams("redirectUrl is required"); + if (!loginParams.authConnection) throw InitializationError.invalidParams("authConnection is required"); + + // check for share + if (this.options.authConnectionConfig) { + const authConnectionConfigItem = this.options.authConnectionConfig[0]; + const share = await this.keyStore.get(authConnectionConfigItem.authConnectionId); + if (authConnectionConfigItem) { + if (share) { + loginParams.dappShare = share; + } + } + } + + const dataObject: AuthSessionConfig = { + actionType: AUTH_ACTIONS.LOGIN, + options: this.options, + params: loginParams, + }; + + const result = await this.authHandler(`${this.baseUrl}/start`, dataObject); + + if (result.type !== "success" || !result.url) { + log.error(`[Web3Auth] login flow failed with error type ${result.type}`); + throw LoginError.loginFailed(`login flow failed with error type ${result.type}`); + } + + const { sessionId, sessionNamespace, error } = getHashQueryParams(result.url); + if (error || !sessionId) { + throw LoginError.loginFailed(error || "SessionId is missing"); + } + + if (sessionId) { + await this.keyStore.set("sessionId", sessionId); + this.sessionManager.sessionId = sessionId; + this.sessionManager.sessionNamespace = sessionNamespace || ""; + } + + const sessionData = await this.authorizeSession(); + + if (!sessionData || Object.keys(sessionData).length === 0) { + throw LoginError.loginFailed("Session data is missing"); + } + + if (sessionData.userInfo?.dappShare.length > 0) { + const verifier = sessionData.userInfo?.groupedAuthConnectionId || sessionData.userInfo?.authConnectionId; + await this.keyStore.set(verifier, sessionData.userInfo?.dappShare); + } + + this.updateState(sessionData); + } + + /** + * Handle user SFA login. + * @param loginParams - The login parameters. + * @param subVerifierInfoArray - The sub-verifier information array. + * @returns The connected user information. + */ + private async connect(loginParams: SdkLoginParams, subVerifierInfoArray?: SubVerifierInfo[]) { + const torusKey = await this.getTorusKey(loginParams, subVerifierInfoArray); + const privateKey = torusKey.finalKeyData?.privKey ?? torusKey.oAuthKeyData?.privKey; + const decodedUserInfo = jwtDecode( + loginParams.idToken + ); + const userInfo: AuthUserInfo = { + email: decodedUserInfo.email, + name: decodedUserInfo.name ?? decodedUserInfo.nickname, + profileImage: decodedUserInfo.picture, + userId: decodedUserInfo.user_id, + authConnectionId: loginParams.authConnectionId, + authConnection: AUTH_CONNECTION.CUSTOM, + groupedAuthConnectionId: loginParams.groupedAuthConnectionId, + oAuthIdToken: loginParams.idToken, + }; + + const signatures = this.getSessionSignatures(torusKey.sessionData); + + const authSessionData: AuthSessionData = { + privKey: privateKey, + userInfo, + signatures, + }; + const sessionId = SessionManager.generateRandomSessionKey(); + this.sessionManager.sessionId = sessionId; + await this.sessionManager.createSession(authSessionData); + await this.keyStore.set("sessionId", sessionId); + + this.updateState(authSessionData); + } + + private async getTorusKey(loginParams: SdkLoginParams, subVerifierInfoArray?: SubVerifierInfo[]) { + const userId = this.getUserIdFromJWT(loginParams.idToken); + const { torusNodeEndpoints, torusNodePub, torusIndexes } = await this.fetchNodeDetails.getNodeDetails({ + verifier: loginParams.authConnectionId, + verifierId: userId, + }); + + let retrieveSharesResponse: TorusKey; + + if (subVerifierInfoArray && subVerifierInfoArray.length > 0) { + const aggregateIdTokenSeeds: string[] = []; + const subVerifierIds: string[] = []; + const verifyParams: { verifier_id: string; idtoken: string }[] = []; + + for (const subVerifierInfo of subVerifierInfoArray) { + const subVerifierId = this.getUserIdFromJWT(subVerifierInfo.idToken); + subVerifierIds.push(subVerifierId); + aggregateIdTokenSeeds.push(subVerifierInfo.idToken); + verifyParams.push({ verifier_id: userId, idtoken: subVerifierInfo.idToken }); + } + + aggregateIdTokenSeeds.sort(); + + const verifierParams: AggregateVerifierParams = { + verifier_id: userId, + verify_params: verifyParams, + sub_verifier_ids: subVerifierIds, + }; + + const separator = String.fromCharCode(29); + const joined = aggregateIdTokenSeeds.join(separator); + const aggregateIdToken = keccak256(Buffer.from(joined, "utf8")).replace(/^0x/, ""); + + retrieveSharesResponse = await this.torusUtils.retrieveShares({ + endpoints: torusNodeEndpoints, + nodePubkeys: torusNodePub, + indexes: torusIndexes, + verifier: loginParams.authConnectionId, + verifierParams: verifierParams, + idToken: aggregateIdToken, + }); + } else { + const verifierParams: VerifierParams = { + verifier_id: userId, + }; + retrieveSharesResponse = await this.torusUtils.retrieveShares({ + endpoints: torusNodeEndpoints, + nodePubkeys: torusNodePub, + indexes: torusIndexes, + verifier: loginParams.authConnectionId, + verifierParams: verifierParams, + idToken: loginParams.idToken, + }); + } + + const isUpgraded = retrieveSharesResponse.metadata?.upgraded; + if (isUpgraded) { + throw LoginError.mfaAlreadyEnabled(); + } + + return retrieveSharesResponse; + } + + private getSessionSignatures(sessionData: TorusKey["sessionData"]): string[] { + return sessionData.sessionTokenData.filter((i) => Boolean(i)).map((session) => JSON.stringify({ data: session.token, sig: session.signature })); + } + + private getUserIdFromJWT(token: string): string { + const decoded = jwtDecode(token); + return decoded["user_id"]; + } + private updateState(newState: State) { this.state = { ...newState }; } @@ -547,7 +725,6 @@ class Web3Auth implements IWeb3Auth { const loginSessionMgr = new SessionManager({ sessionServerBaseUrl: this.options.storageServerUrl, - sessionNamespace: this.options.sessionNamespace, sessionTime: timeout, // each login key must be used with 10 mins (might be used at the end of popup redirect) sessionId: loginId, }); @@ -563,7 +740,6 @@ class Web3Auth implements IWeb3Auth { const configParams: BaseLoginParams = { loginId, - sessionNamespace: this.options.sessionNamespace, storageServerUrl: this.options.storageServerUrl, }; diff --git a/src/session/KeyStore.ts b/src/session/KeyStore.ts index 516da23..6a893df 100644 --- a/src/session/KeyStore.ts +++ b/src/session/KeyStore.ts @@ -1,6 +1,10 @@ import { EncryptedStorage } from "../types/IEncryptedStorage"; import { SecureStore } from "../types/IExpoSecureStore"; +export const KEYSTORE_KEYS = { + IS_SFA: "sfa_storage_is_sfa", +}; + export default class KeyStore { storage: SecureStore | EncryptedStorage; diff --git a/src/types/interface.ts b/src/types/interface.ts index e1a402b..57748cb 100644 --- a/src/types/interface.ts +++ b/src/types/interface.ts @@ -30,13 +30,15 @@ type SdkSpecificInitParams = { accountAbstractionProvider?: IBaseProvider; }; -export type SdkInitParams = Omit & +export type SdkInitParams = Omit & Required> & { defaultChainId?: string; walletServicesConfig?: WalletServicesConfig; }; -export type SdkLoginParams = Omit; +export type SdkLoginParams = Omit & { + idToken?: string; +}; // export type SdkLogoutParams = Partial & Partial; @@ -66,7 +68,7 @@ export interface IWeb3Auth { provider: IProvider | null; connected: boolean; init: () => Promise; - login: (params: SdkLoginParams) => Promise; + connectTo: (params: SdkLoginParams) => Promise; logout: () => Promise; userInfo: () => State["userInfo"]; enableMFA: () => Promise; @@ -83,6 +85,7 @@ export type WalletLoginParams = { params: unknown[]; }; platform: string; + sessionNamespace?: string; }; export enum ChainNamespace { @@ -171,4 +174,15 @@ export type ButtonPositionType = "bottom-left" | "bottom-right" | "top-left" | " export type DefaultPortfolioType = "token" | "nft"; +export type SubVerifierInfo = { + verifier: string; + idToken: string; +}; + +export type AggregateVerifierParams = { + verify_params: { verifier_id: string; idtoken: string }[]; + sub_verifier_ids: string[]; + verifier_id: string; +}; + export { AUTH_CONNECTION, BUILD_ENV, LANGUAGES, MFA_FACTOR, MFA_LEVELS, SUPPORTED_KEY_CURVES, THEME_MODES, WEB3AUTH_NETWORK };