Skip to content

Commit 9d2b805

Browse files
authored
fix: prevent infinite redirect oauth auth flow (coder#10430)
* fix: prevent infinite redirect oauth auth flow
1 parent 7fc1a65 commit 9d2b805

File tree

2 files changed

+38
-3
lines changed

2 files changed

+38
-3
lines changed

coderd/externalauth.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,9 @@ func (api *API) externalAuthCallback(externalAuthConfig *externalauth.Config) ht
268268

269269
redirect := state.Redirect
270270
if redirect == "" {
271-
// This is a nicely rendered screen on the frontend
272-
redirect = fmt.Sprintf("/external-auth/%s", externalAuthConfig.ID)
271+
// This is a nicely rendered screen on the frontend. Passing the query param lets the
272+
// FE know not to enter the authentication loop again, and instead display an error.
273+
redirect = fmt.Sprintf("/external-auth/%s?redirected=true", externalAuthConfig.ID)
273274
}
274275
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
275276
}

site/src/pages/ExternalAuthPage/ExternalAuthPage.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@ import {
66
} from "api/api";
77
import { usePermissions } from "hooks";
88
import { type FC } from "react";
9-
import { useParams } from "react-router-dom";
9+
import { useParams, useSearchParams } from "react-router-dom";
1010
import ExternalAuthPageView from "./ExternalAuthPageView";
1111
import { ApiErrorResponse } from "api/errors";
1212
import { isAxiosError } from "axios";
13+
import Box from "@mui/material/Box";
14+
import Button from "@mui/material/Button";
15+
import { SignInLayout } from "components/SignInLayout/SignInLayout";
16+
import { Welcome } from "components/Welcome/Welcome";
1317

1418
const ExternalAuthPage: FC = () => {
1519
const { provider } = useParams();
1620
if (!provider) {
1721
throw new Error("provider must exist");
1822
}
23+
const [searchParams] = useSearchParams();
1924
const permissions = usePermissions();
2025
const queryClient = useQueryClient();
2126
const getExternalAuthProviderQuery = useQuery({
@@ -72,6 +77,35 @@ const ExternalAuthPage: FC = () => {
7277
!getExternalAuthProviderQuery.data.authenticated &&
7378
!getExternalAuthProviderQuery.data.device
7479
) {
80+
const redirectedParam = searchParams?.get("redirected");
81+
if (redirectedParam && redirectedParam.toLowerCase() === "true") {
82+
// The auth flow redirected the user here. If we redirect back to the
83+
// callback, that resets the flow and we'll end up in an infinite loop.
84+
// So instead, show an error, as the user expects to be authenticated at
85+
// this point.
86+
// TODO: Unsure what to do about the device auth flow, should we also
87+
// show an error there?
88+
return (
89+
<SignInLayout>
90+
<Welcome message="Failed to validate oauth access token" />
91+
<Box textAlign="center">
92+
Attempted to validate the user&apos;s oauth access token from the
93+
authentication flow. This situation may occur as a result of an
94+
external authentication provider misconfiguration. Verify the
95+
external authentication validation URL is accurately configured.
96+
</Box>
97+
<br />
98+
<Button
99+
onClick={() => {
100+
// Redirect to the auth flow again. *crosses fingers*
101+
window.location.href = `/external-auth/${provider}/callback`;
102+
}}
103+
>
104+
Retry
105+
</Button>
106+
</SignInLayout>
107+
);
108+
}
75109
window.location.href = `/external-auth/${provider}/callback`;
76110
return null;
77111
}

0 commit comments

Comments
 (0)