0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JWT認証(Next.js側)— HttpOnly CookieでJWTを安全に管理する

0
Posted at

JWT認証(Next.js側)— HttpOnly CookieでJWTを安全に管理する

はじめに

前回はSpring BootでJWTトークンを発行するログインAPIを実装しました。今回はNext.js側でそのトークンを受け取り、HttpOnly Cookieで安全に保管して認証を維持する方法を整理します。


なぜlocalStorageではダメなのか

JWTの保管場所としてlocalStorageはよく使われますが、セキュリティリスクがあります。

保管場所 XSS耐性 CSRF耐性 実務での評価
localStorage ✗ 弱い ✓ 強い 非推奨
HttpOnly Cookie ✓ 強い △ 要対策 推奨

HttpOnly CookieはJavaScriptからアクセスできないため、XSSでトークンが盗まれるリスクを防げます。


① ログインServer Action — トークンをCookieにセット

// app/login/actions.ts
"use server";

import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function login(formData: FormData) {
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  // Spring BootのログインAPIを叩く
  const res = await fetch(`${process.env.API_URL}/api/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  });

  if (!res.ok) {
    throw new Error("ログインに失敗しました");
  }

  const { token } = await res.json();

  // HttpOnly CookieにJWTをセット
  cookies().set("auth_token", token, {
    httpOnly: true,      // JavaScriptからアクセス不可
    secure: process.env.NODE_ENV === "production", // 本番はHTTPS必須
    sameSite: "lax",    // CSRF対策
    maxAge: 60 * 60 * 24, // 24時間
    path: "/",
  });

  redirect("/dashboard");
}

② ログインフォーム

// app/login/page.tsx
import { login } from "./actions";

export default function LoginPage() {
  return (
    <form action={login}>
      <input name="email" type="email" placeholder="メールアドレス" required />
      <input name="password" type="password" placeholder="パスワード" required />
      <button type="submit">ログイン</button>
    </form>
  );
}

③ 認証済みAPIリクエスト — CookieからトークンをBearerヘッダーに

// lib/fetchWithAuth.ts
import { cookies } from "next/headers";

export async function fetchWithAuth(path: string, options?: RequestInit) {
  const token = cookies().get("auth_token")?.value;

  const res = await fetch(`${process.env.API_URL}${path}`, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`, // JWTをヘッダーに付与
      ...options?.headers,
    },
  });

  return res;
}
// app/dashboard/page.tsx
import { fetchWithAuth } from "@/lib/fetchWithAuth";

export default async function DashboardPage() {
  const res = await fetchWithAuth("/api/users");

  if (!res.ok) {
    // 401など認証エラーはログインへリダイレクト
    redirect("/login");
  }

  const users = await res.json();

  return (
    <ul>
      {users.map((user: { id: number; name: string }) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

④ ログアウト — Cookieを削除

// app/logout/actions.ts
"use server";

import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function logout() {
  cookies().delete("auth_token");
  redirect("/login");
}

⑤ ミドルウェアで未認証リダイレクト

// middleware.ts(プロジェクトルートに配置)
import { NextRequest, NextResponse } from "next/server";

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

  // 認証が必要なパスへのアクセスをチェック
  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

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

まとめ

処理 実装場所 ポイント
ログイン→Cookie保存 Server Action cookies().set() でHttpOnly指定
認証付きAPIリクエスト サーバー側fetch Authorization: Bearer {token}
ログアウト Server Action cookies().delete()
未認証リダイレクト middleware.ts /dashboard などをガード

次回は エラーハンドリング — Spring Boot側でエラーを返してNext.jsで表示する方法を整理します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?