1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

2026年 React/Next.jsの最新技術スタックで認証・認可を深く理解する(NextAuth.js v5)

1
Last updated at Posted at 2026-01-03

0.はじめに

業務アプリケーションで認証・認可は非常に重要ですよね。
そして NextAuth.js (v5 / Auth.js) は裏側の仕組みやデフォルト設定が充実しているため、あいまいな理解でもなんとなくでも動いてしまうのが怖いところです。
この機会に内部の仕組みを深く理解し、ベストプラクティスを目指してみましたので共有します。
もし不備や改善点があれば、遠慮なくコメントでご指摘ください。

トレンド情報: Better Auth について
現在、TypeScriptの型推論や2要素認証の手軽さから Better Auth というライブラリも注目を集めています。 しかし、2026年現在、実務での採用率やエコシステムの大きさでは依然として NextAuth.js (Auth.js) がスタンダードであり、基本を押さえる上で本記事を共有します。

Better Authについてまとめました

✅ この記事で習得できること
image.png

✅ この記事での防衛方針

  • 防衛ライン① : middleware.ts で安全な認証・認可制御
  • 防衛ライン② : 認証結果をNextAuthで暗号化し安全に保管する
  • 防衛ライン③ : 無操作時は自動でログアウトする
  • 防衛ライン④ : 認可(権限による制限)
  • 防衛ライン⑤ : Route Groupsによる隔離 (protected)

layout.tsxでの認証・認可は採用していません

✅ 認証と認可をおさらい

• 認証(Authentication): あなたは誰ですか?(ログイン)
• 認可(Authorization): あなたは何が許可されてますか?(権限管理)

1.デモアプリのイメージと完全なソース

image.png

✅ 完全なソース

2.この記事で押さえるセキュリティ技術

技術 構成要素 概要・役割
NextAuth.js v5 Auth.js 認証の専門家(ブラックボックス)。
ログイン処理、セッション管理(Cookie/JWT)、暗号化を一手に引き受ける。
Next.js Middleware レンダリング開始「前」に認証状態・権限に応じてページ表示を制限する最強の門番。
Next.js Route Groups (protected) のように () で囲ったフォルダ。
セキュリティー機能はないが、ログイン者のみ表示するページを配下に集約することで整理する。

✅ 各種バージョン

アプリケーション バージョン
NextAuth.js @5.0.0-beta.30
next.js @16.1.1
react @19.2.3
tailwindcss @4.1.18
bcryptjs @3.0.3

3.最終ディレクトリ構成

src/
├── auth.ts                      # NextAuth.js v5 設定(認証の専門家)
├── middleware.ts                # 認証・認可制御 最大の門番
├── lib/
│   ├── auth-guard.ts            # 権限チェックロジックの集約
│   └── authenticate.ts          # 認証用のアクションサーバー
├── types/
│   └── next-auth.d.ts           # 型拡張(デフォルトのセッションに権限を追加)
├── components/
│   ├── Navigation.tsx           # ナビゲーション(メニュー)
│   └── NavLinks.tsx             # ナビリンク(権限に応じてメニューを制御)
└── app/
    ├── login/
    │   └── page.tsx             # ログインページ(パブリック)
    ├── (protected)/             # Route Group(認証必須)
    │   ├── layout.tsx           # 認証者のみナビゲーションを表示
    │   ├── page.tsx             # 認証者用のルートページ
    │   ├── admin/
    │   │   └── page.tsx         # 管理者ページ
    │   └── user/
    │       └── page.tsx         # 一般用ページ
    └── api/auth/[...nextauth]/
        └── route.ts             # NextAuth APIエンドポイント

これ以外に環境変数用の env.local ファイルがルート直下に必要です。

✅ .env.local の記述内容

AUTH_SECRET=あなたのシークレットキー
AUTH_URL=http://localhost:3000

4.アンチパターン(危険な実装)

