🐥
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/headersのheaders()を使い、ヘッダー情報を手動で渡す必要がある。
// 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/headersのcookies()を使用する。
// 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).*)"],
};
以上です。
Discussion