レビュー済み·難易度: 中級·更新: 2026-04-18

Next.js App Router の layout で認証チェックして配下ページを保護する

App Router の layout.tsx でセッション Cookie を確認し、未認証ユーザーをログインページへ redirect することで配下のすべてのページを一括保護する例。

nextjsauthenticationrouting

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router の layout 構造と cookies() の基本を理解していること

概要

app/(dashboard)/layout.tsx でセッション Cookie を確認し、未認証ユーザーをログインページへ redirect() する。このレイアウト配下に置いたページはすべて自動的に認証保護される。ミドルウェアを使わずに特定ルートグループだけを保護したい場合に有効なパターン。

インストール

# 追加インストールは不要

実装

ディレクトリ構成

app/
├── (public)/
│   ├── login/
│   │   └── page.tsx       # 公開ページ
│   └── layout.tsx
├── (dashboard)/
│   ├── layout.tsx         # ← ここで認証チェック
│   ├── page.tsx           # /dashboard
│   ├── settings/
│   │   └── page.tsx       # /settings(自動保護)
│   └── profile/
│       └── page.tsx       # /profile(自動保護)
└── layout.tsx             # ルートレイアウト

保護レイアウト(認証チェック)

// app/(dashboard)/layout.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import type { ReactNode } from "react";

type Props = {
  children: ReactNode;
};

export default async function DashboardLayout({ children }: Props) {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("session_id");

  // セッションがなければログインページへ
  if (!sessionId) {
    redirect("/login");
  }

  return (
    <div className="flex min-h-screen">
      {/* サイドバー */}
      <aside className="w-60 border-r bg-gray-50 p-6">
        <nav className="space-y-2">
          <a href="/dashboard" className="block rounded px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100">
            ダッシュボード
          </a>
          <a href="/settings" className="block rounded px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100">
            設定
          </a>
          <a href="/profile" className="block rounded px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100">
            プロフィール
          </a>
        </nav>
      </aside>

      {/* メインコンテンツ */}
      <main className="flex-1 p-8">{children}</main>
    </div>
  );
}

保護対象ページ(認証チェック不要)

// app/(dashboard)/page.tsx
export default function DashboardPage() {
  return (
    <div>
      <h1 className="mb-4 text-2xl font-bold">ダッシュボード</h1>
      <p className="text-gray-600">認証済みユーザーのみ表示されます。</p>
    </div>
  );
}

公開ページ(ログインフォーム)

// app/(public)/login/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function LoginPage() {
  const router = useRouter();
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const fd = new FormData(e.currentTarget);

    const res = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email: fd.get("email"),
        password: fd.get("password"),
      }),
    });

    if (res.ok) {
      router.push("/dashboard");
    } else {
      setError("メールアドレスまたはパスワードが正しくありません");
    }
  }

  return (
    <main className="flex min-h-screen items-center justify-center">
      <form onSubmit={handleSubmit} className="w-80 space-y-4 rounded-lg border p-8">
        <h1 className="text-xl font-bold">ログイン</h1>
        {error && <p className="text-sm text-red-500">{error}</p>}
        <input
          name="email"
          type="email"
          placeholder="メールアドレス"
          required
          className="block w-full rounded border px-3 py-2 text-sm"
        />
        <input
          name="password"
          type="password"
          placeholder="パスワード"
          required
          className="block w-full rounded border px-3 py-2 text-sm"
        />
        <button
          type="submit"
          className="w-full rounded bg-blue-600 py-2 text-sm text-white hover:bg-blue-700"
        >
          ログイン
        </button>
      </form>
    </main>
  );
}

セッション発行 API

// app/api/login/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const body = await request.json() as { email: string; password: string };

  // 実際のアプリでは DB + bcrypt.compare() で検証する
  if (body.email !== "user@example.com" || body.password !== "password") {
    return NextResponse.json({ error: "認証失敗" }, { status: 401 });
  }

  const cookieStore = await cookies();
  cookieStore.set("session_id", crypto.randomUUID(), {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60 * 24,
  });

  return NextResponse.json({ message: "ログイン成功" });
}

ポイント

  • app/(dashboard)/layout.tsx は Route Group (dashboard) の共通レイアウト。cookies() で Cookie を確認し、未認証なら redirect("/login") する。このレイアウトの配下に追加したページはすべて自動的に保護される
  • Route Group(括弧付きディレクトリ)は URL パスに影響しないため、(dashboard) 配下のページも /dashboard/settings のように通常の URL でアクセスできる
  • nextjs-middleware-auth との違い: Middleware は全リクエストに適用されるため柔軟だが設定が複雑。layout での保護は特定のルートグループのみを対象にできてシンプル
  • 実際のアプリでは sessionId.value を DB と照合してセッションの有効性を確認する。Cookie 値が存在するだけで認証済みと判断するのはサンプル簡略化のため
  • redirect()next/navigation からインポートする。Server Component / layout から使える。Client Component では useRouter().push() を使う

注意点

nextjs-middleware-auth はミドルウェアで全リクエストを保護するパターン。nextjs-cookie-session は Cookie の発行・読み取り。nextjs-auth-redirect は認証状態による個別ページ redirect。これは layout.tsx 単位で配下の複数ページを一括保護するパターンに特化。

関連サンプル