layout.tsx にて 認証状態や権限を判定し、通過した場合のみページを表示する仕組みは情報漏えいの危険性が高いため採用できません。

// ダミーのログイン認証
function isLoggedIn() {
  return false;
}

export default async function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  // ダミーチェック(未認証or権限がない場合はリダイレクト)
  if (!isLoggedIn()) {
    redirect('/login');
  }

  // 認証済みに限りpage.tsxを表示(と思ってるが実際は...)
  return <>{children}</>;
}

ブラウザではリダイレクトされて一見安全に見えるが、curlを実行するとペイロードは取得できてしまう。

# ブラウザではリダイレクトされるが、データは丸見えの例
curl -X GET https://yoursite.com/admin/page
# 結果: <html>...管理者ページのコンテンツ...</html>

こちらの記事で詳しく解説されてます。

5.防衛ライン① : middleware.ts で安全な認証・認可制御

middleware.ts は、ページレンダリングが始まる「前」にリクエストを遮断できるため、画面単位(URLベース)のアクセス制御において最も安全かつ確実な手段です。 layout.tsx と異なり、権限のないユーザーにはHTMLの断片すら送信させません。

✅ middleware.ts の記述

import { auth } from "@/auth";
import { NextResponse } from "next/server";
import { ROLES } from "@/lib/constants";

export default auth((req) => {
  const isLoggedIn = !!req.auth?.user;
  const isAdmin = req.auth?.user?.role === ROLES.ADMIN;
  const { nextUrl } = req;

  const isOnLoginPage = nextUrl.pathname === "/login";
  const isOnAdminPage = nextUrl.pathname.startsWith("/admin");

  // 1. ログイン済みならログインページはパスして、ルートページを表示 (逆流防止)
  if (isOnLoginPage) {
    if (isLoggedIn) {
      return NextResponse.redirect(new URL("/", nextUrl));
    }
    return null;
  }

  // 2. 未ログインユーザーはログインページへリダイレクト
  if (!isLoggedIn) {
    let callbackUrl = nextUrl.pathname;
    if (nextUrl.search) {
      callbackUrl += nextUrl.search;
    }
    const encodedCallbackUrl = encodeURIComponent(callbackUrl);

    return NextResponse.redirect(
      // 元のURLをcallbackUrlとして渡すと親切
      new URL(`/login?callbackUrl=${encodedCallbackUrl}`, nextUrl)
    );
  }

  // 3. 認可制御: Adminエリア保護
  if (isOnAdminPage && !isAdmin) {
    // ルートページへ戻す
    return NextResponse.redirect(new URL("/", nextUrl));
  }

  return null; // 通過許可
});

