fix(site): redirect unauthorized users during oauth login (#22101)

The login page component incorrectly uses client-side routing to handle
redirects to /oauth2/authorize. Since this path is not defined as a
route in the react application but as a backend endpoint for the OAuth2
provider flow, the frontend displays a 404 "Route not found" error.

- resolves #22097

<!--

If you have used AI to produce some or all of this PR, please ensure you
have read our [AI Contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
before submitting.

-->
This commit is contained in:
Faur Ioan-Aurel
2026-02-17 19:04:19 +02:00
committed by GitHub
parent d737f8c104
commit 2ee54b0af1
2 changed files with 148 additions and 2 deletions
+145 -1
View File
@@ -1,10 +1,11 @@
import { MockUserOwner } from "testHelpers/entities";
import {
render,
renderWithRouter,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
import { fireEvent, screen } from "@testing-library/react";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { HttpResponse, http } from "msw";
import { createMemoryRouter } from "react-router";
@@ -76,4 +77,147 @@ describe("LoginPage", () => {
// Then
await screen.findByText("Setup");
});
it("redirects to /oauth2/authorize via server-side redirect when signed in", async () => {
// Given - user is signed in
server.use(
http.get("/api/v2/users/me", () => {
return HttpResponse.json(MockUserOwner);
}),
);
const redirectPath =
"/oauth2/authorize?client_id=xxx&response_type=code&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback";
// Spy on window.location.href assignment
const locationHrefSpy = vi.fn();
const originalLocation = window.location;
Object.defineProperty(window, "location", {
configurable: true,
value: {
...originalLocation,
origin: originalLocation.origin,
set href(url: string) {
locationHrefSpy(url);
},
get href() {
return originalLocation.href;
},
},
});
// When
renderWithRouter(
createMemoryRouter(
[
{
path: "/login",
element: <LoginPage />,
},
],
{
initialEntries: [
`/login?redirect=${encodeURIComponent(redirectPath)}`,
],
},
),
);
// Then - it should perform a server-side redirect, not a React navigate
await waitFor(() => {
expect(locationHrefSpy).toHaveBeenCalledWith(
expect.stringContaining("/oauth2/authorize"),
);
});
// Cleanup
Object.defineProperty(window, "location", {
configurable: true,
value: originalLocation,
});
});
it("redirects to /oauth2/authorize after successful login when not already signed in", async () => {
// Given - user is NOT signed in
let loggedIn = false;
server.use(
http.get("/api/v2/users/me", () => {
if (!loggedIn) {
return HttpResponse.json(
{ message: "no user here" },
{ status: 401 },
);
}
return HttpResponse.json(MockUserOwner);
}),
http.post("/api/v2/users/login", () => {
loggedIn = true;
return HttpResponse.json({
session_token: "test-session-token",
});
}),
);
const redirectPath =
"/oauth2/authorize?client_id=xxx&response_type=code&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback";
// Spy on window.location.href
const originalLocation = window.location;
const locationHrefSpy = vi.fn();
Object.defineProperty(window, "location", {
configurable: true,
value: {
...originalLocation,
origin: originalLocation.origin,
set href(url: string) {
locationHrefSpy(url);
},
get href() {
return originalLocation.href;
},
},
});
// When
renderWithRouter(
createMemoryRouter(
[
{
path: "/login",
element: <LoginPage />,
},
],
{
initialEntries: [
`/login?redirect=${encodeURIComponent(redirectPath)}`,
],
},
),
);
await waitForLoaderToBeRemoved();
const email = screen.getByLabelText(new RegExp(Language.emailLabel));
const password = screen.getByLabelText(new RegExp(Language.passwordLabel));
await userEvent.type(email, "test@coder.com");
await userEvent.type(password, "password");
const signInButton = await screen.findByText(Language.passwordSignIn);
fireEvent.click(signInButton);
// Then - it should hard redirect to OAuth endpoint
await waitFor(() => {
expect(locationHrefSpy).toHaveBeenCalledWith(
expect.stringContaining("/oauth2/authorize"),
);
});
// Cleanup
Object.defineProperty(window, "location", {
configurable: true,
value: originalLocation,
});
});
});
+3 -1
View File
@@ -35,7 +35,9 @@ const LoginPage: FC = () => {
// Do nothing
}
const isApiRouteRedirect = redirectTo.startsWith("/api/v2");
const isApiRouteRedirect =
redirectTo.startsWith("/api/v2") ||
redirectTo.startsWith("/oauth2/authorize");
useEffect(() => {
if (!buildInfoQuery.data || isSignedIn) {