Félix Malfait be051c8724 Keep the token pair as a fallback after switching to cookie auth (#23755)
`CookieSessionBootEffect` cleared the token pair the moment it switched
a client onto cookie auth. That leaves the client with a single
credential, and a server that still has
`AUTH_COOKIE_SESSIONS_ENABLED=false` ignores the session cookie entirely
— `extractSessionTokenFromRequest` early-returns when the flag is off. A
cookie-only client is therefore unauthenticated against such a server,
`handleTokenRenewal` finds no refresh token, and
`onUnauthenticatedError` signs the user out.

That is not a hypothetical state. It is every request routed to a
not-yet-rolled pod while the flag is being enabled, and every request
after the flag is rolled back. Requests are load-balanced per request,
so a migrated client hits an old pod almost immediately and gets signed
out; signing back in can migrate it again and repeat for the length of
the rollout.

It also means rollback was not free, contrary to how it was described:
flipping the flag back to `false` signed out everyone who had already
migrated, because the pair they were supposed to fall back to had been
deleted.

## Approach

Keep the token pair as a dormant fallback, and stop *sending* it while
cookie auth is active.

Both halves are needed. Retaining it without suppressing the header
would be worse than the bug: `validateTokenByRequest` checks the Bearer
token first and only falls back to the session cookie when there is
none, so a client that keeps sending Bearer would never exercise the
cookie at all, and `CookieSessionCsrfMiddleware` bypasses on any
Bearer-carrying request. Cookie sessions would silently become a no-op.

So:

- `switchToCookieAuth` no longer nulls the token pair
- the auth link omits `authorization` while cookie auth is active,
leaving the cookie as the credential in use
- on an unauthenticated error while cookie auth is active, the client
deactivates cookie auth once per operation and falls through to the
existing renewal path, which replays with a fresh Bearer

The fallback deliberately goes through renewal rather than replaying
immediately: access tokens live 10 minutes, so the retained one has
usually expired while the client was authenticating by cookie, and an
immediate replay would just fail again.

`isCookieAuthActive` is read and written through `localStorage` from the
link because the links run per request and must agree with the atom
synchronously — a React state update lands a render too late to affect
the request being built.

## Follow-up

This trades the immediate removal of the token pair from `localStorage`
for rollout safety, so the XSS-exfiltration surface that cookie sessions
close stays open a while longer. Once cookie sessions are stable across
every environment, the retained pair should be dropped — reverting to a
clear on `switchToCookieAuth` is a one-line change.

## Test

Three cases added to `apollo.factory.test.ts`: no Bearer header while
cookie auth is active; an unauthenticated response falls back and
replays with the token pair rather than calling
`onUnauthenticatedError`; and the fallback is attempted only once before
going through renewal. The existing `CookieSessionBootEffect` assertion
that the pair is cleared is inverted to assert it is retained.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23755?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-04 17:12:51 +02:00
2026-06-11 11:02:28 +02:00

Twenty logo

The #1 Open-Source CRM

Website · Documentation · Roadmap · Discord · Figma

Twenty banner


Why Twenty

Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.

Learn more about why we built Twenty


Installation

Cloud

The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.

Build an app

Scaffold a new app with the Twenty CLI:

npx create-twenty-app my-app

Define objects, fields, and views as code:

import { defineObject, FieldType } from 'twenty-sdk/define';

export default defineObject({
  nameSingular: 'deal',
  namePlural: 'deals',
  labelSingular: 'Deal',
  labelPlural: 'Deals',
  fields: [
    { name: 'name', label: 'Name', type: FieldType.TEXT },
    { name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
    { name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
  ],
});

Then ship it to your workspace:

npx twenty app:publish --private

See the app development guide for objects, views, agents, and logic functions.

Self-hosting

Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.



Everything you need

Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.

Want to go deeper? Read the User Guide for product walkthroughs, or the Documentation for developer reference.

Create your apps

Learn more about apps in doc

Stay on top with version control

Learn more about version control in doc

All the tools you need to build anything

Learn more about primitives in doc

Customize your layouts

Learn more about layouts in doc

AI agents and chats

Learn more about AI in doc

Plus all the tools of a good CRM

Learn more about CRM features in doc


Stack

Thanks

Greptile      Sentry      Crowdin

Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).

Join the Community

Star the repo · Discord · Feature requests · Releases · X · LinkedIn · Crowdin · Contribute

S
Description
The open alternative to Salesforce, designed for AI.
Readme AGPL-3.0 1.4 GiB
Languages
TypeScript 79.6%
MDX 17.3%
JavaScript 2.7%
Python 0.2%
SCSS 0.1%