// auth関数実行対象
export const config = {
  // api, static, image, favicon 等を除外
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

6.防衛ライン② : 認証結果をNextAuthで暗号化し安全に保管する

認証は複雑な手順が必要です。

  1. 本人確認:
    ユーザー名・パスワード等でユーザーを検証。
  2. トークン発行:
    ユーザー情報(ID, Role等)を含んだ JWTを生成し、改ざん防止の署名を行う。
  3. 安全な保存:
    トークンを 暗号化(JWE) しHttpOnly Cookie に保存。
  4. セッション復元:
    リクエストのたびにサーバー側でCookieを復号・検証し、ユーザー情報を取り出す。

これを手作りするのは非常に手間なので、標準的なライブラリ NextAuth を利用します。

どのようなロジックで本人確認するかなど、必要な情報を NextAuth の auth.ts に記述することで複雑な手順がかなり簡略化されます。

✅ auth.ts の記述方法

ここで 「本人確認」「トークン発行」「安全な保存」 を実現します。

// NextAuthは「システム全体」の設定
export const { handlers, auth, signIn, signOut } = NextAuth({

  // ① Providers: ここが「本人確認」のロジック
  providers: [
    Credentials({
      // ▼ ここが実質の「checkCredentials(username, password)」
      authorize: async (credentials) => {
        // DBアクセス等...
        // 成功時: オブジェクトを返す(これが User オブジェクトになる)
        // 失敗時: null を返す(エラーになる)
        return user 
      },
    }),
  ],

  // ② Callbacks: データは「User → JWT → Session」の順にバケツリレーされる
  callbacks: {
    // 1. ログイン直後、User情報(roleなど)をトークン(JWT)に焼き付ける
    jwt: async ({ token, user }) => { 
      if (user) {
        token.role = user.role // ここで保存しないと消える
      }
      return token 
    },
    // 2. フロントエンドがセッション要求した際、トークンから取り出して渡す
    session: async ({ session, token }) => { 
      session.user.role = token.role // トークンからセッションへ転記
      return session 
    }
  },

  // ③ Pages: 画面の場所指定
  pages: { ... }
})

✅ 暗号化され安全に保管されたセッションの取り出し

await auth()で、自動的に復号化されたセッションが取り出せます。

import { auth } from "@/auth";

export async function isAdmin(): Promise<boolean> {
  const session = await auth();
}

✅ サンプルアプリの完全な auth.ts

============================
   ソースコード全体を表示(折りたたみ)
 =============================
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";

/**
 * NextAuth.js v5 (Auth.js) 設定
 *
 * 認証の専門家(ブラックボックス)として以下を担当:
 * - ログイン処理(Credentials Provider)
 * - セッション管理(Cookie/JWT)
 * - 暗号化
 *
 * ※ 権限チェック(ロール)は AuthGuard が担当
 */

// ダミーユーザーデータ(本番環境ではDBを使用)
// パスワードはbcryptでハッシュ化済み
const users = [
  {
    id: "1",
    username: "admin",
    // 元のパスワード: admin123
    passwordHash:
      "$2b$10$ZObeW92VdQhCjTnZIO5xyewwtbMvVZbTQDpvex3wbObyP5Fy0GI9C",
    role: "管理者" as const,
  },
  {
    id: "2",
    username: "user",
    // 元のパスワード: user123
    passwordHash:
      "$2b$10$FxG5IygF87OfTX9yhgUNL.HigPU5tpq1.riV7CU0C4RdQGj4GhafC",
    role: "一般" as const,
  },
];

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        username: { label: "ユーザー名", type: "text" },
        password: { label: "パスワード", type: "password" },
      },

      // ログイン画面のsignInで実行される認証ロジック
      async authorize(credentials) {
        if (!credentials?.username || !credentials?.password) {
          return null;
        }

        // ユーザー名でユーザーを検索
        const user = users.find((u) => u.username === credentials.username);

        if (!user) {
          return null;
        }

        // bcryptでパスワードを検証
        const isValidPassword = await bcrypt.compare(
          credentials.password as string,
          user.passwordHash
        );

        if (!isValidPassword) {
          return null;
        }

        return {
          id: user.id,
          name: user.username,
          role: user.role,
        };
      },
    }),
  ],
  callbacks: {
    // JWTトークンにロール情報を追加
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role;
      }
      return token;
    },
    // セッションにロール情報を追加
    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role as string;
      }
      return session;
    },
  },
  session: {
    strategy: "jwt",
    /*
    この設定の挙動シミュレーション
      10:00 ログイン: 有効期限は 10:30 です。
      10:04 操作: 前回更新から5分経っていないので、更新されません。(期限 10:30 のまま)
      10:06 操作: 前回から5分以上経過したので、セッションが更新されます。有効期限が 10:36 に延長されます。
      10:06 〜 10:37 放置 (31分): 操作がないまま有効期限(10:36)を過ぎます。
      10:38 操作: 有効期限切れのため、ログアウト状態になります。
    */
    maxAge: 30 * 60, // 30分 (これが本当の寿命)
    updateAge: 5 * 60, // 5分 (5分経過するたびに、寿命を「現在+30分」にリセットする)
  },
  pages: {
    signIn: "/login",
  },
});

