Communitygithub.com

rbikbov/frontend-starter

Next.js (App Router) フロントエンドにおける認可とセッション管理:トークンの保存場所、HttpOnly-cookie を用いた BFF(route handlers)、競合のないリフレッシュトークン、「未認可」の判定(200 を返す GraphQL を含む)、proxy.ts と Data Access Layer での検証、ログイン後のセキュアなリダイレクト、過去のユーザーデータが漏洩しないログアウト、パスワードリセットリンク内のトークン、ログインフォームと復元フォーム。ログイン、登録、パスワード復元、保護ページ、リフレッシュトークンを用いた API クライアントの設計・実装時、またはそのコードレビュー時に使用します。

frontend-starter とは?

このスキルは、Next.js (App Router) のフロントエンドアプリケーションにおいて、セッションと認可を安全かつ堅牢に扱うための包括的なガイドです。主な焦点は、バックエンドから提供されるトークン(アクセストークン、リフレッシュトークン、セッションID)をフロントエンドで適切に保存、延長、検証する方法にあります。このガイドは、Auth.js や Clerk などの認証ライブラリを使用する場合でも、プロキシ(middleware.ts/proxy.ts)や Data Access Layer (DAL) でのセッション検証など、中核となる原則は適用されることを明確にしています。 このスキルは、BFF (Backend-for-Frontend) パターンを使用した推奨モデルを詳述し、Next.js の route handlers を介してトークンを HttpOnly Cookie として保存する方法を説明します。また、同一オリジンとクロスオリジンの要求に対する Cookie 属性(HttpOnly, Secure, SameSite)の適切な設定、競合状態を防止するシングルフライトリフレッシュメカニズム、ログイン後のセキュアなリダイレクト、ログアウト時のクライアントキャッシュとストアの完全なクリーンアップ、パスワードリセットリンク内のトークンの安全な処理、そしてフォーム実装のベストプラクティスについてもカバーしています。さらに、未認可状態の検出(HTTP 401 や GraphQL エラーコード)や、テスト戦略(単体テスト、統合テスト、e2e テスト)に関する具体的な実装ガイダンスも提供しています。

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

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Next.js における認可とセッション

このドキュメントは、バックエンドからセッション(トークンまたはセッションID)を受け取り、それを保存、延長、検証しなければならないフロントエンドに関するものです。プロジェクトがライブラリやプロバイダー(Auth.js、Better Auth、Clerk など)を使用している場合、セッションのメカニズムはそれらが担当しますが、セクション2、6~10のルールは引き続き有効です。コードは references/implementation.md を参照してください。

ルールの優先順位。 ユーザーの指示とプロジェクトのエージェントファイルがより重要です。バックエンドが別の方法を指示する場合(例:自ら Cookie を設定する)、バックエンドに従い、その違いを README または ADR に記録してください。

表記: MUST — 違反してはいけません; SHOULD — デフォルトでは yes、逸脱するには理由が必要です。


1. モデルの選択

モデルいつ使うか
BFF: Next の route handlers がバックエンドからトークンを受け取り、HttpOnly-cookie に保存するバックエンドが応答本文でトークンを返す場合。Next のデフォルトの選択肢
バックエンド自身が HttpOnly-cookie を設定するフロントエンドと API が同一サイトにあり、バックエンドを制御下に置いている場合
ライブラリまたはプロバイダーOAuth、MFA、ユーザー管理が必要な場合 — 自作しない
Access はメモリに、refresh は HttpOnly-cookie にBFF を持たない SPA
トークンを localStorage/sessionStorage に保存禁止: XSS が発生するとセッションが奪われる

2つ目の分岐点は BFF が何をプロキシするか です:

オプションメリットデメリット
login / refresh / logout のみをプロキシし、その他のリクエストは Cookie を持って直接 API へBFF がボトルネックにならない; コードが少ないAPI が Cookie からトークンを読み取り、CORS を設定する必要がある; Cookie が API のドメインまで到達する必要がある
全てのリクエストを BFF 経由で行うAPI が Cookie と CORS を意識しなくて済む; オリジンが1つ各リクエストに余分なホップが発生する; BFF をスケーリングする必要がある

選択は ADR に記録します。


