From 6102a69dbd978ade6e60c1e378248bfe752f04ca Mon Sep 17 00:00:00 2001 From: boomshakar Date: Wed, 26 Aug 2026 16:30:10 +0100 Subject: [PATCH 1/2] fix: preserve manage MFA extra login options --- src/core/auth.ts | 1 + test/auth.test.ts | 122 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 test/auth.test.ts diff --git a/src/core/auth.ts b/src/core/auth.ts index 23485f42..b97f25e7 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -342,6 +342,7 @@ export class Auth { authConnectionId: this.state.userInfo.authConnectionId, groupedAuthConnectionId: this.state.userInfo.groupedAuthConnectionId, extraLoginOptions: { + ...params.extraLoginOptions, login_hint: this.state.userInfo.userId, }, appState: jsonToBase64({ loginId, recordId }), diff --git a/test/auth.test.ts b/test/auth.test.ts new file mode 100644 index 00000000..2aa216dc --- /dev/null +++ b/test/auth.test.ts @@ -0,0 +1,122 @@ +import { AUTH_CONNECTION, UX_MODE } from "@toruslabs/customauth"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { Auth } from "../src/core/auth"; +import { AUTH_ACTIONS, AuthRequestPayload, BUILD_ENV, LoginParams, SDK_MODE, WEB3AUTH_NETWORK } from "../src/utils"; + +type AuthInternals = { + storeAuthPayload: (loginId: string, payload: AuthRequestPayload, timeout?: number, skipAwait?: boolean) => Promise; +}; + +describe("Auth.manageMFA", () => { + let auth: Auth; + let storedPayload: AuthRequestPayload | undefined; + + const manageMFA = async (params: Partial = {}) => { + await auth.manageMFA(params); + expect(storedPayload).toBeDefined(); + return storedPayload as AuthRequestPayload; + }; + + beforeEach(() => { + storedPayload = undefined; + vi.stubGlobal("window", { + location: { + origin: "https://app.example", + pathname: "/settings/security", + }, + open: vi.fn(), + }); + + auth = new Auth({ + clientId: "synthetic-sdk-client-id", + network: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET, + buildEnv: BUILD_ENV.TESTING, + }); + auth.state = { + sessionId: "synthetic-session-id", + userInfo: { + authConnection: AUTH_CONNECTION.CUSTOM, + authConnectionId: "synthetic-auth-connection-id", + groupedAuthConnectionId: "synthetic-grouped-auth-connection-id", + userId: "synthetic-user-id", + isMfaEnabled: true, + }, + }; + + vi.spyOn(auth, "refreshSession").mockResolvedValue(); + vi.spyOn(auth, "getAccessToken").mockResolvedValue("synthetic-access-token"); + vi.spyOn(auth as unknown as AuthInternals, "storeAuthPayload").mockImplementation(async (_loginId, payload) => { + storedPayload = payload; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("preserves custom JWT credentials in extraLoginOptions", async () => { + const payload = await manageMFA({ + extraLoginOptions: { + client_id: "synthetic-custom-client-id", + id_token: "synthetic-custom-id-token", + }, + }); + + expect(payload.params.extraLoginOptions).toEqual({ + client_id: "synthetic-custom-client-id", + id_token: "synthetic-custom-id-token", + login_hint: "synthetic-user-id", + }); + }); + + it("uses the authenticated user's login_hint instead of a caller-provided value", async () => { + const payload = await manageMFA({ + extraLoginOptions: { + login_hint: "synthetic-caller-user-id", + }, + }); + + expect(payload.params.extraLoginOptions?.login_hint).toBe("synthetic-user-id"); + }); + + it("supports omitted extraLoginOptions", async () => { + const payload = await manageMFA(); + + expect(payload.params.extraLoginOptions).toEqual({ + login_hint: "synthetic-user-id", + }); + }); + + it("retains the existing manage MFA payload fields", async () => { + const payload = await manageMFA({ + dappUrl: "https://app.example/custom-return", + loginSource: "synthetic-settings-page", + }); + + expect(payload).toEqual({ + actionType: AUTH_ACTIONS.MANAGE_MFA, + options: { + ...auth.options, + uxMode: UX_MODE.REDIRECT, + sdkMode: SDK_MODE.DEFAULT, + redirectUrl: `${auth.options.dashboardUrl}/wallet/account`, + }, + params: { + dappUrl: "https://app.example/custom-return", + loginSource: "synthetic-settings-page", + authConnection: AUTH_CONNECTION.CUSTOM, + authConnectionId: "synthetic-auth-connection-id", + groupedAuthConnectionId: "synthetic-grouped-auth-connection-id", + extraLoginOptions: { + login_hint: "synthetic-user-id", + }, + appState: expect.any(String), + }, + sessionId: "synthetic-session-id", + accessToken: "synthetic-access-token", + }); + expect(window.open).toHaveBeenCalledWith(expect.stringContaining("/start#b64Params="), "_blank"); + }); +}); From 9a0396ef922323f450783637812b1238d7d31b0b Mon Sep 17 00:00:00 2001 From: boomshakar Date: Wed, 26 Aug 2026 16:44:45 +0100 Subject: [PATCH 2/2] fix: preserve extra login options when enabling MFA --- src/core/auth.ts | 1 + test/auth.test.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/core/auth.ts b/src/core/auth.ts index b97f25e7..b1649f2d 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -287,6 +287,7 @@ export class Auth { authConnectionId: this.state.userInfo.authConnectionId, groupedAuthConnectionId: this.state.userInfo.groupedAuthConnectionId, extraLoginOptions: { + ...params.extraLoginOptions, login_hint: this.state.userInfo.userId, }, mfaLevel: "mandatory", diff --git a/test/auth.test.ts b/test/auth.test.ts index 2aa216dc..68b31040 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -2,16 +2,28 @@ import { AUTH_CONNECTION, UX_MODE } from "@toruslabs/customauth"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Auth } from "../src/core/auth"; -import { AUTH_ACTIONS, AuthRequestPayload, BUILD_ENV, LoginParams, SDK_MODE, WEB3AUTH_NETWORK } from "../src/utils"; +import { AUTH_ACTIONS, AuthRequestPayload, BUILD_ENV, LoginParams, POPUP_TIMEOUT, SDK_MODE, WEB3AUTH_NETWORK } from "../src/utils"; type AuthInternals = { + authHandler: (url: string, payload: AuthRequestPayload, popupTimeout?: number) => Promise; storeAuthPayload: (loginId: string, payload: AuthRequestPayload, timeout?: number, skipAwait?: boolean) => Promise; }; -describe("Auth.manageMFA", () => { +describe("Auth MFA methods", () => { let auth: Auth; + let handledPayload: AuthRequestPayload | undefined; + let handledPopupTimeout: number | undefined; + let handledUrl: string | undefined; let storedPayload: AuthRequestPayload | undefined; + const enableMFA = async (params: Partial = {}) => { + auth.state.userInfo.isMfaEnabled = false; + const result = await auth.enableMFA(params); + expect(result).toBe(false); + expect(handledPayload).toBeDefined(); + return handledPayload as AuthRequestPayload; + }; + const manageMFA = async (params: Partial = {}) => { await auth.manageMFA(params); expect(storedPayload).toBeDefined(); @@ -19,6 +31,9 @@ describe("Auth.manageMFA", () => { }; beforeEach(() => { + handledPayload = undefined; + handledPopupTimeout = undefined; + handledUrl = undefined; storedPayload = undefined; vi.stubGlobal("window", { location: { @@ -46,6 +61,12 @@ describe("Auth.manageMFA", () => { vi.spyOn(auth, "refreshSession").mockResolvedValue(); vi.spyOn(auth, "getAccessToken").mockResolvedValue("synthetic-access-token"); + vi.spyOn(auth as unknown as AuthInternals, "authHandler").mockImplementation(async (url, payload, popupTimeout) => { + handledUrl = url; + handledPayload = payload; + handledPopupTimeout = popupTimeout; + return null; + }); vi.spyOn(auth as unknown as AuthInternals, "storeAuthPayload").mockImplementation(async (_loginId, payload) => { storedPayload = payload; }); @@ -56,7 +77,7 @@ describe("Auth.manageMFA", () => { vi.unstubAllGlobals(); }); - it("preserves custom JWT credentials in extraLoginOptions", async () => { + it("manageMFA preserves custom JWT credentials in extraLoginOptions", async () => { const payload = await manageMFA({ extraLoginOptions: { client_id: "synthetic-custom-client-id", @@ -71,7 +92,7 @@ describe("Auth.manageMFA", () => { }); }); - it("uses the authenticated user's login_hint instead of a caller-provided value", async () => { + it("manageMFA uses the authenticated user's login_hint instead of a caller-provided value", async () => { const payload = await manageMFA({ extraLoginOptions: { login_hint: "synthetic-caller-user-id", @@ -81,7 +102,7 @@ describe("Auth.manageMFA", () => { expect(payload.params.extraLoginOptions?.login_hint).toBe("synthetic-user-id"); }); - it("supports omitted extraLoginOptions", async () => { + it("manageMFA supports omitted extraLoginOptions", async () => { const payload = await manageMFA(); expect(payload.params.extraLoginOptions).toEqual({ @@ -89,7 +110,7 @@ describe("Auth.manageMFA", () => { }); }); - it("retains the existing manage MFA payload fields", async () => { + it("manageMFA retains the existing payload fields", async () => { const payload = await manageMFA({ dappUrl: "https://app.example/custom-return", loginSource: "synthetic-settings-page", @@ -119,4 +140,67 @@ describe("Auth.manageMFA", () => { }); expect(window.open).toHaveBeenCalledWith(expect.stringContaining("/start#b64Params="), "_blank"); }); + + it("enableMFA preserves custom JWT credentials in extraLoginOptions", async () => { + const payload = await enableMFA({ + extraLoginOptions: { + client_id: "synthetic-custom-client-id", + id_token: "synthetic-custom-id-token", + }, + }); + + expect(payload.params.extraLoginOptions).toEqual({ + client_id: "synthetic-custom-client-id", + id_token: "synthetic-custom-id-token", + login_hint: "synthetic-user-id", + }); + }); + + it("enableMFA uses the authenticated user's login_hint instead of a caller-provided value", async () => { + const payload = await enableMFA({ + extraLoginOptions: { + login_hint: "synthetic-caller-user-id", + }, + }); + + expect(payload.params.extraLoginOptions?.login_hint).toBe("synthetic-user-id"); + }); + + it("enableMFA supports omitted extraLoginOptions", async () => { + const payload = await enableMFA(); + + expect(payload.params.extraLoginOptions).toEqual({ + login_hint: "synthetic-user-id", + }); + }); + + it("enableMFA retains the existing payload fields", async () => { + const payload = await enableMFA({ + appState: "synthetic-app-state", + loginSource: "synthetic-settings-page", + }); + + expect(payload).toEqual({ + actionType: AUTH_ACTIONS.ENABLE_MFA, + options: { + ...auth.options, + sdkMode: SDK_MODE.DEFAULT, + }, + params: { + appState: "synthetic-app-state", + loginSource: "synthetic-settings-page", + authConnection: AUTH_CONNECTION.CUSTOM, + authConnectionId: "synthetic-auth-connection-id", + groupedAuthConnectionId: "synthetic-grouped-auth-connection-id", + extraLoginOptions: { + login_hint: "synthetic-user-id", + }, + mfaLevel: "mandatory", + }, + sessionId: "synthetic-session-id", + accessToken: "synthetic-access-token", + }); + expect(handledUrl).toBe(`${auth.baseUrl}/start`); + expect(handledPopupTimeout).toBe(POPUP_TIMEOUT); + }); });