Communitygithub.com

rbikbov/frontend-starter

Авторизация и сессия во фронтенде на Next.js (App Router): где хранить токены, BFF на route handlers с HttpOnly-cookie, refresh токена без гонок, определение «не авторизован» (включая GraphQL, который отвечает 200), проверки в proxy.ts и Data Access Layer, безопасный редирект после входа, выход без утечки данных прошлого пользователя, токены в ссылках сброса пароля, формы входа и восстановления. Использовать, когда проектируешь или пишешь вход, регистрацию, восстановление пароля, защищённые страницы, API-клиент с refresh-токеном, или ревьюишь такой код.

What is frontend-starter?

frontend-starter is a Claude Code agent skill that авторизация и сессия во фронтенде на Next.js (App Router): где хранить токены, BFF на route handlers с HttpOnly-cookie, refresh токена без гонок, определение «не авторизован» (включая GraphQL, который отвечает 200), проверки в proxy.ts и Data Access Layer, безопасный редирект после входа, выход без утечки данных прошлого пользователя, токены в ссылках сброса пароля, формы входа и восстановления. Использовать, когда проектируешь или пишешь вход, регистрацию, восстановление пароля, защищённые страницы, API-клиент с refresh-токеном, или ревьюишь такой код.

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/rbikbov/frontend-starter/tree/HEAD/.claude/skills/frontend-auth-session

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Авторизация и сессия в Next.js

Документ про фронтенд, который получает сессию от бэкенда (токены или сессионный id) и должен хранить её, продлевать и проверять. Если проект использует библиотеку или провайдера (Auth.js, Better Auth, Clerk и т. п.) — механику сессии берёт она, но правила разделов 2, 6–10 остаются в силе. Код — references/implementation.md.

Приоритет правил. Инструкции пользователя и agent-файлы проекта важнее. Если бэкенд диктует иное (например, сам ставит cookie) — следуй бэкенду и запиши отличие в README или ADR.

Обозначения: MUST — нарушать нельзя; SHOULD — по умолчанию да, отступление требует причины.


1. Выбор модели

МодельКогда
BFF: route handlers Next получают токены от бэкенда и кладут в HttpOnly-cookieБэкенд отдаёт токены в теле ответа. Вариант по умолчанию для Next
Бэкенд сам ставит HttpOnly-cookieФронтенд и API на одном сайте, бэкенд под твоим контролем
Библиотека или провайдерНужны OAuth, MFA, управление пользователями — не пиши это сам
Access в памяти, refresh в HttpOnly-cookieSPA без BFF
Токены в localStorage/sessionStorageЗапрещено: любой XSS уносит сессию

Вторая развилка — что проксирует BFF:

ВариантПлюсыМинусы
Только login / refresh / logout, остальные запросы идут на API напрямую с cookieBFF не узкое место; меньше кодаAPI читает токен из cookie и настраивает CORS; cookie должны долетать до домена API
Все запросы через BFFAPI не знает про cookie и CORS; один originКаждый запрос — лишний хоп; BFF надо масштабировать

Выбор записывается в ADR.


2. Cookie (MUST)

  • HttpOnly для всего, что даёт доступ (access, refresh, id сессии).
  • Secure в продакшене. Локально браузеры разрешают Secure на http://localhost, но не на других http-хостах.
  • SameSite=Lax по умолчанию. Strict надёжнее против CSRF, но пользователь, пришедший по ссылке из письма или с другого сайта, на первом переходе выглядит неавторизованным — выбирай осознанно и запиши причину.
  • Path=/; срок жизни — из exp токена или из ответа бэкенда, не «на глаз».
  • SHOULD: префикс __Host- в имени (требует Secure, Path=/, без Domain) — cookie нельзя подменить с поддомена.
  • Cookie ставятся и удаляются только на сервере (route handler, Server Action, proxy.ts). Server Components cookie ставить не могут.
  • Нечитаемая из JS cookie — единственный носитель сессии. Флаг вида is_authenticated без HttpOnly допустим только как подсказка для UI и никогда не используется для решений о доступе.

3. BFF: route handlers (MUST)

  • Вход валидируется схемой (zod); тело с неверной формой — 400.
  • Мутирующие обработчики проверяют Origin против origin приложения: SameSite=Lax не защищает от запросов с поддоменов того же сайта. Server Actions сравнивают Origin с Host сами.
  • Клиентский контекст пробрасывается бэкенду: X-Forwarded-For, User-Agent — иначе rate limit бэкенда видит только IP сервера.
  • Ошибки бэкенда переводятся в ответ с kind и статусом (401, 403, 422, 429 с Retry-After, 503), текст бэкенда наружу не уходит.
  • Refresh: если бэкенд отклонил refresh-токен — удалить cookie и ответить 401. Если бэкенд недоступен (сеть, 5xx) — cookie не трогать, ответить 503: временный сбой не должен разлогинивать.
  • Logout: отозвать токен на бэкенде (ошибку залогировать, но не показывать), cookie удалить в любом случае.

