Авторизованный пользователь видит форму входа. Bitrix жив. Next.js жив. Они просто не договорились.
Когда мы перенесли каталог 28 000 товаров на Next.js, первые два дня после деплоя были про CSS и перфоманс. На третий день пришёл тикет: «Авторизованные пользователи видят форму входа вместо личного кабинета».
Bitrix был жив. Next.js был жив. Логи ничего не кричали. Но авторизованный пользователь с точки зрения фронтенда был никем.
Авторизация в headless Bitrix — это не одна проблема. Это три, которые появляются по очереди, когда фронт и бэк живут на разных доменах. Рассказываю по очереди.
Как Bitrix работает с сессией в монолите
В классическом стеке всё просто. Bitrix хранит сессию в $_SESSION, браузер получает PHPSESSID cookie с флагами httpOnly; SameSite=Lax. Каждый следующий запрос браузер отправляет этот cookie обратно — Bitrix его читает, достаёт пользователя из сессии, всё работает.
Дополнительно Bitrix использует BX_SESSID — антиCSRF-токен. На все POST-запросы через REST API нужен заголовок X-Bitrix-Csrf-Token с этим значением. Без него — 403. Это не документировано явно, но это так.
В монолите ты об этом не думаешь. Все запросы на одном домене, браузер сам несёт cookie, CSRF генерируется и отправляется незаметно.
Что происходит на разных доменах
Фронт: shop.example.com. Бэк (Bitrix API): api.example.com.
Разные домены — разные правила для cookie. Браузер по умолчанию не отправит PHPSESSID на api.example.com, когда запрос идёт с shop.example.com. Это SameSite-политика. Для кросс-доменных cookie нужен флаг SameSite=None; Secure — что, в свою очередь, требует HTTPS на обоих доменах и явного разрешения в настройках Bitrix.
Мы настроили CORS на Bitrix (Access-Control-Allow-Origin, Access-Control-Allow-Credentials: true), выставили SameSite=None, и клиентские fetch-запросы с credentials: "include" заработали.
Это закрыло половину проблемы.
BX_SESSID: тихий убийца POST-запросов
Личный кабинет отображался. Но любое действие пользователя возвращало 403. Добавить в корзину, сохранить адрес, оформить заказ.
Причина: BX_SESSID. Этот токен Bitrix генерирует при инициализации сессии и ожидает в заголовке X-Bitrix-Csrf-Token на каждый POST. В монолите он подставляется автоматически через PHP-шаблон. В headless нужно получить его явно: отдельный GET-запрос к Bitrix при инициализации сессии, потом передавать в каждый POST.
Схема: при логине получаем BX_SESSID из ответа Bitrix (поле result.SESSID), сохраняем в состоянии фронтенда или cookie, добавляем в заголовок каждого мутирующего запроса.
Звучит несложно. Но документация этого не описывает. Поняли из access.log: запросы без этого заголовка тихо возвращали 403, без объяснений.
Третья проблема: SSR без клиентского контекста
Next.js умеет делать запросы к API на стороне сервера — в getServerSideProps или в Server Components. Это хорошо для SEO и скорости первой загрузки.
Но SSR делает запрос к Bitrix без cookie браузера. Сервер Next.js не знает, кто залогинен. Если вы хотите рендерить персонализированный контент на сервере (корзина, wishlist, персональные цены), нужна дополнительная механика.
Три варианта.
Первый — проброс cookie. В getServerSideProps читаем cookie из входящего запроса (req.cookies) и передаём в заголовке Cookie на Bitrix. Работает, но хрупко: cookie пользователя проходит через серверный код, при SSR в Vercel нужно аккуратно с безопасностью.
Второй — JWT. При логине Bitrix выдаёт JWT-токен (через кастомный endpoint или OAuth2). Next.js хранит его в httpOnly cookie, на сервере и на клиенте работает через Bearer. Bitrix-side нужна собственная JWT-валидация. Трудоёмко, но чисто.
Третий — гибридный: CSR для авторизованного контента. Персонализированные части рендерятся на клиенте. Статический контент (каталог, описания) — SSR. Личный кабинет рендерится на клиенте после гидрации.
Что выбрали мы — и почему
На проекте 28k SKU мы выбрали гибридный подход с пробросом cookie через SSR для некритичных случаев и CSR для персонализированного контента.
Конкретно:
- Каталог, описания товаров, категории — SSR без авторизации.
- Корзина, личный кабинет, персональные цены — клиентский рендеринг после гидрации.
- SSR с авторизацией оставили только для страниц, где SEO и персонализация пересекаются (их было ноль).
BX_SESSID получаем при инициализации сессии через отдельный GET, храним в памяти клиента, добавляем в заголовок.
Для SameSite=None потребовалась правка в настройках безопасности Bitrix (.settings.php) и явное добавление домена фронтенда в разрешённые CORS origins.
От JWT отказались: времени на реализацию кастомного endpoint не было, а стандартный OAuth2 у Bitrix рассчитан на внешние приложения, не на собственный фронтенд.
Что проверить перед запуском
Если у вас headless Bitrix + Next.js и есть авторизованные пользователи — вот рабочий чеклист:
- CORS на Bitrix настроен с
credentials: trueи явнымAllow-Origin(не*). - Cookie Bitrix выставлен с
SameSite=None; Secure— и на Bitrix, и на обоих доменах есть HTTPS. - При логине получаете
SESSIDиз ответа и сохраняете на клиенте. - POST-запросы через REST API несут заголовок
X-Bitrix-Csrf-Token: <BX_SESSID>. - SSR-запросы к Bitrix либо анонимные, либо с явным пробросом cookie из
req.cookies. - Проверьте личный кабинет в режиме инкогнито — без кэша браузера самая честная картина.
- Проверьте в браузере DevTools → Network → что cookie реально отправляется на Bitrix-домен (флаг
Includeу SameSite).
Три дня на то, чтобы разобраться в этом — это честная цена за то, что нигде не написано.
Следующий раз на предпроектной диагностике я спрашиваю: «Есть авторизованные пользователи в каталоге или это только для оформления?» Если да — закладываю ещё 3 дня в смету и явно объясняю клиенту, почему.
*Похожие задачи встречались при работе с API-контрактом между фронтом и Bitrix — про другие сюрпризы REST API читайте в Пять вещей, которые меня удивили в Bitrix REST API. Про то, что вообще оставить на Bitrix при headless — Что мы оставили на Bitrix.*