2. Cookie (MUST)

  • アクセス権を与えるすべてのもの(access、refresh、セッションID)に対して HttpOnly にします。
  • 本番環境では Secure にします。ローカルでは、ブラウザは http://localhost での Secure を許可しますが、他の 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 をアプリケーションのオリジンと照合します: SameSite=Lax は同一サイトのサブドメインからのリクエストを防御しません。Server Actions は Origin と Host を自身で比較します。
  • クライアントコンテキストはバックエンドに転送します: X-Forwarded-For、User-Agent — そうしないと、バックエンドのレート制限はサーバーの IP しか認識しません。
  • バックエンドのエラーは kind とステータス(401、403、422、Retry-After 付き 429、503)を含む応答に変換し、バックエンドのテキストは外部に出さないでください。
  • リフレッシュ: バックエンドが refresh トークンを 拒否した 場合 — Cookie を削除し 401 を返します。バックエンドが 利用不可能な 場合(ネットワーク、5xx) — Cookie に触れず、503 を返します: 一時的な障害でログアウトさせてはいけません。
  • ログアウト: バックエンドでトークンを無効にし(エラーはログに記録しますが、表示しません)、いずれにしても Cookie を削除します。

4. セッションの確認場所 (MUST)

場所何をするか何をしないか
proxy.ts(Next 16+、以前は middleware.ts)Cookie の有無による楽観的チェック: ログインページへのリダイレクト、およびログインページからのリダイレクトデータベースや API にはアクセスしない; 唯一の防御ではない
Data Access Layer(server-only モジュール内の verifySession())データの近くで確認: サーバーコンポーネント、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)またはバックエンドの応答に基づきます。検証されていない JWT ペイロードをアクセスの根拠にしてはいけません。
  • Server Components は Cookie を更新できません。サーバーレンダリング時に access が期限切れの場合: proxy.ts がセッションを更新するか(応答に Cookie を設定できます — これはドキュメントナビゲーションの場合のみ行い、prefetch では行わず、refresh トークンのローテーションを考慮してください)、またはページがクライアントサイドのロードをレンダリングし、クライアントが refresh を行います。

5. クライアント: 競合のないリフレッシュ (MUST)

  • 「未認可」の判定: HTTP 401、または GraphQL 応答(200)内の少なくとも1つのエラーに extensions.code === "UNAUTHENTICATED"(errors.some、errors[0] ではない)がある場合。メッセージテキストとの比較は、バックエンドがコードを返さない場合に限り、1つの関数内で、コメントとテストを付けて行います。
  • シングルフライト: 401 を受け取った並行リクエストは、1つの共通リフレッシュを待ち、その結果を使用します。Promise は finally でリセットされます。
  • 元のリクエストは 1回だけ 再試行します。2回目の 401 はセッション喪失を意味し、2度目のリフレッシュは行いません。
  • リフレッシュリクエスト自体はインターセプターを通過しません(再帰を防ぐため)。
  • リフレッシュの結果は3つの結果を区別します: 発行された(true)→ 再試行; 拒否された(false)→ セッション期限切れ; ネットワークまたは5xx(例外)→ network エラー、アカウントからのログアウトは行いません。
  • ログアウト中はリフレッシュがブロックされます: そうしないと、ログアウト時に 401 で落ちたリクエストがすぐにセッションを復元してしまいます。
  • 期限切れセッション: リクエストキャッシュをクリアし、現在のページの callbackUrl を付けてログインページに移動します。
  • 再試行するリクエストの本文は再試行可能(文字列、FormData)である必要があり、使い捨ての ReadableStream であってはいけません。