4. Где проверять сессию (MUST)

МестоЧто делаетЧего не делает
proxy.ts (Next 16+, раньше middleware.ts)Оптимистичная проверка по наличию cookie: редирект на вход и со страниц входаНе ходит в БД и API; не единственная защита
Data Access Layer (verifySession() в server-only модуле)Проверка рядом с данными: в серверных компонентах, route handlers, Server Actions—
ЛейаутНичего: лейауты не перерисовываются при навигацииНе проверяет доступ
БэкендИсточник истины: проверяет токен на каждом запросе—
  • Каждый route handler и Server Action — публичная точка входа и сам вызывает verifySession().
  • proxy.ts проверяет refresh-cookie (или id сессии): access мог истечь, а сессия жива.
  • Совпадение пути — по сегментам: /app и /app/…, но не /apple.
  • С cacheComponents (по умолчанию в проектах на Next 16.4) у чтения сессии и кэширования данных пользователя свои правила: до того как писать DAL, прочитай node_modules/next/dist/docs/01-app/02-guides/authentication-with-cache-components.md.
  • Права (роль, владение) решаются по проверенным данным: по подписи (JWKS, jose) или по ответу бэкенда. Непроверенный payload JWT — не основание для доступа.
  • Server Components не могут обновить cookie. Если access истёк к моменту серверного рендера: либо proxy.ts обновляет сессию (он может ставить cookie в ответ — делай это только для навигаций документа, не для prefetch, и учти ротацию refresh-токенов), либо страница рендерит клиентскую загрузку и refresh делает клиент.

5. Клиент: refresh без гонок (MUST)

  • Определение «не авторизован»: HTTP 401, либо в GraphQL-ответе (он 200) хотя бы одна ошибка с extensions.code === "UNAUTHENTICATED" (errors.some, не errors[0]). Сравнение с текстом сообщения — только если бэкенд не отдаёт кодов, в одной функции, с комментарием и тестом.
  • Single-flight: параллельные запросы, получившие 401, ждут один общий refresh и используют его результат. Промис сбрасывается в finally.
  • Исходный запрос повторяется один раз. Повторный 401 — сессия потеряна, без второго refresh.
  • Сам запрос refresh не проходит через перехватчик (иначе рекурсия).
  • Результат refresh различает три исхода: выдан (true) → повторить; отклонён (false) → сессия истекла; сеть или 5xx (исключение) → ошибка network, без выхода из аккаунта.
  • Во время выхода refresh заблокирован: иначе запрос, упавший с 401 в момент logout, тут же восстановит сессию.
  • Истекшая сессия: очистить кэш запросов, увести на вход с callbackUrl текущей страницы.
  • Тело повторяемого запроса должно быть повторяемым (строка, FormData), не одноразовый ReadableStream.

