Communitygithub.com

rbikbov/frontend-starter

Autorización y sesión en el frontend con Next.js (App Router): dónde almacenar tokens, BFF con route handlers usando HttpOnly-cookie, renovación de tokens sin condiciones de carrera, detección de «no autorizado» (incluyendo GraphQL que responde 200), verificaciones en proxy.ts y capa de acceso a datos, redirección segura tras inicio de sesión, cierre de sesión sin fugas de datos del usuario anterior, tokens en enlaces de restablecimiento de contraseña, formularios de inicio de sesión y recuperación. Úsalo cuando diseñes o implementes inicio de sesión, registro, recuperación de contraseña, páginas protegidas, un cliente API con refresh token, o revises dicho código.

¿Qué es frontend-starter?

Este skill proporciona una guía completa y técnica (basada en Next.js con App Router) para implementar autenticación y gestión de sesión robusta en el frontend, especialmente cuando el backend entrega tokens. Su valor principal radica en detallar un patrón BFF (Backend For Frontend) con route handlers que almacenan tokens en cookies HttpOnly, garantizando que ningún token sea accesible desde JavaScript y mitigando ataques XSS. El skill cubre aspectos críticos como la renovación de tokens sin condiciones de carrera mediante single-flight, la definición de «no autorizado» tanto en respuestas HTTP 401 como en GraphQL, la verificación optimista en proxy.ts y la verificación real en la capa de acceso a datos (DAL), la redirección segura tras inicio de sesión, el cierre de sesión con limpieza de caché y stores del cliente, y la correcta eliminación de tokens de las URLs de restablecimiento de contraseña para evitar fugas. Es útil para desarrolladores que estén diseñando o implementando sistemas de autenticación en Next.js y necesitan una referencia detallada y aplicable para evitar vulnerabilidades comunes.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Autorización y sesión en Next.js

Documento sobre el frontend que recibe la sesión del backend (tokens o id de sesión) y debe almacenarla, renovarla y verificarla. Si el proyecto utiliza una librería o proveedor (Auth.js, Better Auth, Clerk, etc.), la mecánica de la sesión la asume, pero las reglas de las secciones 2, 6–10 siguen vigentes. Código — references/implementation.md.

Prioridad de reglas. Las instrucciones del usuario y los archivos agent del proyecto son más importantes. Si el backend dicta otra cosa (por ejemplo, él mismo coloca la cookie), sigue al backend y anota la diferencia en el README o ADR.

Notaciones: MUST — no se puede violar; SHOULD — por defecto sí, la excepción requiere motivo.


1. Elección del modelo

ModeloCuándo
BFF: los route handlers de Next reciben los tokens del backend y los colocan en HttpOnly-cookieEl backend entrega tokens en el cuerpo de la respuesta. Opción por defecto para Next
El propio backend coloca la HttpOnly-cookieFrontend y API en el mismo sitio, el backend está bajo tu control
Librería o proveedorSe necesitan OAuth, MFA, gestión de usuarios — no lo implementes tú mismo
Access en memoria, refresh en HttpOnly-cookieSPA sin BFF
Tokens en localStorage/sessionStorageProhibido: cualquier XSS se lleva la sesión

Segunda bifurcación — qué proxea el BFF:

OpciónVentajasDesventajas
Solo login / refresh / logout, las demás solicitudes van directamente a la API con la cookieBFF no es cuello de botella; menos códigoLa API lee el token de la cookie y configura CORS; las cookies deben llegar al dominio de la API
Todas las solicitudes a través del BFFLa API no sabe de cookies ni CORS; un solo origenCada solicitud es un salto extra; el BFF debe escalarse

La elección se registra en un ADR.


2. Cookie (MUST)

  • HttpOnly para todo lo que concede acceso (access, refresh, id de sesión).
  • Secure en producción. Localmente los navegadores permiten Secure en http://localhost, pero no en otros hosts http.
  • SameSite=Lax por defecto. Strict es más seguro contra CSRF, pero un usuario que llega por un enlace de un correo o desde otro sitio, en la primera transición parece no autenticado — elígelo conscientemente y anota el motivo.
  • Path=/; tiempo de vida — del exp del token o de la respuesta del backend, no «a ojo».
  • SHOULD: prefijo __Host- en el nombre (requiere Secure, Path=/, sin Domain) — la cookie no puede ser sustituida desde un subdominio.
  • Las cookies se colocan y se eliminan solo en el servidor (route handler, Server Action, proxy.ts). Los Server Components no pueden colocar cookies.
  • La cookie no legible desde JS es el único portador de la sesión. Una bandera del tipo is_authenticated sin HttpOnly solo está permitida como indicación para la UI y nunca se usa para decisiones de acceso.