6. ログイン後のリダイレクト (MUST)

  • callbackUrl(next、returnTo)は getSafeRedirect(raw, fallback, appOrigin) 関数で検証します: \ および制御文字(ブラウザは /\evil.com や /<tab>/evil.com を //evil.com に変換します)を拒否し、new URL(raw, appOrigin) を解析し、オリジンを比較してパスのみを返します。 「/ で始まり // で始まらない」というチェックは 不十分 です。
  • そのテストスイートは references/implementation.md、セクション7にあります。
  • ログイン後、サーバーコンポーネントは新しい Cookie を認識する必要があります: 完全なナビゲーション(window.location.assign)または router.replace + router.refresh() を使用します。

7. ログアウト (MUST)

  • サーバーは Cookie を削除します(セクション3)。
  • クライアントはリクエストキャッシュ(queryClient.clear())とユーザーデータを含むすべてのクライアントストアをクリアします: 同じタブの次のユーザーが、一瞬でも他人のデータを見ることがあってはいけません。
  • キャッシュされた保護画面に「戻る」ことができないように、ログインページへのナビゲーションは(replace または完全なナビゲーションで)行います。
  • ログアウトの確認はプロダクトの任意; リクエスト中のボタン無効化は必須です。
  • SHOULD: 他のタブがログアウトを認識する(BroadcastChannel またはフラグによる storage イベント) — そうしないと、最初の 401 が発生するまでデータを表示し続けます。

8. リンク内のトークン(パスワードリセット、メール確認)

  • トークンは URL から 1回だけ 状態に読み込まれ、その後、アドレスからトークンが削除されます(router.replace(pathname)): 履歴に残らず、Referer に送信されることもありません。
  • これらのページでは Referrer-Policy: no-referrer を設定し、サードパーティのスクリプト(分析、ウィジェット)は一切使用しません。
  • トークンはログに記録しません。トークンの1回限りの使用と有効期限はバックエンドが保証します; フロントエンドは「リンクの有効期限が切れました」という明確な画面と「新しいリンクをリクエスト」アクションを表示します。

9. ログインフォームと復元フォーム

  • パスワード復元は、登録済みメールアドレスと未登録メールアドレスで同じ応答を返します(「アカウントが存在する場合、メールが送信されました」)、誰が登録されているかを明かしません。バックエンドが漏らす場合は、制限事項として記録します。
  • 送信はリクエスト中ブロックされます; 再送信は2回目のリクエストを作成しません。
  • 429: Retry-After があれば、その待機時間を含むメッセージを表示します。
  • フィールド: type="email" + autoComplete="email"; パスワードはログイン時 autoComplete="current-password"、登録と変更時は "new-password"; ペーストはブロックしません。
  • 「パスワードを表示」ボタンは type="button"、aria-label と aria-pressed を設定し、フィールドからフォーカスを奪いません。
  • 「メールアドレスまたはパスワードが間違っています」というエラーは、何が間違っているのかを特定しません。
  • サーバーエラーはコードに基づいてフィールドにマッピングします; 一般的なエラーはフォームの上に表示します。

10. データとログ

  • ユーザーに依存する応答は、共有キャッシュ("use cache"、CDN)に入れてはいけません。サーバー上の QueryClient はリクエストごとに新しくします。
  • ログにはトークン、パスワード、メールアドレスを含めてはいけません。最大でもユーザーIDとエラーの kind のみです。
  • セッショントークンのペイロードは最小限に: ID とロールのみで、個人データは含めません。

11. テスト

  • 単体テスト: getSafeRedirect(references からの全スイート); 「未認可」の判定(401、GraphQL コード、空の本文、JSON の代わりに HTML)。
  • シングルフライト: 401 で N 個の並行リクエスト → 1 回のリフレッシュ、N 回の再試行; リフレッシュが拒否される → 再試行なし、onSessionExpired が1回呼ばれる; リフレッシュがネットワークエラーで落ちる → network エラー、ログアウトなし; リフレッシュ成功後の2度目の 401 → 2度目のリフレッシュなし; ログアウト中 → リフレッシュなし。
  • Route handlers: Cookie フラグ; リフレッシュ拒否 → Cookie 削除; バックエンド利用不可 → Cookie 維持、503; 不正な本文 → 400; 異なる Origin → 403。
  • e2e: ログイン → 保護ページ → ログアウト → 保護ページが利用不可; access の期限切れ(テスト環境で短い TTL) → 透過的なリフレッシュ; パスワードリセットリンク → トークンがアドレスバーから消える。

12. Definition of Done

  • どのトークンも JS から読み取られない; localStorage はセッションに使用されない。
  • Cookie: HttpOnly、本番で Secure、SameSite、Path、有効期限はトークンから。
  • リフレッシュ: バックエンドの拒否と利用不可が区別される。
  • 結果を伴うシングルフライト、1回の再試行、ログアウト中のブロック — テストでカバーされている。
  • 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; навести порядок в молодом проекте после генератора.

関連スキル