diff --git a/app/playwright.config.ts b/app/playwright.config.ts index 0e07de457a..04147d9cb7 100644 --- a/app/playwright.config.ts +++ b/app/playwright.config.ts @@ -46,6 +46,12 @@ export default defineConfig({ name: "webkit", use: { ...devices["Desktop Safari"] }, }, + { + name: "rate limit", + use: { ...devices["Desktop Chrome"] }, + dependencies: ["chromium", "firefox", "webkit"], + testMatch: "**/*.rate-limit.spec.ts", + }, /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', diff --git a/app/src/pages/auth/LoginForm.tsx b/app/src/pages/auth/LoginForm.tsx index 54b915cc7f..4cf96de794 100644 --- a/app/src/pages/auth/LoginForm.tsx +++ b/app/src/pages/auth/LoginForm.tsx @@ -39,7 +39,10 @@ export function LoginForm(props: LoginFormProps) { body: JSON.stringify(params), }); if (!response.ok) { - setError("Invalid login"); + const errorMessage = response.status === 429 + ? "Too many requests. Please try again later." + : "Invalid login"; + setError(errorMessage); return; } } catch (error) { diff --git a/app/tests/login.rate-limit.spec.ts b/app/tests/login.rate-limit.spec.ts new file mode 100644 index 0000000000..fa99a080ae --- /dev/null +++ b/app/tests/login.rate-limit.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from "@playwright/test"; + +test("that login gets rate limited after too many attempts", async ({ page }) => { + await page.goto("/login"); + await page.waitForURL("**/login"); + + const email = `fakeuser@localhost.com`; + // Add the user + await page.getByLabel("Email").fill(email); + await page.getByLabel("Password *", { exact: true }).fill("not-a-password"); + + const numberOfAttempts = 10; + for (let i = 0; i < numberOfAttempts; i++) { + await page.getByRole("button", { name: "Login" }).click(); + } + await expect(page.getByText("Too many requests. Please try again later.")).toBeVisible(); +}); diff --git a/src/phoenix/server/api/routers/auth.py b/src/phoenix/server/api/routers/auth.py index d9aab97788..5b67775150 100644 --- a/src/phoenix/server/api/routers/auth.py +++ b/src/phoenix/server/api/routers/auth.py @@ -48,7 +48,7 @@ rate_limiter = ServerRateLimiter( per_second_rate_limit=0.2, - enforcement_window_seconds=30, + enforcement_window_seconds=60, partition_seconds=60, active_partitions=2, )