6. Редирект после входа (MUST)

  • callbackUrl (next, returnTo) проверяется функцией getSafeRedirect(raw, fallback, appOrigin): отклоняет \ и управляющие символы (браузеры превращают /\evil.com и /<tab>/evil.com в //evil.com), разбирает new URL(raw, appOrigin), сравнивает origin и возвращает только путь. Проверка «начинается с / и не с //» — недостаточна.
  • Набор тестов на неё — references/implementation.md, раздел 7.
  • После входа серверные компоненты должны увидеть новые cookie: полная навигация (window.location.assign) или router.replace + router.refresh().

7. Выход (MUST)

  • Сервер удаляет cookie (раздел 3).
  • Клиент очищает кэш запросов (queryClient.clear()) и все клиентские сторы с данными пользователя: следующий пользователь в той же вкладке не должен увидеть чужие данные даже на мгновение.
  • Навигация на страницу входа без возможности вернуться «назад» в закэшированный защищённый экран (replace или полная навигация).
  • Подтверждение выхода — по желанию продукта; блокировка кнопки на время запроса — обязательно.
  • SHOULD: другие вкладки узнают о выходе (BroadcastChannel или событие storage по флагу), иначе они продолжают показывать данные до первого 401.

8. Токены в ссылках (сброс пароля, подтверждение email)

  • Токен читается из URL один раз в состояние, затем адрес заменяется без токена (router.replace(pathname)): он не остаётся в истории и не уезжает в Referer.
  • Для этих страниц — Referrer-Policy: no-referrer и никаких сторонних скриптов (аналитика, виджеты).
  • Токен не логируется. Одноразовость и срок жизни обеспечивает бэкенд; фронтенд показывает понятный экран «ссылка устарела» с действием «запросить новую».

9. Формы входа и восстановления

  • Восстановление пароля отвечает одинаково для существующего и несуществующего email («если аккаунт есть, письмо отправлено»), не раскрывая, кто зарегистрирован. Если бэкенд раскрывает — записать как ограничение.
  • Отправка блокируется на время запроса; повторная отправка не создаёт второй запрос.
  • 429: сообщение с временем ожидания из Retry-After, если оно есть.
  • Поля: type="email" + autoComplete="email"; пароль — autoComplete="current-password" при входе и "new-password" при регистрации и смене; вставка не блокируется.
  • Кнопка «показать пароль» — type="button", aria-label и aria-pressed, не забирает фокус у поля.
  • Ошибка «неверный email или пароль» не уточняет, что именно неверно.
  • Ошибки сервера сопоставляются полям по коду; общая — над формой.

10. Данные и логи

  • Ответы, зависящие от пользователя, не попадают в общие кэши ("use cache", CDN). QueryClient на сервере — новый на каждый запрос.
  • В логах нет токенов, паролей, email. Максимум — id пользователя и kind ошибки.
  • Payload сессионного токена минимален: id и роль, без персональных данных.

11. Тесты

  • Unit: getSafeRedirect (весь набор из references); определение «не авторизован» (401, GraphQL-коды, пустое тело, HTML вместо JSON).
  • Single-flight: N параллельных запросов с 401 → один refresh, N повторов; refresh отклонён → ни одного повтора, один вызов onSessionExpired; refresh упал по сети → ошибка network, без выхода; повторный 401 после успешного refresh → без второго refresh; во время logout → без refresh.
  • Route handlers: флаги cookie; refresh отклонён → cookie удалены; бэкенд недоступен → cookie на месте, 503; неверное тело → 400; чужой Origin → 403.
  • e2e: вход → защищённая страница → выход → защищённая страница недоступна; истечение access (короткий TTL в тестовом окружении) → прозрачный refresh; ссылка сброса пароля → токен исчез из адресной строки.

12. Definition of Done

  • Ни один токен не читается из JS; localStorage не используется для сессии.
  • Cookie: HttpOnly, Secure в проде, SameSite, Path, срок из токена.
  • Refresh: отклонение и недоступность бэкенда различаются.
  • Single-flight с результатом, один повтор, блокировка во время logout — покрыто тестами.
  • proxy.ts — только оптимистичная проверка; DAL вызывается в handlers, actions и серверных компонентах.
  • getSafeRedirect с полным набором тестов.
  • Выход очищает кэш и сторы клиента.
  • Токены из ссылок убираются из адресной строки.
  • В логах нет токенов и персональных данных.
  • Выбор модели и прокси записан в ADR.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

rbikbov/frontend-starter

Creates Architecture Decision Records documenting key technical decisions with context, alternatives considered, tradeoffs, consequences, and decision owners. Use when documenting "architecture decisions", "technical choices", "design decisions", or "ADRs".

rbikbov/frontend-starter

Проверка фронтенд-кода и документации на «нейрослоп» перед коммитом, PR или публикацией репозитория: остатки генераторов, мёртвые и недоделанные опции, утверждения в README и комментариях, не подтверждённые кодом, маркетинговый тон и эмодзи, лишние абстракции, хрупкая обработка ошибок, персональные данные в логах, несуществующие классы стилей. Использовать, когда просят проверить код или репозиторий «на слоп», подготовить проект к публикации или показу, сделать ревью кода, написанного с помощью ИИ, или перед тем как закоммитить собственную большую правку.

rbikbov/frontend-starter

Процесс разработки фичи с агентом: PRD с Non-Goals → компактная спецификация → задачи по одной на коммит → read-only исследование → tracer bullet → сборка с тестом сначала → независимое ревью субагентом → проверка в браузере. Использовать, когда просят сделать фичу, экран, пользовательский сценарий или заметное изменение поведения; когда нужно написать PRD, спецификацию или разбить работу на задачи; когда работа больше одного коммита. Не использовать для опечаток и однострочных правок.

rbikbov/frontend-starter

Старт нового фронтенд-проекта на Next.js (App Router) + TypeScript: фундамент, который закладывается до первой фичи. Использовать, когда нужно: создать проект с нуля; решить, нужны ли Next.js, монорепо, BFF, микрофронтенды; разложить FSD поверх роутинга Next; завести строгий TS, линтер границ, env-конфиг, модель ошибок и API-клиент, тесты, git-хуки, CI, каркас README; навести порядок в молодом проекте после генератора.

Related Skills