fix(#411): sanitize login error messages through parseAuthError — prevent raw error leakage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,9 +44,10 @@ vi.mock("@/lib/auth/fetch-with-retry", () => ({
|
||||
fetchWithRetry: mockFetchWithRetry,
|
||||
}));
|
||||
|
||||
// Mock parseAuthError to use the real implementation
|
||||
vi.mock("@/lib/auth/auth-errors", async (importOriginal) => {
|
||||
return importOriginal();
|
||||
// Use real parseAuthError implementation — vi.mock required for module resolution in vitest
|
||||
vi.mock("@/lib/auth/auth-errors", async () => {
|
||||
const actual = await import("../../../lib/auth/auth-errors");
|
||||
return { ...actual };
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -317,7 +318,7 @@ describe("LoginPage", (): void => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error banner on sign-in failure", async (): Promise<void> => {
|
||||
it("sanitizes BetterAuth error messages through parseAuthError", async (): Promise<void> => {
|
||||
mockFetchConfig(EMAIL_ONLY_CONFIG);
|
||||
mockSignInEmail.mockResolvedValueOnce({
|
||||
error: { message: "Invalid credentials" },
|
||||
@@ -334,8 +335,39 @@ describe("LoginPage", (): void => {
|
||||
await user.type(screen.getByLabelText(/password/i), "wrong");
|
||||
await user.click(screen.getByRole("button", { name: /continue/i }));
|
||||
|
||||
// Raw "Invalid credentials" is mapped through parseAuthError to a PDA-friendly message
|
||||
await waitFor((): void => {
|
||||
expect(screen.getByText("Invalid credentials")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Authentication didn't complete. Please try again when ready.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(mockPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("maps raw DB/server errors to PDA-friendly messages instead of leaking details", async (): Promise<void> => {
|
||||
mockFetchConfig(EMAIL_ONLY_CONFIG);
|
||||
// Simulate a leaked internal server error from BetterAuth
|
||||
mockSignInEmail.mockResolvedValueOnce({
|
||||
error: { message: "Internal server error: connection to DB pool exhausted" },
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor((): void => {
|
||||
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByLabelText(/email/i), "test@example.com");
|
||||
await user.type(screen.getByLabelText(/password/i), "wrong");
|
||||
await user.click(screen.getByRole("button", { name: /continue/i }));
|
||||
|
||||
// parseAuthError maps "internal server" keyword to server_error PDA-friendly message
|
||||
await waitFor((): void => {
|
||||
expect(
|
||||
screen.getByText("The service is taking a break. Please try again in a moment.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(mockPush).not.toHaveBeenCalled();
|
||||
@@ -359,9 +391,11 @@ describe("LoginPage", (): void => {
|
||||
await user.type(screen.getByLabelText(/password/i), "wrong");
|
||||
await user.click(screen.getByRole("button", { name: /continue/i }));
|
||||
|
||||
// When error.message is falsy, parseAuthError receives the raw error object
|
||||
// which falls through to the "unknown" code PDA-friendly message
|
||||
await waitFor((): void => {
|
||||
expect(
|
||||
screen.getByText("Unable to sign in. Please check your credentials and try again.")
|
||||
screen.getByText("Authentication didn't complete. Please try again when ready.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -102,11 +102,10 @@ export default function LoginPage(): ReactElement {
|
||||
const result = await signIn.email({ email, password });
|
||||
|
||||
if (result.error) {
|
||||
setError(
|
||||
typeof result.error.message === "string"
|
||||
? result.error.message
|
||||
: "Unable to sign in. Please check your credentials and try again."
|
||||
const parsed = parseAuthError(
|
||||
result.error.message ? new Error(String(result.error.message)) : result.error
|
||||
);
|
||||
setError(parsed.message);
|
||||
} else {
|
||||
router.push("/tasks");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user