Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/login/sentry.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ Sentry.init({
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,

ignoreErrors: [
// Malformed RSC requests from bots/crawlers sending invalid Next-Router-State-Tree headers
"The router state header was sent but could not be parsed",
// Node.js TransformStream race condition during RSC streaming (node:internal/webstreams bug)
"transformAlgorithm is not a function",
],

// Enable sending user PII (Personally Identifiable Information)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
Expand Down
3 changes: 2 additions & 1 deletion apps/login/src/app/(main)/(boxed)/accounts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SessionsList } from "@/components/sessions-list";
import { Translated } from "@/components/translated";
import { getAllSessionCookieIds } from "@/lib/cookies";
import { generateRouteMetadata } from "@/lib/metadata";
import { toPlainObject } from "@/lib/serialize";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import {
getBrandingSettings,
Expand Down Expand Up @@ -55,7 +56,7 @@ export default async function Page(props: {
}
}

let sessions = await loadSessions({ serviceUrl });
let sessions = toPlainObject(await loadSessions({ serviceUrl }));

const branding = await getBrandingSettings({
serviceUrl,
Expand Down
26 changes: 21 additions & 5 deletions apps/login/src/app/(main)/(boxed)/idp/[provider]/success/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,27 @@ export default async function Page(props: {
return loginFailed("IDP context missing");
}

const intent = await retrieveIDPIntent({
serviceUrl,
id,
token,
});
let intent;
try {
intent = await retrieveIDPIntent({
serviceUrl,
id,
token,
});
} catch (error) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === 9 /* FAILED_PRECONDITION */
) {
// Intent was already consumed by the createNewSessionFromIdpIntent
// server action. This re-render is expected — the client is already
// processing the redirect, so return a benign loading state.
return loginFailed("Session is being created, please wait…");
}
throw error;
}

const { idpInformation, userId: intentUserId } = intent;
const resolvedUserId = intentUserId || qpUserId;
Expand Down
3 changes: 2 additions & 1 deletion apps/login/src/app/(main)/(boxed)/logout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SessionsClearList } from "@/components/sessions-clear-list";
import { Translated } from "@/components/translated";
import { getAllSessionCookieIds } from "@/lib/cookies";
import { generateRouteMetadata } from "@/lib/metadata";
import { toPlainObject } from "@/lib/serialize";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getDefaultOrg, listSessions } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
Expand Down Expand Up @@ -47,7 +48,7 @@ export default async function Page(props: {
}
}

let sessions = await loadSessions({ serviceUrl });
let sessions = toPlainObject(await loadSessions({ serviceUrl }));

const params = new URLSearchParams();

Expand Down
4 changes: 3 additions & 1 deletion apps/login/src/components/session-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export function SessionItem({
if (valid && session?.factors?.user) {
try {
const resp = await continueWithSession({
...session,
sessionId: session.id,
loginName: session.factors?.user?.loginName,
organizationId: session.factors?.user?.organizationId,
requestId: requestId,
});

Expand Down
11 changes: 11 additions & 0 deletions apps/login/src/lib/serialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Converts protobuf message objects into plain JSON-serializable objects
* suitable for passing across the React Server Component → Client Component
* boundary. Protobuf-ES v2 messages can contain bigint values and
* null-prototype objects that RSC serialization rejects.
*/
export function toPlainObject<T>(obj: T): T {
return JSON.parse(
JSON.stringify(obj, (_, v) => (typeof v === "bigint" ? Number(v) : v)),
);
}
35 changes: 23 additions & 12 deletions apps/login/src/lib/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
} from "@/lib/zitadel";
import { Duration } from "@zitadel/client";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { headers } from "next/headers";
import { getNextUrl } from "../client";
Expand Down Expand Up @@ -71,38 +70,50 @@ export async function skipMFAAndContinueWithNextUrl({

export async function continueWithSession({
requestId,
...session
}: Session & { requestId?: string }) {
sessionId,
loginName,
organizationId,
}: {
requestId?: string;
sessionId: string;
loginName?: string;
organizationId?: string;
}) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);

const loginSettings = await getLoginSettings({
serviceUrl,
organization: session.factors?.user?.organizationId,
organization: organizationId,
});

const url =
requestId && session.id && session.factors?.user
requestId && sessionId
? await getNextUrl(
{
sessionId: session.id,
requestId: requestId,
organization: session.factors.user.organizationId,
sessionId,
requestId,
organization: organizationId,
},
loginSettings?.defaultRedirectUri,
)
: session.factors?.user
: loginName
? await getNextUrl(
{
loginName: session.factors.user.loginName,
organization: session.factors.user.organizationId,
loginName,
organization: organizationId,
},
loginSettings?.defaultRedirectUri,
)
: null;

if (!url) {
console.error("Could not get next url", { requestId, session });
console.error("Could not get next url", {
requestId,
sessionId,
loginName,
organizationId,
});
}

return url ? { redirect: url } : null;
Expand Down
Loading