fix(login): improve error classification (#11926)

Closes #11923

# Which Problems Are Solved

Client-side gRPC errors (e.g. NotFound, InvalidArgument,
PermissionDenied) were being surfaced as HTTP 500 Internal Server
Errors, causing false SRE alerts and poor UI feedback.

# How the Problems Are Solved

- New transport interceptor (`error-classification.ts`): Automatically
enriches every ConnectError with httpStatus and isUserError metadata via
a `ClassifiedConnectError` wrapper
- Route handler protection (`route.ts`, `flow-initiation.ts`): Catches
classified errors from `getAuthRequest` / `getSAMLRequest` and returns
correct HTTP status codes instead of 500
- Type safety: Replaced all magic number error.code === 9 checks with
typed Code.FailedPrecondition + instanceof ConnectError across
`oidc.ts`, `saml.ts`, `password.ts`, `zitadel.ts`
- Classification-aware logging (`session.ts`): Client/user errors log at
warn level, server errors at error level
- Observability (`otel.ts`): Spans now include error.is_user_error and
http.status_code attributes for alert filtering

# Additional Changes

Gitignore `next-env.d.ts`: This file is auto-generated by Next.js on
every next dev and next build invocation. The two modes write slightly
different import paths (.next/dev/types/ vs .next/types/), which causes
git diff --exit-code to fail in CI whenever a developer runs next dev
locally before committing. Since Next.js regenerates it automatically,
there's no need to track it

---------

Co-authored-by: Ramon <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Max Peintner
2026-03-31 11:20:56 +02:00
committed by GitHub
co-authored by Ramon Copilot
parent 8e41d288ff
commit ff4aae7b3f
18 changed files with 332 additions and 36 deletions
+3 -2
View File
@@ -1,6 +1,7 @@
"use server";
import { getSessionCookieById } from "@/lib/cookies";
import { isClassifiedError } from "@/lib/grpc/interceptors/error-classification";
import { createLogger } from "@/lib/logger";
import { getServiceConfig } from "@/lib/service-url";
import {
@@ -18,7 +19,7 @@ import {
ServiceConfig,
updateHuman,
} from "@/lib/zitadel";
import { Code, ConnectError, create } from "@zitadel/client";
import { Code, create } from "@zitadel/client";
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
import {
@@ -276,7 +277,7 @@ async function handleExplicitLinking(ctx: IDPHandlerContext): Promise<IDPHandler
logger.error("Error linking IDP", { error });
const errorMessage = error instanceof Error ? error.message : t("errors.unknownError");
let params = buildRedirectParams({ error: errorMessage });
if (error instanceof ConnectError && error.code === Code.AlreadyExists) {
if (isClassifiedError(error) && error.code === Code.AlreadyExists) {
params = buildRedirectParams({ error: "external_idp_taken" });
}
return { redirect: `/idp/${provider}/linking-failed?${params}` };