7.防衛ライン③ : 無操作時は自動でログアウトする

一定時間操作しなかった場合、自動でログアウトはよくある業務要件と思います。
NextAuthの auth.ts にて、セッションの有効時間を設定できます。

  session: {
    strategy: "jwt",
    maxAge: 30 * 60, // 30分 (これが本当の寿命)
    updateAge: 5 * 60, // 5分 (5分経過するたびに、寿命を「現在+30分」にリセットする)
  },

上記サンプルでは下記の設定にしてあります。

  • 30分間無操作なら自動ログアウト
  • 5分間隔で操作の有無を確認

🔶この設定の挙動シミュレーション

10:00 ログイン: 有効期限は 10:30 です。
10:04 操作: 前回更新から5分経っていないので、更新されません。(期限 10:30 のまま)
10:06 操作: 前回から5分以上経過したので、セッションが更新されます。有効期限が 10:36 に延長されます。
10:06 〜 10:37 放置 (31分): 操作がないまま有効期限(10:36)を過ぎます。
10:38 操作: 有効期限切れのため、ログアウト状態になります。

8.防衛ライン④ : 認可(権限による制限)

最初にNextAuthを拡張し、権限を扱えるようにします。

✅ NextAuth.jsの型を拡張し権限(role)を扱えるようにする

NextAuthはデフォルトでは権限がありません。
拡張して権限を扱えるようにします。

/src/types/next-auth.d.ts

import "next-auth";

/**
 * NextAuth.js 型拡張
 * 
 * デフォルトのUser/Sessionにroleプロパティを追加
 */
declare module "next-auth" {
  interface User {
    role?: string;
  }

  interface Session {
    user: {
      id?: string;
      name?: string | null;
      email?: string | null;
      image?: string | null;
      role?: string;
    };
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    role?: string;
  }
}

✅ 権限の判定ロジック

個別に権限判定ロジックを記述するのはバグの温床なので一箇所に集約します。

/src/lib/auth-guard.ts

import { auth } from "@/auth";
import { ROLES } from "./constants";

export async function isAdmin(): Promise<boolean> {
  const session = await auth();
  return session?.user?.role === ROLES.ADMIN;
}

✅ 権限ごとのメニュー制御

「ログインできれば誰でも全ての機能が使える」のでは業務アプリとして不十分です。「一般社員がマスタデータを削除してしまった」という事態を防ぐ必要があります。
まずはメニュー表示を権限に応じて制限します。

/src/components/Navigation.tsx
ログイン済みか、管理者か否かを判定の上でセットして NavLinks を呼び出します。

import { auth, signOut } from "@/auth";
import { isAdmin } from "@/lib/auth-guard";

export default async function Navigation() {
  const session = await auth();
  const isUserAdmin = await isAdmin()

  (中略)

  {/* ナビゲーションリンクは管理者とそれ以外で分ける  */}
  <NavLinks 
    isAdmin={isUserAdmin}
  />

/src/components/NavLinks.tsx

isAdmin を受取り、メニュー表示を制限してます。

type NavLinksProps = {
  isAdmin: boolean;
};

export default function NavLinks({ isAdmin }: NavLinksProps) {

  (中略)

      {/* 管理者ページへのリンクは、管理者のみ表示 */}
      {isAdmin && (
        <Link
          href="/admin"
          className={`rounded-md px-4 py-2 text-sm font-medium transition-colors ${
            pathname === "/admin"
              ? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900"
              : "text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100"
          }`}
        >
          管理者ページ
        </Link>
      )}

9.防衛ライン⑤ : Route Groupsによる隔離 (protected)

プロジェクト構造を見ると、認証が必要なページはすべて src/app/(protected)/ という特殊なフォルダの下に配置されています。

src/
└── app/
    ├── (protected)/             # Route Group(認証必須)
    │   ├── layout.tsx           # 認証者のみナビゲーションを表示
    │   ├── page.tsx             # 認証者用のルートページ
    │   ├── admin/
    │   │   └── page.tsx         # 管理者ページ
    │   └── user/
    │       └── page.tsx         # 一般用ページ

( ) で囲まれたフォルダ名はURLには影響しませんが、開発者が「ここは保護されたエリアだ」と認識し、レイアウト(layout.tsx)や設定を共有するのに役立ちます。

✅ /src/app/(protected)/layout.tsx

認証者のみがたどり着けるページです。
ここではじめてナビゲーションメニューを表示します。

import Navigation from "@/components/Navigation";

export default async function ProtectedLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      {/* 認証者へのみナビゲーションを表示する */}
      <Navigation />
      {children}
    </>
  );
}

