Next.js App RouterでAuth.js v5を導入したら6時間溶けた話|ハマりポイント全まとめ【2026年版】
Next.js App Router + Auth.js v5の認証実装でハマった失敗談。v4からの破壊的変更・Middleware設定・Session/JWT切り替えのつまずきポイントを現役フリーランスが実コードで解説。
※当サイトはアフィリエイトプログラムに参加しています。記事内のリンクから商品を購入すると、当サイトに報酬が支払われることがあります。詳しくはプライバシーポリシーをご覧ください。
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つです。
NextAuth()は設定オブジェクトを受け取って、複数の関数(handlers,signIn,signOut,auth)を返す形になった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.role と token.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.tsx に SessionProvider を追加する際は、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_SECRET、GOOGLE_CLIENT_ID、GOOGLE_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も確認してください。