mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user