10.ログインの実装例

✅ ログイン画面の実装

重要なのは useActionState を利用して認証用のサーバーアクションを呼び出している部分です。

/src/app/login/page.tsx

"use client";

import { authenticate } from "@/lib/authenticate";
import { useActionState } from "react";

export default function LoginPage() {
  // 第1引数: Server Action, 第2引数: 初期状態
  const [state, dispatch, isPending] = useActionState(authenticate, {
    message: undefined,
    errors: undefined,
  });

  return (
    <div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
      <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-lg dark:bg-zinc-900">
        <h1 className="mb-6 text-2xl font-bold text-black dark:text-zinc-50">
          ログイン
        </h1>
        <form action={dispatch} className="space-y-4">
          {" "}
          <div>
            <label
              htmlFor="username"
              className="block text-sm font-medium text-black dark:text-zinc-50"
            >
              ユーザー名
            </label>
            <input
              id="username"
              name="username"
              type="text"
              required
              className="mt-1 w-full rounded-md border border-zinc-300 px-3 py-2 text-black dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50"
              placeholder="admin または user"
            />
          </div>
          <div>
            <label
              htmlFor="password"
              className="block text-sm font-medium text-black dark:text-zinc-50"
            >
              パスワード
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              className="mt-1 w-full rounded-md border border-zinc-300 px-3 py-2 text-black dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50"
              placeholder="admin123 または user123"
            />
          </div>
          {/* エラー表示 */}
          {state.errors && (
            <div className="rounded-md bg-red-100 p-3 text-sm text-red-700 dark:bg-red-900 dark:text-red-300">
              {state.errors}
            </div>
          )}
          <button
            type="submit"
            disabled={isPending}
            className="w-full rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 disabled:bg-gray-400"
          >
            {isPending ? "ログイン中..." : "ログイン"}
          </button>
        </form>
        <div className="mt-6 text-sm text-zinc-600 dark:text-zinc-400">
          <p className="mb-2 font-semibold">テストアカウント:</p>
          <p>管理者: admin / admin123</p>
          <p>一般: user / user123</p>
        </div>

        {/* Footer */}
        <footer className="mt-8 mb-4 text-lg text-center text-gray-200">
          <div className="mb-2">
            <span>Developed by </span>
            <a
              href="https://hakamata-soft.com/"
              target="_blank"
              rel="noopener noreferrer"
              className="text-blue-500 hover:underline font-medium"
            >
              HakamataSoft
            </a>
          </div>

          <div className="flex text-xs items-center justify-center gap-2">
            <span className="text-gray-200">Powered by</span>
            {/* Next.js のブランドカラー (黒/白) */}
            <span className="bg-black text-white px-2 py-0.5 rounded font-bold">
              Next.js
            </span>
            <span className="text-gray-800">&</span>
            {/* Tailwind CSS のブランドカラー (シアン) */}
            <span className="bg-cyan-500 text-white px-2 py-0.5 rounded font-bold">
              Tailwind CSS
            </span>
          </div>
        </footer>
      </div>
    </div>
  );
}

✅ ログインページから呼び出す認証用のサーバーアクション

src/lib/authenticate.ts

"use server";

import { signIn } from "@/auth";
import { AuthError } from "next-auth";

