Next.js App RouterでAuth.js v5を導入したら6時間溶けた話|ハマりポイント全まとめ【2026年版】

Next.js App Router + Auth.js v5の認証実装でハマった失敗談。v4からの破壊的変更・Middleware設定・Session/JWT切り替えのつまずきポイントを現役フリーランスが実コードで解説。

Next.jsAuth.jsNextAuth.js認証App Router2026年

※当サイトはアフィリエイトプログラムに参加しています。記事内のリンクから商品を購入すると、当サイトに報酬が支払われることがあります。詳しくはプライバシーポリシーをご覧ください。

4月26日、GWに入る前日の金曜夜。

「連休中に認証まわりをちゃんと作り直そう」と思い立って、Auth.js v5をインストールしたのが23時ごろでした。「v4と大して変わらないだろう、1時間もあれば終わる」という見立てが完全に外れて、気づいたら翌朝5時17分になっていました。

正確には6時間17分の溶かしっぷりです。

この記事は、その6時間17分で学んだことをすべて書き残しておくための記録です。Auth.js(旧NextAuth.js)をNext.js App Routerで使おうとしているなら、同じ罠にハマらないように先に読んでおいてください。


暗い部屋でモニターを見つめるエンジニアのイメージ

Auth.js v5とは何か——v4からの変化

Auth.jsはNext.jsで認証を実装するためのライブラリです。もともとは「NextAuth.js」という名前でしたが、Next.js専用から他のフレームワークにも対応できるように設計を変えて、名称もAuth.jsに変わりました。

v5はその中でも大きな変更が入ったバージョンで、2024年末ごろからベータが続いていたものが、2025年に入って安定版として広く使われるようになっています。

問題は、v4とv5の間に破壊的変更が複数あるにもかかわらず、ドキュメントが追いついていない部分があることです。正直に言うと、Auth.jsのドキュメントは散らかっています。v4の情報とv5の情報が混在していて、Stack OverflowやZennで見つかる解説の多くがv4ベースで書かれていることも混乱に拍車をかけます。

v5で変わった主な点を先にまとめておきます。

  • インポートパスの変更next-auth から next-auth/v5 ではなく、設定ファイル経由の独自パスになる)
  • Middlewareの書き方が完全に別物になった
  • Session callbackとJWT callbackの型定義が変わった

「それだけ知ってれば大丈夫でしょ」と思うかもしれません。僕もそう思っていました。それが間違いの始まりでした。

ちょっと話が逸れますが、僕は文系出身でSEから転職してエンジニアになったので、認証まわりの設計を「なんとなく」で理解してきた部分があります。今回のハマりの背景にはそういう甘さもあったと思っています。自分の背景が気になった人はTypeScript vs JavaScriptどちらを先に学ぶべきかも読んでみてください。


失敗その1——importパスが「変わった」だけじゃなかった

v4での書き方

v4では、こんなふうに書いていました。

// pages/api/auth/[...nextauth].ts(v4・Pages Router)
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
});

App Routerに移行した後も、「パスが変わるだけだろう」という認識で最初はこう書いていました。

// app/api/auth/[...nextauth]/route.ts(間違った移行例)
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
});

export { handler as GET, handler as POST };

これ、一見動いているように見えてエラーが出ます。正確には型エラーで、TypeScriptが「NextAuth の使い方が違う」と言ってくる。

v5の正しい書き方

v5では、設定ファイルを別に切り出す構造になっています。

// auth.ts(プロジェクトルートに置く)
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [Google],
});
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";

export const { GET, POST } = handlers;

ポイントは2つです。

  1. NextAuth() は設定オブジェクトを受け取って、複数の関数(handlers, signIn, signOut, auth)を返す形になった
  2. auth.ts という設定ファイルを用意して、そこから各コンポーネントやMiddlewareに必要なものをimportする設計になっている

「それくらい公式ドキュメントに書いてあるでしょ」と思うでしょう。書いてあります。でも僕が最初に見つけた記事がv4の解説で、そこに「App Routerではこう書く」として間違ったv5コードが載っていました。情報源の鮮度を確認する癖をつけることの大切さを、改めて実感した瞬間でした。

「それどういうこと?」と思った人、次のセクションで実際のエラーメッセージも含めて説明します。


失敗その2——Middlewareが全然違う

これが今回の6時間のうち、おそらく3時間以上を占めた最大の罠でした。

v4のMiddleware

v4ではMiddlewareをこう書いていました。

// middleware.ts(v4)
export { default } from "next-auth/middleware";

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

next-auth/middleware からデフォルトエクスポートを再エクスポートするだけ。シンプルで、これが動いていたので「v5も同じでしょ」と思っていました。

v5でこれを書くと何が起きるか

next-auth/middleware がv5では存在しません

インポートしようとすると、モジュールが見つからないというエラーになります。これは「ミスっているのかインストール失敗なのか」の判別がつきにくくて、package.json を何度も確認したり node_modules を削除して再インストールしたりと無駄な時間を使いました。

