diff --git a/src/core/auth.ts b/src/core/auth.ts index 23485f42..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", @@ -342,6 +343,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..68b31040 --- /dev/null +++ b/test/auth.test.ts @@ -0,0 +1,206 @@ +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, 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 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(); + return storedPayload as AuthRequestPayload; + }; + + beforeEach(() => { + handledPayload = undefined; + handledPopupTimeout = undefined; + handledUrl = undefined; + 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, "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; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("manageMFA 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("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", + }, + }); + + expect(payload.params.extraLoginOptions?.login_hint).toBe("synthetic-user-id"); + }); + + it("manageMFA supports omitted extraLoginOptions", async () => { + const payload = await manageMFA(); + + expect(payload.params.extraLoginOptions).toEqual({ + login_hint: "synthetic-user-id", + }); + }); + + it("manageMFA retains the existing 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"); + }); + + 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); + }); +});