Skip to content

Commit f31364e

Browse files
authored
fix(login): display errors received in the callback URL MAASENG-6125 (#5958)
- Added a `<Notification />` for when an error is received in the callback URL after logging in via SSO - Such errors usually occur when the user uses an email domain that the IdP doesn't allow, or other mismatches between the IdP's config and its config in MAAS Resolves [MAASENG-6125](https://warthogs.atlassian.net/browse/MAASENG-6125)
1 parent 708c9d5 commit f31364e

File tree

2 files changed

+43
-7
lines changed

2 files changed

+43
-7
lines changed

src/app/login/LoginCallback/LoginCallback.test.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("LoginCallback", () => {
3131
expect(screen.getByRole("alert")).toHaveTextContent(Labels.MissingParams);
3232
});
3333

34-
it("shows an error if callback fails", async () => {
34+
it("shows an error if callback fails due to a server error", async () => {
3535
mockServer.use(authResolvers.getCallback.error());
3636
renderWithProviders(<LoginCallback />, {
3737
initialEntries: ["/login/oidc/callback?code=abc123&state=xyz789"],
@@ -40,6 +40,28 @@ describe("LoginCallback", () => {
4040
await waitFor(() => {
4141
expect(screen.getByRole("alert")).toHaveTextContent(Labels.CallbackError);
4242
});
43+
expect(screen.getByRole("alert")).toHaveTextContent(
44+
"Please try logging in again."
45+
);
46+
});
47+
48+
it("shows an error if the callback URL contains an error parameter", async () => {
49+
renderWithProviders(<LoginCallback />, {
50+
initialEntries: [
51+
"/login/oidc/callback?error=access_denied&error_description=User%20denied%20access",
52+
],
53+
state,
54+
});
55+
await waitFor(() => {
56+
expect(screen.getByRole("alert")).toHaveTextContent(
57+
"Error: access_denied"
58+
);
59+
});
60+
expect(screen.getByRole("alert")).toHaveTextContent(
61+
"Please try logging in again."
62+
);
63+
expect(screen.getByRole("alert")).toHaveTextContent("User denied access");
64+
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
4365
});
4466

4567
it("shows a loading state while processing the callback", async () => {

src/app/login/LoginCallback/LoginCallback.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ import {
88
Strip,
99
} from "@canonical/react-components";
1010
import { useDispatch, useSelector } from "react-redux";
11-
import { useLocation, useNavigate } from "react-router";
11+
import { Link, useLocation, useNavigate } from "react-router";
1212

1313
import { useCreateSession, useGetCallback } from "@/app/api/query/auth";
1414
import PageContent from "@/app/base/components/PageContent";
15+
import urls from "@/app/base/urls";
1516
import { statusActions } from "@/app/store/status";
1617
import statusSelectors from "@/app/store/status/selectors";
1718

1819
export const Labels = {
1920
MissingParams: "Missing code or state in the callback URL.",
20-
CallbackError:
21-
"An error occurred during authentication. Please try logging in again.",
21+
CallbackError: "An error occurred during authentication.",
2222
AlreadyAuthenticated: "You are already authenticated. Redirecting...",
2323
};
2424

25-
export const LoginCallback = (): React.ReactElement => {
25+
const LoginCallback = (): React.ReactElement => {
2626
const location = useLocation();
2727
const authenticated = useSelector(statusSelectors.authenticated);
2828
const dispatch = useDispatch();
@@ -31,7 +31,10 @@ export const LoginCallback = (): React.ReactElement => {
3131
const params = new URLSearchParams(location.search);
3232
const code = params.get("code");
3333
const state = params.get("state");
34+
const error = params.get("error");
35+
const errorDescription = params.get("error_description");
3436
const hasParams = Boolean(code && state);
37+
const hasError = Boolean(error || errorDescription);
3538

3639
const callback = useGetCallback(
3740
{
@@ -45,7 +48,7 @@ export const LoginCallback = (): React.ReactElement => {
4548

4649
const hasHandledSuccess = useRef(false);
4750
const shouldShowMissingParamsError =
48-
!authenticated && !hasParams && !callback.isError;
51+
!authenticated && !hasParams && !callback.isError && !hasError;
4952

5053
useEffect(() => {
5154
if (authenticated) {
@@ -70,17 +73,28 @@ export const LoginCallback = (): React.ReactElement => {
7073
<Strip>
7174
<Row>
7275
<Col emptyLarge={4} size={6}>
76+
{hasError && (
77+
<Notification role="alert" severity="negative">
78+
{error && `Error: ${error}. `}
79+
<br />
80+
{errorDescription}
81+
<br />
82+
Please try <Link to={urls.login}>logging in</Link> again.
83+
</Notification>
84+
)}
7385
{shouldShowMissingParamsError && (
7486
<Notification role="alert" severity="information">
7587
{Labels.MissingParams}
7688
</Notification>
7789
)}
78-
{code && state && callback.isPending && (
90+
{code && state && !hasError && callback.isPending && (
7991
<Spinner aria-label={"Loading..."} text="Loading..." />
8092
)}
8193
{callback.isError && (
8294
<Notification role="alert" severity="negative">
8395
{Labels.CallbackError}
96+
<br />
97+
Please try <Link to={urls.login}>logging in</Link> again.
8498
</Notification>
8599
)}
86100
{authenticated && (

0 commit comments

Comments
 (0)