v5の正しいMiddlewareの書き方

// middleware.ts(v5)
import { auth } from "@/auth";
import { NextResponse } from "next/server";

export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard");

  if (isOnDashboard && !isLoggedIn) {
    return NextResponse.redirect(new URL("/login", req.nextUrl));
  }

  return NextResponse.next();
});

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

auth.ts で作った auth 関数をMiddlewareのラッパーとして使う形です。req.auth の中にセッション情報が入っているので、ログイン状態の確認もそこで行います。

ここで注意が必要なのは matcher の設定です。v4時代のmatcherをそのままv5に持ち込むと、静的ファイルへのリクエストにもMiddlewareが走ってパフォーマンスが落ちます。上のコードのような「静的アセットを除外する正規表現」を使うのが定石です。

App Router全体の動作についてはReact Server Componentsの落とし穴まとめにも関連する話が書いてあります。Server ComponentsとMiddlewareの関係は地味につまずきポイントが多いので、合わせて読んでおくと理解が深まります。


コードが画面に表示されているラップトップのイメージ

失敗その3——Session callbackの型が変わってた

Middlewareの問題を乗り越えたところで、次の壁が来ました。Session callbackとJWT callbackの型定義の変更です。

やりたかったこと

ユーザーのロール情報(role: "admin" | "user")をセッションに含めたい。v4ではこうしていました。

// v4のcallbacks(間違ったv5コード)
callbacks: {
  async session({ session, token }) {
    session.user.role = token.role; // TypeScriptエラーになる
    return session;
  },
  async jwt({ token, user }) {
    if (user) {
      token.role = user.role; // TypeScriptエラーになる
    }
    return token;
  },
},

v5でこれを書くと、session.user.roletoken.role の部分でTypeScriptが怒ります。「role なんていうプロパティは知らない」と。

v5の正しいやり方

型を拡張する必要があります。プロジェクトルートかどこか適切な場所に型定義ファイルを置きます。

// types/next-auth.d.ts
import { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface Session {
    user: {
      role: "admin" | "user";
    } & DefaultSession["user"];
  }

  interface User {
    role: "admin" | "user";
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    role: "admin" | "user";
  }
}

この型定義を用意した上で、auth.ts のcallbacksを書きます。

// auth.ts(型拡張後の正しい実装)
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [Google],
  callbacks: {
    async session({ session, token }) {
      if (token.role) {
        session.user.role = token.role;
      }
      return session;
    },
    async jwt({ token, user }) {
      if (user?.role) {
        token.role = user.role;
      }
      return token;
    },
  },
});

TypeScriptとの付き合い方についてはTypeScript入門——型定義で防げるバグの話が参考になります。「型定義ファイルを作るとかめんどくさい」と思う気持ちはわかりますが、これをサボると後から必ず後悔します。コードで言うなら、型定義は「仕様書をコードに落とした状態」で、書いた瞬間から自分を守ってくれる保険になります。


結局どう動かしたか——最終的な構成

6時間17分かけてたどり着いた、動く構成をまとめます。

ファイル構成はこうなっています。

project-root/
├── auth.ts              ← NextAuth設定の核心
├── middleware.ts        ← 認証ガード
├── types/
│   └── next-auth.d.ts  ← 型拡張
└── app/
    ├── api/
    │   └── auth/
    │       └── [...nextauth]/
    │           └── route.ts  ← APIハンドラー
    └── dashboard/
        └── page.tsx     ← 認証が必要なページ

auth.ts の全体像を再掲します。実際に動いているコードです。

// auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/db";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async session({ session, token }) {
      if (token.sub) {
        session.user.id = token.sub;
      }
      if (token.role) {
        session.user.role = token.role;
      }
      return session;
    },
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role ?? "user";
      }
      return token;
    },
  },
  pages: {
    signIn: "/login",
    error: "/auth-error",
  },
});

アダプターにはDrizzle ORMを使っています。ORMの選定についてはDrizzle ORM vs Prisma 1ヶ月使い比べレビューに詳しく書きました。Auth.jsのアダプターもDrizzleとPrismaの両方に公式対応しているので、どちらを選んでも問題ありません。

Server Componentでセッションを取得するときは、auth.ts から auth をimportして使います。

// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth();

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

  return (
    <div>
      <p>ようこそ、{session.user.name} さん</p>
    </div>
  );
}

Client Componentでは useSession フックが使えますが、そのためには SessionProvider でラップする必要があります。app/layout.tsxSessionProvider を追加する際は、Client ComponentとServer Componentの境界に注意してください。この境界の話はReact Server Componentsの落とし穴で詳しく書いています。

デプロイ時の環境変数の扱いについてはNext.js デプロイ比較2026——Vercel vs Cloudflare Pages vs Fly.ioも参照してください。Auth.js v5はAUTH_SECRETという環境変数名が推奨に変わっていて、v4時代のNEXTAUTH_SECRETでも動くことは動くのですが、将来的な互換性を考えると早めに移行したほうがいいです。


