Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 }),
Expand Down
206 changes: 206 additions & 0 deletions test/auth.test.ts
Original file line number Diff line number Diff line change
@@ -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<null>;
storeAuthPayload: (loginId: string, payload: AuthRequestPayload, timeout?: number, skipAwait?: boolean) => Promise<void>;
};

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<LoginParams> = {}) => {
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<LoginParams> = {}) => {
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);
});
});