Communitygithub.com

rbikbov/frontend-starter

Autorisation et session côté frontend avec Next.js (App Router) : où stocker les tokens, BFF via les route handlers avec cookie HttpOnly, renouvellement de token sans concurrence, détection de « non autorisé » (y compris GraphQL qui répond en 200), vérifications dans proxy.ts et la couche d'accès aux données, redirection sécurisée après connexion, déconnexion sans fuite de données utilisateur, tokens dans les liens de réinitialisation de mot de passe, formulaires de connexion et de récupération. À utiliser lors de la conception ou de l'écriture de l'authentification, de l'inscription, de la réinitialisation du mot de passe, des pages protégées, d'un client API avec refresh token, ou lors de la revue de ce code.

Qu'est-ce que frontend-starter ?

Ce skill décrit les bonnes pratiques pour implémenter l'authentification et la gestion de session côté frontend avec Next.js (App Router). Il couvre le choix entre un modèle BFF (Backend for Frontend) via les route handlers de Next.js avec cookies HttpOnly, ou directement via le backend. Il détaille les règles strictes pour les cookies (HttpOnly, Secure, SameSite, préfixe __Host-), la mise en place d'un BFF avec validation Zod, la vérification de session à plusieurs niveaux (proxy.ts, Data Access Layer, route handlers, backend), le rafraîchissement de token sans conditions de course (single-flight), la détection de « non autorisé » y compris pour GraphQL, la redirection sécurisée après connexion, la déconnexion propre (nettoyage cache et stores, notification aux autres onglets), la gestion des tokens dans les liens de réinitialisation de mot de passe (retrait de l'URL, Referrer-Policy), et les formulaires d'authentification (protection contre le timing attack, gestion des erreurs). Le skill est destiné aux développeurs utilisant Next.js, recommandé pour les projets avec ou sans bibliothèque d'authentification (Auth.js, Better Auth, Clerk). Il s'applique à Claude Code, Cursor, Codex et autres agents IA.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Autorisation et session dans Next.js

Document sur le frontend qui reçoit une session du backend (tokens ou identifiant de session) et doit la stocker, la prolonger et la vérifier. Si le projet utilise une bibliothèque ou un fournisseur (Auth.js, Better Auth, Clerk, etc.), la mécanique de session est gérée par celui-ci, mais les règles des sections 2, 6 à 10 restent applicables. Code — references/implementation.md.

Priorité des règles. Les instructions de l'utilisateur et les fichiers agent du projet sont plus importants. Si le backend impose autre chose (par exemple, s'il place lui-même le cookie), suis le backend et note la différence dans le README ou un ADR.

Notations : MUST — ne pas enfreindre ; SHOULD — par défaut oui, une dérogation nécessite une justification.


1. Choix du modèle

ModèleQuand
BFF : les route handlers Next reçoivent les tokens du backend et les placent dans un cookie HttpOnlyLe backend renvoie les tokens dans le corps de la réponse. Option par défaut pour Next
Le backend place lui-même le cookie HttpOnlyFrontend et API sur le même site, backend sous ton contrôle
Bibliothèque ou fournisseurOAuth, MFA, gestion des utilisateurs nécessaires — ne les implémente pas toi-même
Access en mémoire, refresh dans un cookie HttpOnlySPA sans BFF
Tokens dans localStorage/sessionStorageInterdit : toute XSS emporte la session

Deuxième embranchement — ce que le BFF proxy :

OptionAvantagesInconvénients
Seulement login / refresh / logout, les autres requêtes vont directement à l'API avec le cookieLe BFF n'est pas un goulot d'étranglement ; moins de codeL'API lit le token depuis le cookie et configure CORS ; les cookies doivent atteindre le domaine de l'API
Toutes les requêtes via le BFFL'API ne connaît ni cookie ni CORS ; une seule origineChaque requête ajoute un saut ; le BFF doit être dimensionné

Le choix est consigné dans un ADR.


2. Cookie (MUST)

  • HttpOnly pour tout ce qui donne accès (access, refresh, id de session).
  • Secure en production. Localement, les navigateurs acceptent Secure sur http://localhost, mais pas sur d'autres hôtes http.
  • SameSite=Lax par défaut. Strict est plus robuste contre CSRF, mais l'utilisateur arrivant par un lien depuis un e-mail ou un autre site semble non authentifié lors de la première navigation — choisis en connaissance de cause et note la raison.
  • Path=/ ; durée de vie basée sur exp du token ou la réponse du backend, pas « à vue de nez ».
  • SHOULD : préfixe __Host- dans le nom (nécessite Secure, Path=/, sans Domain) — le cookie ne peut pas être usurpé depuis un sous-domaine.
  • Les cookies sont placés et supprimés uniquement côté serveur (route handler, Server Action, proxy.ts). Les Server Components ne peuvent pas placer de cookies.
  • Le cookie non lisible depuis JS est le seul vecteur de session. Un drapeau du type is_authenticated sans HttpOnly est uniquement autorisé comme indicateur pour l'UI et n'est jamais utilisé pour des décisions d'accès.

