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

Next.js の cookies() で httpOnly セッション Cookie を管理する

next/headers の cookies() を使い、ログイン時に httpOnly セッション Cookie を発行し、Server Component・Route Handler・Middleware で読み書きする例。

nextjsauthenticationapi

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router の基本と HTTP Cookie の仕組みを理解していること

概要

next/headerscookies() でログイン時に httpOnly Session Cookie を発行し、Server Component・Route Handler・Middleware からそれを読み取って認証状態を確認する。外部ライブラリ不要で Next.js 標準 API のみで完結するセッション管理パターン。

インストール

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

実装

ログイン API(Cookie 発行)

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

const FAKE_USERS: Record<string, string> = {
  "user@example.com": "password123",
};

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

  if (!body.email || !body.password) {
    return NextResponse.json({ error: "email and password are required" }, { status: 400 });
  }

  if (FAKE_USERS[body.email] !== body.password) {
    return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
  }

  const cookieStore = await cookies();

  // セッション ID を発行(実際はランダム生成 + DB保存)
  const sessionId = crypto.randomUUID();

  cookieStore.set("session_id", sessionId, {
    httpOnly: true,    // JS から読めないようにする
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60 * 24, // 24時間
  });

  return NextResponse.json({ message: "Logged in" });
}

ログアウト API(Cookie 削除)

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

export async function POST() {
  const cookieStore = await cookies();
  cookieStore.delete("session_id");
  return NextResponse.json({ message: "Logged out" });
}

Server Component での認証確認

// app/dashboard/page.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("session_id");

  if (!sessionId) {
    redirect("/login");
  }

  return (
    <main className="p-8">
      <h1 className="text-2xl font-bold">ダッシュボード</h1>
      <p className="mt-2 text-sm text-gray-600">
        セッション ID: {sessionId.value}
      </p>
    </main>
  );
}

Middleware でのルート保護

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const sessionId = request.cookies.get("session_id");

  // 未認証の場合はログインページにリダイレクト
  if (!sessionId) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/profile/:path*"],
};

クライアント側ログインフォーム

// app/login/page.tsx
"use client";

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

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

  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 {
      const data = await res.json();
      setError(data.error ?? "ログインに失敗しました");
    }
  }

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

ポイント

  • cookies() は Server Component / Route Handler では await が必要(Next.js 15 から非同期)。Middleware では request.cookies を直接参照する
  • httpOnly: true を設定すると JavaScript(document.cookie)から読めなくなり XSS によるセッション奪取を防げる
  • secure: true は HTTPS 接続のみ Cookie を送信する。本番環境では必須
  • セッション ID は crypto.randomUUID() で生成しているが、実際のアプリでは DB にセッション情報を保存して ID と紐付けることでセッション無効化(ログアウト)を確実にする
  • nextjs-middleware-auth との使い分け: Middleware での保護ロジックの書き方は同記事を参照。これは Cookie の発行・削除・Server Component での読み取りに焦点を当てている
  • FAKE_USERS の平文パスワード比較はサンプル用。実際のアプリでは bcrypt 等でハッシュ化したパスワードを DB に保存し、bcrypt.compare() で検証する

注意点

nextjs-middleware-auth はミドルウェアでルート保護のみ(セッション発行しない)。supabase-email-auth は外部サービス依存。これは cookies() で httpOnly Cookie を発行・検証するだけの Next.js 標準 API 実装。

関連サンプル