3. BFF: route handlers (MUST)

  • El inicio de sesión se valida con un esquema (zod); cuerpo con forma incorrecta — 400.
  • Los handlers mutantes verifican Origin contra el origen de la aplicación: SameSite=Lax no protege contra solicitudes desde subdominios del mismo sitio. Las Server Actions comparan Origin con Host por sí mismas.
  • El contexto del cliente se propaga al backend: X-Forwarded-For, User-Agent — de lo contrario, el rate limit del backend solo ve la IP del servidor.
  • Los errores del backend se traducen a una respuesta con kind y estado (401, 403, 422, 429 con Retry-After, 503), el texto del backend no sale al exterior.
  • Refresh: si el backend rechazó el refresh token — eliminar la cookie y responder 401. Si el backend no está disponible (red, 5xx) — no tocar la cookie, responder 503: un fallo temporal no debe cerrar la sesión.
  • Logout: revocar el token en el backend (el error se registra, pero no se muestra), la cookie se elimina en cualquier caso.

4. Dónde verificar la sesión (MUST)

LugarQué haceQué no hace
proxy.ts (Next 16+, antes middleware.ts)Verificación optimista por presencia de cookie: redirigir al login y desde páginas de loginNo accede a la BD ni a la API; no es la única protección
Data Access Layer (verifySession() en módulo server-only)Verificación junto a los datos: en Server Components, route handlers, Server Actions—
LayoutNada: los layouts no se redibujan con la navegaciónNo verifica acceso
BackendFuente de verdad: verifica el token en cada solicitud—
  • Cada route handler y Server Action es un punto de entrada público y por sí mismo llama a verifySession().
  • proxy.ts verifica la refresh-cookie (o id de sesión): el access pudo haber expirado, pero la sesión sigue viva.
  • Coincidencia de ruta — por segmentos: /app y /app/…, pero no /apple.
  • Con cacheComponents (por defecto en proyectos con Next 16.4), la lectura de la sesión y el almacenamiento en caché de los datos del usuario tienen sus propias reglas: antes de escribir un DAL, lee node_modules/next/dist/docs/01-app/02-guides/authentication-with-cache-components.md.
  • Los permisos (rol, propiedad) se deciden según datos verificados: por firma (JWKS, jose) o por la respuesta del backend. Un payload JWT no verificado no es base para el acceso.
  • Los Server Components no pueden actualizar la cookie. Si el access caduca durante el renderizado del servidor: o bien proxy.ts renueva la sesión (puede colocar cookie en la respuesta — hazlo solo para navegaciones de documento, no para prefetch, y ten en cuenta la rotación de refresh tokens), o la página renderiza una carga cliente y el refresh lo hace el cliente.

5. Cliente: refresh sin condiciones de carrera (MUST)

  • Definición de «no autorizado»: HTTP 401, o en la respuesta GraphQL (es 200) al menos un error con extensions.code === "UNAUTHENTICATED" (errors.some, no errors[0]). La comparación con el texto del mensaje solo si el backend no entrega códigos, en una única función, con comentario y test.
  • Single-flight: las solicitudes paralelas que reciben 401 esperan un único refresh común y usan su resultado. La promesa se limpia en finally.
  • La solicitud original se repite una vez. Un segundo 401 — sesión perdida, sin segundo refresh.
  • La propia solicitud de refresh no pasa por el interceptor (si no, recursión).
  • El resultado del refresh distingue tres casos: concedido (true) → repetir; rechazado (false) → sesión caducada; red o 5xx (excepción) → error network, sin cerrar sesión.
  • Durante el logout, el refresh está bloqueado: de lo contrario, una solicitud que cae en 401 en el momento del logout restablecería la sesión inmediatamente.
  • Sesión caducada: limpiar caché de solicitudes, redirigir al login con callbackUrl de la página actual.
  • El cuerpo de la solicitud que se repite debe ser repetible (string, FormData), no un ReadableStream de un solo uso.