3. BFF : route handlers (MUST)

  • La connexion est validée par un schéma (zod) ; un corps mal formé renvoie 400.
  • Les handlers mutateurs vérifient Origin contre l'origine de l'application : SameSite=Lax ne protège pas contre les requêtes provenant de sous-domaines du même site. Les Server Actions comparent Origin avec Host eux-mêmes.
  • Le contexte client est transmis au backend : X-Forwarded-For, User-Agent — sinon le rate limit du backend ne voit que l'IP du serveur.
  • Les erreurs du backend sont traduites en réponse avec kind et un statut (401, 403, 422, 429 avec Retry-After, 503), le texte du backend n'est pas divulgué.
  • Refresh : si le backend rejette le token de refresh — supprimer le cookie et répondre 401. Si le backend est indisponible (réseau, 5xx) — ne pas toucher au cookie, répondre 503 : une panne temporaire ne doit pas déconnecter.
  • Logout : révoquer le token côté backend (l'erreur est journalisée mais pas affichée), supprimer le cookie dans tous les cas.

4. Où vérifier la session (MUST)

EndroitCe qu'il faitCe qu'il ne fait pas
proxy.ts (Next 16+, auparavant middleware.ts)Vérification optimiste basée sur la présence du cookie : redirection vers la connexion et depuis les pages de connexionN'interroge pas la BDD ni l'API ; n'est pas la seule protection
Data Access Layer (verifySession() dans un module server-only)Vérification près des données : dans les Server Components, route handlers, Server Actions—
LayoutRien : les layouts ne sont pas réaffichés lors de la navigationNe vérifie pas l'accès
BackendSource de vérité : vérifie le token à chaque requête—
  • Chaque route handler et Server Action est un point d'entrée public et appelle verifySession() lui-même.
  • proxy.ts vérifie le cookie de refresh (ou l'id de session) : l'access a pu expirer alors que la session est toujours active.
  • Correspondance du chemin par segments : /app et /app/…, mais pas /apple.
  • Avec cacheComponents (par défaut dans les projets Next 16.4), la lecture de la session et la mise en cache des données utilisateur ont leurs propres règles : avant d'écrire le DAL, lis node_modules/next/dist/docs/01-app/02-guides/authentication-with-cache-components.md.
  • Les droits (rôle, propriété) sont décidés en fonction des données vérifiées : via la signature (JWKS, jose) ou via la réponse du backend. Un payload JWT non vérifié n'est pas une base pour l'accès.
  • Les Server Components ne peuvent pas mettre à jour le cookie. Si l'access a expiré au moment du rendu serveur : soit proxy.ts renouvelle la session (il peut placer un cookie dans la réponse — fais-le uniquement pour les navigations document, pas pour le prefetch, et prends en compte la rotation des tokens de refresh), soit la page affiche un chargement côté client et le refresh est effectué côté client.

5. Client : refresh sans concurrence (MUST)

  • Détection de « non autorisé » : HTTP 401, ou dans une réponse GraphQL (statut 200) au moins une erreur avec extensions.code === "UNAUTHENTICATED" (errors.some, pas errors[0]). La comparaison avec le texte du message uniquement si le backend ne fournit pas de codes, dans une seule fonction, avec commentaire et test.
  • Single-flight : les requêtes parallèles recevant 401 attendent un seul refresh commun et utilisent son résultat. La promesse est réinitialisée dans finally.
  • La requête d'origine est rejouée une fois. Un deuxième 401 signifie une session perdue, sans deuxième refresh.
  • La requête de refresh elle-même ne passe pas par l'intercepteur (sinon récursion).
  • Le résultat du refresh distingue trois cas : accordé (true) → rejouer ; rejeté (false) → session expirée ; réseau ou 5xx (exception) → erreur network, sans déconnexion.
  • Pendant la déconnexion, le refresh est bloqué : sinon une requête tombant en 401 au moment du logout rétablirait immédiatement la session.
  • Session expirée : vider le cache des requêtes, rediriger vers la page de connexion avec callbackUrl de la page courante.
  • Le corps de la requête à rejouer doit être rejouable (chaîne, FormData), pas un ReadableStream à usage unique.

6. Redirection après connexion (MUST)

  • callbackUrl (next, returnTo) est vérifié par la fonction getSafeRedirect(raw, fallback, appOrigin) : elle rejette \ et les caractères de contrôle (les navigateurs transforment /\evil.com et /<tab>/evil.com en //evil.com), analyse new URL(raw, appOrigin), compare l'origine et ne renvoie que le chemin. La vérification « commence par / et non par // » est insuffisante.
  • La série de tests associée se trouve dans references/implementation.md, section 7.
  • Après la connexion, les Server Components doivent voir les nouveaux cookies : navigation complète (window.location.assign) ou router.replace + router.refresh().

7. Déconnexion (MUST)

  • Le serveur supprime le cookie (section 3).
  • Le client vide le cache des requêtes (queryClient.clear()) et tous les stores clients contenant les données de l'utilisateur : l'utilisateur suivant dans le même onglet ne doit pas voir les données d'autrui, ne serait-ce qu'un instant.
  • Navigation vers la page de connexion sans possibilité de revenir « en arrière » vers un écran protégé mis en cache (replace ou navigation complète).
  • Confirmation de déconnexion — selon le souhait du produit ; le bouton doit être désactivé pendant la requête — c'est obligatoire.
  • SHOULD : les autres onglets sont informés de la déconnexion (BroadcastChannel ou événement storage basé sur un indicateur), sinon ils continuent d'afficher les données jusqu'au premier 401.

8. Tokens dans les liens (réinitialisation de mot de passe, confirmation d'email)

  • Le token est lu depuis l'URL une fois dans un état, puis l'adresse est remplacée sans le token (router.replace(pathname)) : il ne reste pas dans l'historique et ne part pas dans le Referer.
  • Pour ces pages — Referrer-Policy: no-referrer et aucun script tiers (analytique, widgets).
  • Le token n'est pas journalisé. L'unicité d'utilisation et la durée de vie sont gérées par le backend ; le frontend affiche un écran explicite « lien expiré » avec une action « demander un nouveau lien ».

9. Formulaires de connexion et de récupération

  • La récupération de mot de passe répond de manière identique pour un email existant ou inexistant (« si le compte existe, un email a été envoyé »), sans révéler qui est inscrit. Si le backend divulgue cette information, la noter comme limitation.
  • L'envoi est bloqué pendant la requête ; un nouvel envoi ne crée pas une deuxième requête.
  • 429 : message avec le temps d'attente issu de Retry-After, s'il est présent.
  • Champs : type="email" + autoComplete="email" ; mot de passe — autoComplete="current-password" pour la connexion et "new-password" pour l'inscription et le changement ; la copie n'est pas bloquée.
  • Bouton « afficher le mot de passe » — type="button", aria-label et aria-pressed, ne vole pas le focus du champ.
  • L'erreur « email ou mot de passe incorrect » ne précise pas ce qui est faux.
  • Les erreurs du serveur sont associées aux champs par code ; une erreur générale est placée au-dessus du formulaire.

10. Données et journaux

  • Les réponses dépendantes de l'utilisateur ne sont pas mises dans les caches partagés ("use cache", CDN). QueryClient côté serveur est nouveau pour chaque requête.
  • Les journaux ne contiennent pas de tokens, mots de passe, emails. Au maximum — l'id de l'utilisateur et le kind de l'erreur.
  • Le payload du token de session est minimal : id et rôle, sans données personnelles.

11. Tests

  • Unitaire : getSafeRedirect (toute la série de références) ; détection de « non autorisé » (401, codes GraphQL, corps vide, HTML au lieu de JSON).
  • Single-flight : N requêtes parallèles avec 401 → un seul refresh, N rejeux ; refresh rejeté → aucun rejeu, un seul appel à onSessionExpired ; refresh échoue réseau → erreur network, sans déconnexion ; deuxième 401 après un refresh réussi → pas de deuxième refresh ; pendant le logout → pas de refresh.
  • Route handlers : drapeaux du cookie ; refresh rejeté → cookie supprimé ; backend indisponible → cookie conservé, 503 ; corps invalide → 400 ; Origin étranger → 403.
  • e2e : connexion → page protégée → déconnexion → page protégée inaccessible ; expiration de l'access (TTL court dans l'environnement de test) → refresh transparent ; lien de réinitialisation de mot de passe → token disparu de la barre d'adresse.

12. Definition of Done

  • Aucun token n'est lu depuis JS ; localStorage n'est pas utilisé pour la session.
  • Cookie : HttpOnly, Secure en prod, SameSite, Path, durée basée sur le token.
  • Refresh : rejet et indisponibilité du backend sont distingués.
  • Single-flight avec résultat, un seul rejeu, blocage pendant le logout — couvert par les tests.
  • proxy.ts — uniquement vérification optimiste ; DAL appelé dans les handlers, actions et Server Components.
  • getSafeRedirect avec l'ensemble complet des tests.
  • La déconnexion vide le cache et les stores clients.
  • Les tokens des liens sont retirés de la barre d'adresse.
  • Les journaux ne contiennent pas de tokens ni de données personnelles.
  • Le choix du modèle et du proxy est consigné dans 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 associés