🐥

Next.js App Router: JWT クッキー認証の実装ハッカソンメモ

に公開

はじめに

こないだ参加したハッカソンで得た知見の備忘録です。

1. JWT 発行と HttpOnly クッキー設定

認証の基本フローとして、登録・ログイン API で JWT を発行し、XSS 対策のためHttpOnly属性を付与したクッキーに保存する。

/api/auth/register/route.ts

import jwt from "jsonwebtoken";
import { NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  // ... ユーザー作成処理 ...
  const newUser = {
    id: "1",
    email: "test@test.com",
    username: "Test",
    user_id: "test_123",
  };

  const token = jwt.sign(
    {
      id: newUser.id,
      email: newUser.email,
      username: newUser.username,
      userId: newUser.user_id,
    },
    process.env.JWT_SECRET!,
    { expiresIn: "7d" }
  );

  const response = NextResponse.json({ message: "アカウント作成成功" });

  response.cookies.set("auth-token", token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // 7日間
  });

  return response;
}

2. 認証付きfetchリクエスト

クライアントコンポーネントからのfetch

クライアントサイドのfetchで Cookie を送信するにはcredentials: 'include'オプションが必要。

// Client Component内
const res = await fetch("/api/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ content: "新しい投稿です" }),
  credentials: "include", // Cookieをリクエストに含める
});

サーバーコンポーネントからのfetch

サーバーコンポーネントから内部 API へのfetchでは、ブラウザからのリクエストに含まれる Cookie は自動で引き継がれない。next/headersheaders()を使い、ヘッダー情報を手動で渡す必要がある。

// Server Component内
import { headers } from "next/headers";

const requestHeaders = new Headers(headers());

const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/posts`, {
  cache: "no-store",
  headers: requestHeaders, // ヘッダーを引き継ぐ
});

3. サーバーコンポーネントでの認証情報取得

fetchを介さずにサーバーコンポーネントで直接認証情報を取得する場合、next/headerscookies()を使用する。

// Server Component内
import { cookies } from "next/headers";
import { verifyAuthToken } from "@/lib/auth"; // 自作の検証関数

const cookieStore = await cookies();
const token = cookieStore.get("auth-token")?.value;
const user = token ? verifyAuthToken(token) : null;

4. Middleware によるルート保護

JWT ライブラリの選定 (jose vs jsonwebtoken)

Middleware は Edge Runtime で実行されるため、Node.js API に依存するjsonwebtokenは署名検証エラーを起こす可能性がある。Edge 互換のjoseを使用することが推奨される。

middleware.tsの実装

未認証ユーザーのアクセス制限や、認証済みユーザーの特定ページへのアクセス制御などを一元的に行う。

// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify } from "jose";

const AUTH_PATHS = ["/login", "/signup"];
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const token = request.cookies.get("auth-token")?.value;

  // ログイン済みのユーザーがログインページなどにアクセスした場合
  const isAuthPath = AUTH_PATHS.some((path) => pathname.startsWith(path));
  if (isAuthPath) {
    if (token) {
      try {
        await jwtVerify(token, SECRET);
        return NextResponse.redirect(new URL("/", request.url));
      } catch (error) {
        /* 無効なトークンなら何もしない */
      }
    }
    return NextResponse.next();
  }

  // 保護されたルートにトークンなしでアクセスした場合
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  // トークンはあるが、無効な場合
  try {
    await jwtVerify(token, SECRET);
    return NextResponse.next();
  } catch (err) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

 
 
 
 
以上です。

KA projects

Discussion