051506b5bb fix(login): prevent crash on a stale session cookie (#12423)
<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->
# Which Problems Are Solved

The `sessions` cookie lifetime is independent of Zitadel's server-side
session validity, so a cookie can reference an invalid session. In that
case `getSession` throws `not_found` (QUERY-SFeaa).

- `loadMostRecentSession` lets that error propagate instead of treating
it as "no session", crashing the server render of every page that calls
it without its own guard.
- Most visibly, the "forgot password" flow landing on `/password/set`
throws `[not_found] Session does not exist (QUERY-SFeaa)` (gRPC code 5,
HTTP 404) and fails to render.
- 12 of the 16 call sites are unguarded and share this crash.

# How the Problems Are Solved

Catch the error in `loadMostRecentSession` and return `undefined`,
unifying "cookie references a dropped session" with the existing "no
cookie" case. Every call site already handles an undefined session,
either with a graceful fallback or by throwing its own explicit error.

```ts
return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token })
  .then((resp: GetSessionResponse) => resp.session)
  .catch((error) => {
    console.warn("[Session] Could not load most recent session", error);
    return undefined;
  });
```

All `loadMostRecentSession` call sites — **Previous** = behaviour when
the cookie referenced an invalid session; **Change** = what this PR does
to that call site:

| # | Call site | Change in this PR | Previous behaviour (invalid
session) | New behaviour |

|---|-----------|-------------------|--------------------------------------|---------------|
| — | `lib/session.ts` — `loadMostRecentSession` | **Added `.catch` →
`undefined`** (core fix) | Threw `not_found` to caller | Resolves to
`undefined` |
| 1 | `password/set/page.tsx` | none | **Crash** (the reported bug) |
Falls back to login-name flow |
| 2 | `password/change/page.tsx` | none | **Crash** | `undefined` →
`failedLoading` alert if no user |
| 3 | `passkey/set/page.tsx` | none | **Crash** | `undefined` → `userId`
fallback / `unknownContext` |
| 4 | `otp/[method]/page.tsx` | none | **Crash** | `undefined` →
`unknownContext` alert |
| 5 | `otp/[method]/set/page.tsx` | none | **Crash** (raw
`ConnectError`) | `undefined` → own `throw "No session found"` |
| 6 | `mfa/page.tsx` | none | **Crash** | `undefined` →
`verify.noResults` alert |
| 7 | `mfa/set/page.tsx` | none | **Crash** (raw `ConnectError`) |
`undefined` → own `throw "Could not get user id from session"` |
| 8 | `authenticator/set/page.tsx` | none | **Crash** (raw
`ConnectError`) | `undefined` → own throw / `unknownContext` |
| 9 | `signedin/page.tsx` | none | **Crash** | `undefined` → renders
with `loginName` fallback |
| 10 | `u2f/set/page.tsx` | none | **Crash** | `undefined` →
`unknownContext` alert |
| 11 | `u2f/page.tsx` | none | **Crash** | `undefined` → renders on
`loginName` |
| 12 | `lib/server/verify.ts` — `verifyTOTP` | none | **Crash** (raw
`ConnectError`) | `undefined` → own `throw "No user id found in
session."` |
| 13 | `password/page.tsx` | **Removed redundant `try/catch`** | Caught
locally → `undefined` | `undefined` — unchanged |
| 14 | `verify/page.tsx` | **Removed redundant `.catch`** ¹ | Caught
locally → `undefined` | `undefined` — unchanged |
| 15 | `verify/success/page.tsx` | **Removed redundant `.catch`** |
Caught locally → `undefined` | `undefined` — unchanged |
| 16 | `passkey/page.tsx` | **Removed redundant `.catch`** (login-name
path) | Caught locally → `undefined` | `undefined` — unchanged |

Rows 1–12 were unguarded and crashed; the central change fixes them.
Rows 13–16 already handled the error locally.

# Additional Changes

- This makes the four callers that wrapped the call in their own
try/catch or `.catch` redundant, so remove that now-dead handling
(password, verify, verify/success, passkey).
- Add unit tests covering `loadMostRecentSession`: no cookie, success,
and the stale-cookie rejection path.