教訓と「おすすめしない人」

やってよかった点

Auth.js v5の設計は、v4より明らかに良くなっています。auth.ts に設定を集約して、そこから必要なものをimportする構造は、コードの見通しがよくて気持ちいい。Middlewareも req.auth で直接セッションにアクセスできるので、v4時代の「Middlewareでセッション確認するためにtokenを複合化して……」みたいな煩雑さがなくなりました。

型安全性も上がっています。型定義ファイルを一度書いてしまえば、セッションの中身にアクセスするときにIDEが補完してくれる。地味ですが、開発体験として大きな違いです。

正直ここは微妙

  • ドキュメントの品質:繰り返しになりますが、v4とv5の情報が混在していて、公式ドキュメントを信頼しきれない場面がある。GitHubのDiscussionsやIssueを読むほうが最新情報にたどり着けることも多い
  • アダプターのバグ:v5対応のアダプターはまだバグが残っているものがあり、特にマイナーなDBアダプターは注意が必要
  • エラーメッセージがわかりにくい:設定ミスをしたときのエラーが「設定オブジェクトが不正」みたいな抽象的なメッセージで、どこが間違っているかわかりにくい

Auth.js v5をおすすめしない人

端的に言えば、以下に当てはまる場合はAuth.js以外を検討したほうが時間の節約になります。

  • 学習コスト より 実装速度を優先したい場合:SupabaseのAuthやClerkのほうが、特にNext.jsとの統合が簡単で詰まる要素が少ない。Supabaseポートフォリオ統合日記で比較を書きました
  • v4のコードが動いていて問題ない場合:v5への移行は「今の要件で困っていないなら急がなくていい」というのが正直な感想
  • チーム開発でAuth.jsの知見がない場合:ドキュメントが散らかっている以上、チームメンバー全員が独力でトラブルシュートできる環境が必要になります

テスト戦略についても一言だけ。Auth.jsを使った認証フローのテストは別途考える必要があります。フロントエンドテスト Jest vs Vitest 2026年版にも認証フローのモックについて少し触れています。


コーヒーを飲みながらコードを書くエンジニアのイメージ

よくある質問

Q: Auth.js v5はNext.js 14と15のどちらに対応していますか?

A: 両方に対応しています。ただし、Next.js 15はReact 19対応の変更が入っているため、Auth.jsのバージョンによっては型の不整合が出ることがあります。next-auth@beta の最新版を使うのが無難です。インストール時は npm install next-auth@beta を使ってください。

Q: v4からv5への移行は難しいですか?

A: 難しくはないですが、「一箇所変えれば終わり」ではありません。この記事で紹介した3つの変更点(インポートパス・Middleware・型定義)は最低限対応が必要です。既存プロジェクトで動いているv4コードがある場合、半日は見ておくことをおすすめします。

Q: Clerk、Supabase Auth、Auth.jsをどう使い分ければいいですか?

A: 個人的な基準はこうです。Clerk:認証UI込みで早く動かしたい・有料でもOK。Supabase Auth:Supabaseをすでに使っている・DBと認証を一体管理したい。Auth.js:無料・OSS・自前DBで認証を完全にコントロールしたい。コードで言うなら、ClerkはSaaSのマネージドサービス、Auth.jsはオープンソースのライブラリです。

Q: AUTH_SECRET の値はどうやって生成しますか?

A: openssl rand -base64 32 コマンドで生成できます。Vercelにデプロイする場合は、Auth.jsの公式ドキュメントにVercel用の自動生成リンクがあるのでそちらを使うのが簡単です。ローカルでは .env.local に書いて、本番環境では環境変数として設定します。絶対にGitHubにコミットしないこと。

Q: GitHub Actionsで自動デプロイする場合、環境変数の管理はどうすればいいですか?

A: GitHub ActionsのSecrets機能を使います。GitHub Actions入門2026年版にシークレットの設定方法を書きました。AUTH_SECRETGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET あたりはSecretsとして登録して、ワークフローファイルから参照する形にしてください。


追記(2026/05/19)

この記事を公開後、いくつか追加情報をいただきました。

まず、session.strategy: "database" を使う場合(JWTではなくDBにセッションを保存する場合)は、jwt callbackの中でやることがほぼなくなります。ただし、アダプターを正しく設定していないとセッションが永続化されないので、DBの接続確認を先にやっておくことをおすすめします。

もう一点。Next.js 15.1以降では、auth() の呼び出しがv15以前よりも若干変わっている部分があります。具体的には、Middlewareでの auth の型推論が改善されていて、req.auth の型がより精確になっています。古いv5のドキュメントを参照している場合は、GitHub上の最新READMEも確認してください。


あわせて読みたい

【買い切り型】Colosoでプロから学ぶ

業界トップの講師によるオンライン講座。買い切り型で何度でも復習可能。受講期限なし。

講座一覧を見る →