// useActionState用の型定義
export type State = {
  errors?: string;
  message?: string;
};

export async function authenticate(
  prevState: State,
  formData: FormData
): Promise<State> {
  try {
    await signIn("credentials", formData);
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case "CredentialsSignin":
          return {
            errors: "ユーザー名またはパスワードが間違っています。",
          };
        default:
          return {
            errors: "サーバーエラーが発生しました。",
          };
      }
    }
    // 重要: リダイレクト用のエラーは再スローする
    throw error;
  }

  // ここには到達しない(成功時はリダイレクトされるため)
  return { message: "success" };
}

✅ 認証APIの開通(重要)

Auth.js を正常に動作させるために、Next.js と Auth.js をつなぐ「通信の出入り口(API Route)」を設置する必要があります。

今回はサーバーアクションでログインを行いますが、以下の機能を利用するためにこのAPIルートは必須です。

  • クライアント側でのセッション取得(useSessionなど)
  • 将来的なソーシャルログイン(Googleログイン等のコールバックURL)
  • Auth.js 内部でのCookie管理やセッション更新処理

src/app/api/auth/[...nextauth]/route.ts

import { handlers } from "@/auth"; // auth.ts で作った設定を読み込む

// GETリクエストもPOSTリクエストも、すべてNextAuthのhandlersにお任せする
export const { GET, POST } = handlers;

ファイル名の [...nextauth] とは?
これは「キャッチオールセグメント」と呼ばれ、/api/auth/ 以下に来るあらゆるアクセス(ログイン、ログアウト、コールバックなど)をこの1ファイルですべて引き受けるための特別な命名規則です。

✅ auth.ts の認証フロー

  1. [Client] ログイン画面で「ユーザー名」「パスワード」を入力しサブミット。
  2. [Client] Next.jsがサーバーアクション authenticate をPOST送信。
  3. [Server] authenticate.ts 内で signIn("credentials") が実行される。
  4. [Server] 直接 auth.config.ts の authorize が実行される。
  5. [Server] authorize 関数が実行され、DB等のユーザー情報と照合。
  6. [Server] 一致すれば「認証成功」とし、jwt コールバックでトークンに権限(role)を書き込む。
  7. [Server] session コールバックを経由して、クライアントが利用可能なセッションを発行。

✅ ログイン成功時に発行されるセッション
NextAuth.js v5のデフォルトの設定だと、認証情報はCookieに保存されます。

image.png

項目 デフォルト値と内容
セッション戦略 jwt(デフォルト)
保存先 authjs.session-token
暗号化 JWTはAUTH_SECRETで暗号化される
maxAge 30日(2,592,000秒) → デモは30分に変更済み
updateAge 24時間(86,400秒) → デモは5分に変更済み

✅ JWTの暗号化(JWE)

NextAuth (Auth.js) はデフォルトで JWS (署名のみ) ではなく JWE (暗号化) を使用しています。

🔶 暗号化/復号化のロジック (JWE)
内部では jose ライブラリを使用し、以下の標準規格で処理されています。

規格: JWE (JSON Web Encryption) Compact Serialization
アルゴリズム: A256GCM (AES-GCM 256-bit)
鍵導出: HKDF (HMAC-based Extract-and-Expand Key Derivation Function)
AUTH_SECRET をそのまま暗号化キーにするのではなく、HKDFを使って「暗号化用」「署名用」などのキーを安全に生成してから使います。

🔶 処理フロー
ブラウザ(クライアント)はCookie内の文字列を見ても、中身(ペイロード)を解読できません。

🔸書き込み時 (Login / Session Update)

セッションデータ(JSON)を用意。
AUTH_SECRET から派生させた鍵で 暗号化。
暗号化された文字列(JWE)をCookie (authjs.session-token) に保存。

🔸読み出し時 (Middleware / API Routes / Server Components)

CookieからJWE文字列を取得。
AUTH_SECRET から派生させた鍵で 復号化。