¹ `verify/page.tsx`'s `.catch` also eagerly fetched `loginSettings`;
that is already covered by the page's own lazy fetch, so behaviour is
unchanged (one cosmetic edge case under `ignoreUnknownUsernames` with
both `userId` and `loginName` present — no functional impact).

# Additional Context

- Session *expiry* alone does not trigger this: expired rows are not
removed from the session projection, so `getSession` still returns them.
The `not_found` specifically means the session was **removed**
server-side (logout, admin/API termination, or a user/org/instance
cascade) while the browser still held the cookie.
- Mirrors the pattern upstream already applies inline in
`verify/page.tsx` (*"ignore error, as we might not have a session
yet"*), lifted to the shared function.
- **Related issue (not in this PR):** the sibling `loadSessionById`
helpers on the `?sessionId=` path (`passkey`, `otp`, `mfa`, `mfa/set`,
`signedin`, `authenticator/set`, `u2f`) call `getSession` directly and
share the same defect. Lower practical exposure (short-lived in-flow
URLs; a clean logout clears the cookie), so a follow-up PR should lift
those into a shared `loadSessionById` in `lib/session.ts`.
- Prior art: upstream added an equivalent guard to verify/page.tsx in
#11130, but the other call sites were never given the same treatment —
this PR resolves that inconsistency at the source.

Error trace:
```
{"level":"error","message":"⨯ Error [ConnectError]: [not_found] Session does not exist (QUERY-SFeaa)\n    at R (.next/server/chunks/ssr/_0uqqxk7._.js:3:25490)\n    at <unknown> (.next/server/chunks/ssr/_0uqqxk7._.js:3:26477)\n    at next (.next/server/chunks/ssr/_0uqqxk7._.js:3:68112)\n    at async (.next/server/chunks/ssr/_0uqqxk7._.js:10:31326)\n    at async (.next/server/chunks/ssr/_0uqqxk7._.js:10:33734)\n    at async Object.unary (.next/server/chunks/ssr/_0uqqxk7._.js:3:66838)\n    at async Object.getSession (.next/server/chunks/ssr/_0uqqxk7._.js:3:72371)\n    at async o (.next/server/chunks/ssr/[root-of-the-server]__0msfcq_._.js:1:7440) {\n  rawMessage: 'Session does not exist (QUERY-SFeaa)',\n  code: 5,\n  metadata: Headers {\n    'accept-encoding': 'gzip',\n    'alt-svc': 'h3=\":443\"; ma=2592000',\n    'content-length': '240',\n    'content-type': 'application/json',\n    date: 'Wed, 08 Jul 2026 09:06:09 GMT',\n     'strict-transport-security': 'max-age=63072000; includeSubDomains; preload',\n     vary: 'Origin',\n     },\n  details: [\n    {\n      type: 'zitadel.v1.ErrorDetail',\n      value: [Uint8Array],\n      debug: [Object]\n    }\n  ],\n  cause: undefined,\n  httpStatus: 404,\n  isUserError: true,\n  digest: '634020356'\n}","timestamp":"2026-07-08T09:06:09.969Z"}
```

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Liam Neville <[email protected]>
2026-07-14 09:03:46 +00:00
2026-07-09 13:28:09 +02:00
2023-07-17 10:08:20 +02:00
2026-07-10 09:17:49 +02:00
2024-07-26 22:39:55 +02:00
2021-02-08 16:48:41 +01:00
2025-04-02 16:53:06 +02:00
2026-02-04 11:51:43 +01:00
2026-06-04 14:33:04 +02:00
2026-02-25 19:36:00 -03:00
2026-06-10 11:53:26 +00:00

ZITADEL Logo ZITADEL Logo

GitHub Workflow Status (with event) GitHub contributors

The Identity Infrastructure for Developers

ZITADEL is an open-source identity and access management platform built for teams that need more than basic auth. Whether you're securing a SaaS product, building a B2B platform, or self-hosting a production IAM stack — ZITADEL gives you everything out of the box: SSO, MFA, Passkeys, OIDC, SAML, SCIM, and a battle-tested multi-tenancy model.

No vendor lock-in. No compromise on control. Just a robust, API-first identity platform you can own.


🏡 Website  |  💬 Chat  |  📋 Docs  |  🧑‍💻 Blog  |  📞 Contact


Why ZITADEL

We built ZITADEL to handle the hardest IAM challenges at scale — starting with multi-tenancy.

ZITADEL FusionAuth Keycloak Auth0/Okta
Open-source ✅ ❌ ✅ ❌
Self-hostable ✅ ✅ ✅ ❌
Infrastructure-level tenants ✅ Instances (High scale) ✅ Tenants 🟡 Realms (Scaling limits) ❌ (Multi-tenant = multi-account)
B2B Organizations ✅ Native & Unlimited 🟡 via Entity Management ✅ (Recent addition) 🟡 (Plan/Account dependent)
Full audit trail ✅ Comprehensive Event Stream* 🟡 Audit logs 🟡 Audit logs 🟡 Audit logs
Passkeys (FIDO2) ✅ ✅ ✅ ✅
Actions / webhooks ✅ ✅ 🟡 via SPI ✅
API-first (gRPC + REST) ✅ 🟡 REST only 🟡 REST only 🟡 REST only
SaaS + self-host parity ✅ ✅ ➖ N/A ➖ N/A

ZITADEL Cloud and self-hosted ZITADEL run the same codebase.

Key differentiators for architects:

  • Relational core, event-driven soul — every mutation is written as an immutable event for a complete, API-accessible audit trail. Unlike systems that log only select activities, ZITADEL provides a comprehensive event stream that can be audited or streamed to external systems via Webhooks.
  • Strict multi-tenant hierarchy — Identity System → Organizations → Projects, with isolated data and policy scoping at multiple levels
  • API-first design — every resource and action is available via connectRPC, gRPC, and HTTP/JSON APIs
  • Zero-downtime updates and horizontal scalability without external session stores

Get Started in 3 Minutes

👉 Quick Start Guide

ZITADEL Self-Hosted

# Docker Compose — up and running in under 3 minutes
curl -LO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.yml \
  && curl -LO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/.env.example \
  && cp .env.example .env \
  && docker compose up -d --wait

Full deployment guides:

Need professional support for your self-hosted deployment? Contact us.

ZITADEL Cloud (SaaS)

Start for free at zitadel.com — no credit card required. Available in US · EU · AU · CH. Pay-as-you-go pricing.


Integrate with the V2 API

ZITADEL exposes every capability over a typed API. Here's how to create a user with the V2 REST API:

curl -X POST https://$ZITADEL_DOMAIN/v2/users/human \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "[email protected]",
    "profile": { "givenName": "Alice", "familyName": "Smith" },
    "email": { "email": "[email protected]", "sendCode": {} }
  }'

Explore the full API reference — including connectRPC and gRPC transports — or jump straight to quickstart examples.


Features

Authentication

Multi-Tenancy

Integration

Self-Service & Admin

Deployment

Track upcoming features on our roadmap and follow our changelog for recent updates.


Showcase

Login V2

Our new, fully customizable login experience — documentation


Adopters & Ecosystem

Used in production by organizations worldwide. See the full Adopters list — and add yours by submitting a pull request.


How To Contribute

ZITADEL is built in the open and welcoming to contributions of all kinds.

Contributors

Made with contrib.rocks.


Security

Security policy: SECURITY.md

Vulnerability Disclosure Policy — how to responsibly report security issues.

Technical Advisories are published for major issues that could impact security or stability in production.

License

AGPL-3.0 — see LICENSING.md for the full licensing policy, including Apache 2.0 and MIT exceptions for specific directories.

S
Description
No description provided
Readme AGPL-3.0
510 MiB
Languages
Go 74.6%
TypeScript 12%
MDX 7.9%
HTML 2.6%
SCSS 1.2%
Other 1.7%