6. Redirección después del login (MUST)

  • callbackUrl (next, returnTo) se verifica con la función getSafeRedirect(raw, fallback, appOrigin): rechaza \ y caracteres de control (los navegadores convierten /\evil.com y /<tab>/evil.com en //evil.com), parsea new URL(raw, appOrigin), compara el origen y devuelve solo la ruta. La verificación «empieza con / y no con //» es insuficiente.
  • El conjunto de tests para ella — references/implementation.md, sección 7.
  • Después del login, los Server Components deben ver las nuevas cookies: navegación completa (window.location.assign) o router.replace + router.refresh().

7. Cierre de sesión (MUST)

  • El servidor elimina la cookie (sección 3).
  • El cliente limpia la caché de solicitudes (queryClient.clear()) y todos los stores del cliente con datos del usuario: el siguiente usuario en la misma pestaña no debe ver datos ajenos ni siquiera por un instante.
  • Navegación a la página de login sin posibilidad de volver «atrás» a una pantalla protegida en caché (replace o navegación completa).
  • Confirmación de cierre de sesión — opcional según el producto; bloqueo del botón durante la solicitud — obligatorio.
  • SHOULD: otras pestañas se enteran del cierre de sesión (BroadcastChannel o evento storage a través de una bandera), si no, siguen mostrando datos hasta el primer 401.

8. Tokens en enlaces (restablecimiento de contraseña, confirmación de email)

  • El token se lee de la URL una vez en un estado, luego la dirección se reemplaza sin el token (router.replace(pathname)): no queda en el historial y no se envía en Referer.
  • Para estas páginas — Referrer-Policy: no-referrer y ningún script de terceros (analítica, widgets).
  • El token no se registra en logs. La caducidad y el uso único los garantiza el backend; el frontend muestra una pantalla clara de «enlace caducado» con la acción «solicitar uno nuevo».

9. Formularios de inicio de sesión y recuperación

  • La recuperación de contraseña responde igual para un email existente y uno inexistente («si la cuenta existe, se ha enviado un correo»), sin revelar quién está registrado. Si el backend revela información — anotarlo como limitación.
  • El envío se bloquea durante la solicitud; el reenvío no crea una segunda solicitud.
  • 429: mensaje con tiempo de espera de Retry-After, si está presente.
  • Campos: type="email" + autoComplete="email"; contraseña — autoComplete="current-password" en inicio de sesión y "new-password" en registro y cambio; no se bloquea la inserción.
  • Botón «mostrar contraseña» — type="button", aria-label y aria-pressed, no quita el foco del campo.
  • El error «email o contraseña incorrectos» no especifica qué es incorrecto.
  • Los errores del servidor se asignan a los campos según el código; el error general — sobre el formulario.

10. Datos y logs

  • Las respuestas que dependen del usuario no entran en cachés compartidos ("use cache", CDN). QueryClient en el servidor — nuevo por cada solicitud.
  • En los logs no hay tokens, contraseñas, emails. Como máximo — id de usuario y kind del error.
  • El payload del token de sesión es mínimo: id y rol, sin datos personales.

11. Tests

  • Unitario: getSafeRedirect (todo el conjunto de references); definición de «no autorizado» (401, códigos GraphQL, cuerpo vacío, HTML en lugar de JSON).
  • Single-flight: N solicitudes paralelas con 401 → un refresh, N repeticiones; refresh rechazado → ninguna repetición, una llamada a onSessionExpired; refresh falla por red → error network, sin cerrar sesión; segundo 401 tras refresh exitoso → sin segundo refresh; durante logout → sin refresh.
  • Route handlers: flags de cookie; refresh rechazado → cookie eliminada; backend no disponible → cookie intacta, 503; cuerpo inválido → 400; Origin ajeno → 403.
  • e2e: inicio de sesión → página protegida → cierre de sesión → página protegida no accesible; expiración de access (TTL corto en entorno de pruebas) → refresh transparente; enlace de restablecimiento de contraseña → token desaparece de la barra de direcciones.

12. Definition of Done

  • Ningún token se lee desde JS; no se usa localStorage para la sesión.
  • Cookie: HttpOnly, Secure en prod, SameSite, Path, tiempo de vida del token.
  • Refresh: se distingue entre rechazo e indisponibilidad del backend.
  • Single-flight con resultado, una repetición, bloqueo durante logout — cubierto con tests.
  • proxy.ts — solo verificación optimista; DAL se llama en handlers, actions y Server Components.
  • getSafeRedirect con el conjunto completo de tests.
  • El cierre de sesión limpia la caché y los stores del cliente.
  • Los tokens de los enlaces se eliminan de la barra de direcciones.
  • En los logs no hay tokens ni datos personales.
  • La elección del modelo y del proxy está registrada en un 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; навести порядок в молодом проекте после генератора.

Skills relacionados