🔶 簡単にはデコードさせないJWE
JWTのデコードサイトなどに生成されたJWEを貼り付けても暗号化されているのでデコードさせません。
image.png

✅ JWT暗号化のためのシークレット(AUTH_SECRET)

.env.local に記述

AUTH_SECRET=your_secret

✅ JWTの中身のサンプル

{
  name: "admin",
  sub: "1",        // ユーザーID
  role: "管理者",   // カスタムで追加したロール
  iat: 1234567890, // 発行時刻
  exp: 1234567890, // 有効期限
  jti: "xxx"       // トークンID
}

✅ JWTの各フィールドの説明

フィールド 説明 設定箇所
name ユーザー名 authorize() で返す name
sub ユーザーID(Subject) authorize() で返す id
role ロール(カスタム) jwt コールバックで追加
iat 発行時刻(Issued At) 自動生成
exp 有効期限(Expiration) maxAge: から計算
jti トークンID(JWT ID) 自動生成(ユニークID)

✅ パスワードの暗号化(JWTの暗号化とは別物)
情報漏えい対策として、ユーザーのパスワードを生のままDBに登録するのはNGです。
ここでは bcryptjsを使ってハッシュ化しています。

✅ bcryptの特徴
ソルト内蔵: ハッシュにソルトが含まれている
コスト係数: $2b$10$ の 10 がコスト(計算回数 2^10)
一方向: ハッシュから元のパスワードは復元不可

✅ なぜパスワードはbcryptを利用するのか?(代表的な暗号化SHA-256と比較)

特徴 SHA-256 bcrypt
主な用途 通信の暗号化、ファイルの同一性チェック パスワードの保存
計算速度 爆速 (攻撃者に有利) 激遅 (攻撃者に不利)
GPU耐性 弱い (GPUで超高速解析可能) 強い (GPUでも遅い)
ソルト管理 自前で実装が必要 ライブラリが全自動で処理
安全性 パスワード用としては 不適切 パスワード用として 業界標準

11.AWSへのデプロイ時の注意事項

今回のデモは「AWS Amplify」へデプロイしてます。

✅ 環境変数の設定
以下、3つの環境変数の設定が必須です。

環境変数 設定内容
AUTH_SECRET JWTを暗号化するためのシークレット
AUTH_TRUST_HOST true (プロキシ背後での動作に必須)
AUTH_URL デプロイ先のルートURL (例: https://myapp.com )

image.png

✅ ビルドの設定(重要)
この追加が無いと、環境変数が読み込めずエラーになります。

Amplifyコンソールで設定した環境変数は、デフォルトではビルド環境には存在しますが、Next.jsのSSR/API Routes実行時(Lambda等)に渡らないことがあります。 ビルドフェーズで明示的に .env.production に書き出すことでこれを回避します。

amplify.yml の build フェーズに追記してください。

  # 以下の行を追加: 環境変数を .env.production に書き出す
    - echo "AUTH_SECRET=$AUTH_SECRET" >> .env.production
    - echo "AUTH_TRUST_HOST=$AUTH_TRUST_HOST" >> .env.production
    - echo "AUTH_URL=$AUTH_URL" >> .env.production

image.png

12.最後に

認証・認可はアプリケーションの要であり、最も堅牢性が求められる部分です。 本記事では「多層防御」を意識して実装しましたが、セキュリティ技術は日々進化し、同時に新たな脅威も生まれ続けています。

ライブラリのバージョンアップや脆弱性情報には常にアンテナを張り、継続的なメンテナンスを心がけてください。 本記事が、ブラックボックスになりがちな NextAuth.js の理解の一助となれば幸いです。

✅ 免責事項
可能な限り正確な情報を記述・検証するよう努めていますが、本記事の内容やサンプルコードを利用したことによる損害や不具合について、著者は一切の責任を負いかねます。本番環境への導入は、各プロジェクトの要件に合わせて十分な検証を行った上で、自己責任にてお願いいたします。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?