Skip to content

Commit 2be8630

Browse files
fix: show styled error page instead of raw JSON for login failures
Previously, when the /login route encountered errors (expired auth request, missing SAML request, unreachable IDP, no request params), it returned raw JSON like {"error":"Auth request not found or expired"} directly to the browser. Since this route is reached via browser navigation (not a fetch() call), users saw unhelpful raw JSON text. Replace all user-facing NextResponse.json() error responses with redirects to a new /error page that renders inside the existing boxed card layout with a clear title and actionable message. The catch block for getAuthRequest now inspects the ConnectError code from Zitadel to provide specific guidance: expired sessions (NOT_FOUND), permission issues, service unavailability, and timeouts each get a tailored message. The two JSON responses intentionally left unchanged: - RSC check: internal Next.js safeguard, never user-facing - Prompt.NONE: OIDC spec requires no UI for silent auth; the calling application handles this programmatically Made-with: Cursor
1 parent c670d3b commit 2be8630

2 files changed

Lines changed: 80 additions & 21 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Alert, AlertType } from "@/components/alert";
2+
3+
export default async function Page(props: {
4+
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
5+
}) {
6+
const searchParams = await props.searchParams;
7+
const title = searchParams.title ?? "Something went wrong";
8+
const message =
9+
searchParams.error ?? "An unexpected error occurred. Please try again.";
10+
11+
return (
12+
<>
13+
<h1 className="text-2xl font-semibold">{title}</h1>
14+
<div className="pt-6">
15+
<Alert type={AlertType.ALERT}>{message}</Alert>
16+
</div>
17+
</>
18+
);
19+
}

apps/login/src/app/login/route.ts

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
listSessions,
1717
startIdentityProviderFlow,
1818
} from "@/lib/zitadel";
19-
import { create } from "@zitadel/client";
19+
import { ConnectError, create } from "@zitadel/client";
2020
import { Prompt } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
2121
import {
2222
CreateCallbackRequestSchema,
@@ -32,6 +32,37 @@ import { DEFAULT_CSP } from "../../../constants/csp";
3232
// NOTE: Route segment configs (dynamic, revalidate, fetchCache) removed for Next.js 15.6+ compatibility
3333
// With dynamicIO enabled, route handlers are dynamic by default
3434

35+
const gotoError = ({
36+
request,
37+
title,
38+
error,
39+
}: {
40+
request: NextRequest;
41+
title: string;
42+
error: string;
43+
}): NextResponse<unknown> => {
44+
const errorUrl = constructUrl(request, "/error");
45+
errorUrl.searchParams.set("title", title);
46+
errorUrl.searchParams.set("error", error);
47+
return NextResponse.redirect(errorUrl);
48+
};
49+
50+
const getAuthRequestErrorMessage = (error: unknown): string => {
51+
if (error instanceof ConnectError) {
52+
switch (error.code) {
53+
case 5: // NOT_FOUND
54+
return "Your login session has expired. Please return to the application and try signing in again.";
55+
case 7: // PERMISSION_DENIED
56+
return "You don't have permission to access this login request. Please contact your administrator.";
57+
case 14: // UNAVAILABLE
58+
return "The authentication service is temporarily unavailable. Please try again in a few moments.";
59+
case 4: // DEADLINE_EXCEEDED
60+
return "The authentication service took too long to respond. Please try again.";
61+
}
62+
}
63+
return "Your login request could not be found or has expired. Please return to the application and try signing in again.";
64+
};
65+
3566
const gotoAccounts = ({
3667
request,
3768
requestId,
@@ -142,17 +173,20 @@ export async function GET(request: NextRequest) {
142173
}));
143174
} catch (error) {
144175
console.error("Failed to get auth request:", error);
145-
return NextResponse.json(
146-
{ error: "Auth request not found or expired" },
147-
{ status: 400 },
148-
);
176+
return gotoError({
177+
request,
178+
title: "Login request expired",
179+
error: getAuthRequestErrorMessage(error),
180+
});
149181
}
150182

151183
if (!authRequest) {
152-
return NextResponse.json(
153-
{ error: "Auth request not found" },
154-
{ status: 400 },
155-
);
184+
return gotoError({
185+
request,
186+
title: "Login request not found",
187+
error:
188+
"Your login request could not be found. Please return to the application and try signing in again.",
189+
});
156190
}
157191

158192
let organization = "";
@@ -248,10 +282,12 @@ export async function GET(request: NextRequest) {
248282
});
249283

250284
if (!url) {
251-
return NextResponse.json(
252-
{ error: "Could not start IDP flow" },
253-
{ status: 500 },
254-
);
285+
return gotoError({
286+
request,
287+
title: "Sign-in unavailable",
288+
error:
289+
"We couldn't connect to your identity provider. Please try again or contact your administrator.",
290+
});
255291
}
256292

257293
if (url.startsWith("/")) {
@@ -487,10 +523,12 @@ export async function GET(request: NextRequest) {
487523
});
488524

489525
if (!samlRequest) {
490-
return NextResponse.json(
491-
{ error: "No samlRequest found" },
492-
{ status: 400 },
493-
);
526+
return gotoError({
527+
request,
528+
title: "Login request not found",
529+
error:
530+
"Your SAML login request could not be found or has expired. Please return to the application and try signing in again.",
531+
});
494532
}
495533

496534
let selectedSession = await findValidSession({
@@ -574,9 +612,11 @@ export async function GET(request: NextRequest) {
574612
}
575613
// Device Authorization does not need to start here as it is handled on the /device endpoint
576614
else {
577-
return NextResponse.json(
578-
{ error: "No authRequest nor samlRequest provided" },
579-
{ status: 500 },
580-
);
615+
return gotoError({
616+
request,
617+
title: "No login request",
618+
error:
619+
"No authentication request was provided. Please start the sign-in process from your application.",
620+
});
581621
}
582622
}

0 commit comments

Comments
 (0)