Claude CodeでAuth.js v5へ移行する — auth.ts集約からロールの型まで
NextAuth.js v4からAuth.js v5への移行をClaude Codeで段階的に進める手順です。auth.tsへの集約、環境変数のAUTH_化、ロールを載せるcallbacksと型の拡張を順に直します。
NextAuth.js v4からAuth.js v5への移行は、差分の数は多くありません。ただし変更が散らばる点が厄介です。設定は1か所に集まり、呼び出し側はgetServerSessionやwithAuthからauth()へ置き換わり、環境変数名も変わります。
Claude Codeに任せるなら、置換のパターンが決まっている作業から順に渡すのが安全です。この記事では、auth.tsへの集約、呼び出し側の置換、環境変数、ロールを載せるcallbacksと型の拡張という順番と、各段階で何を確かめるかを説明します。
v4からv5で変わる場所
v5はnext-authパッケージの大きな書き直しですが、破壊的変更はできるだけ少なく抑えられています。移行ガイドが挙げる主な変更を、直す場所の側から並べ直すと次のようになります。
| 場所 | v4 | v5 |
|---|---|---|
| 設定 | v4authOptionsを各所へ渡す | v5ルートのauth.tsがauth・handlers・signIn・signOutをエクスポート |
| Server Component | v4getServerSession(authOptions) | v5auth() |
| Proxy(旧Middleware) | v4withAuth(...)ラッパー | v5authのエクスポートかauth()ラッパー |
| API Route(Node.js) | v4getServerSession(req, res, authOptions)とgetToken(req) | v5auth(req, res) |
| getServerSideProps | v4getServerSession(ctx.req, ctx.res, authOptions) | v5auth(ctx) |
| Client Component | v4useSession() | v5useSession()(変更なし) |
| 環境変数 | v4NEXTAUTH_接頭辞 | v5AUTH_接頭辞 |
| 型 | v4NextAuthOptions | v5NextAuthConfig |
| Cookie名 | v4next-auth接頭辞 | v5authjs接頭辞 |
あわせて、最低要件はNext.js 14.0以上になりました。next-auth/nextとnext-auth/middlewareからのimportは使えなくなります。データベースアダプターは@next-auth/*-adapterから@auth/*-adapterのスコープへ移ります。
導入はnext-auth@betaのタグを指定する形で、移行ガイドもこのタグで案内しています。なお、Auth.jsの各ページには「Auth.jsプロジェクトはBetter Authの一部になった」という告知が出ています。採用や移行の判断では、この点も確認材料になります。
着手前にClaude Code側で整える
認証まわりは、直す対象に秘密情報が隣り合います。最初にClaude Codeの設定を整えておくと、後の作業が楽になります。
秘密情報を読ませない
AUTH_SECRETや各プロバイダーのクライアントシークレットは.env系のファイルに入っています。公式のベストプラクティスは、denyルールでReadを止める例を示しています。denyはautoやbypassPermissionsを含むどの権限モードでも効きます。
{
"permissions": {
"allow": ["Bash(pnpm tsc --noEmit)"],
"ask": ["Bash(git push *)"],
"deny": ["Read(./.env)", "Read(./.env.local)", "Read(./secrets/**)"]
}
}この設定を.claude/settings.jsonに置くと、Readツールとcat .envのどちらでも読めなくなります。一方で、ファイルを開くスクリプトまでは止められません。環境変数の改名では中身を見せずに済む作業を選び、キー名の一覧だけを.env.exampleから渡すのが現実的です。
確認コマンドをCLAUDE.mdに書く
公式は、CLAUDE.mdに入れる価値があるものとして、確認用のコマンドを最初に挙げています。移行の間だけ、次のような節を足す形が使えます。
# Auth.js v5移行(作業中のみ)
- 型検査: pnpm tsc --noEmit
- v4の呼び出しの残り: grep -rn "getServerSession\|next-auth/next\|next-auth/middleware\|authOptions" src app pages
- auth.ts以外でNextAuth()を呼ばない
- .envは読まない。変数名は.env.exampleで確認する移行が終わったらこの節は消します。残すと、次の作業でもv4の手順が前提として読まれます。
先にプランモードで見積もる
変更をディスクに書く前に確認したいときは、プランモードが使えます。claude --permission-mode planで起動するか、セッション中にShift+Tabで切り替えます。プランモードのClaudeはファイルを読んで計画を出しますが、承認するまで編集しません。
Auth.js v4からv5への移行を計画して。まだ編集しない。
1. authOptionsを使っている全ファイルと、getServerSession・getToken・withAuthの呼び出し箇所を一覧にする
2. NEXTAUTH_で始まる環境変数の参照箇所を一覧にする
3. callbacksにあるjwt・sessionの中身と、型拡張(declare module)の有無を報告する出てきた一覧が、以降の段階で「どこまで終わったか」を測る物差しになります。
移行を4段階に分ける
- 1
auth.tsに集約する
設定を
auth.tsへ移し、API Routeをhandlersの再エクスポートだけにします。 - 2
呼び出し側を置換する
getServerSession・getToken・withAuthをauth()に置き換えます。 - 3
環境変数を改名する
NEXTAUTH_系をAUTH_系へ。変更はプロバイダー側の設定にも及びます。 - 4
ロールとセッションの型を直す
callbacksでロールを載せ、型を宣言で拡張します。
手順1:auth.tsに設定を集約する
目標は、設定オブジェクトを1つのファイルからエクスポートし、他の場所はauthOptionsを持ち回らない形にすることです。移行ガイドは、リポジトリのルートにauth.tsを置き、そこからauth・handlers・signIn・signOutをエクスポートする形を示しています。
authOptionsを定義しているファイルを、ルートのauth.tsに移して。
- NextAuth(authOptions)の呼び出しを const { auth, handlers, signIn, signOut } = NextAuth({...}) に変える
- app/api/auth/[...nextauth]/route.ts は handlers の GET と POST の再エクスポートだけにする
- プロバイダーの定義はそのまま。@auth/coreからのimportがあれば next-auth/providers/* に直すAPI Routeのファイルは大きく縮みます。公式の例は次のとおりです。
import { handlers } from "@/auth"
export const { GET, POST } = handlers設定オブジェクトの中身は、これまでと同じです。ガイドも「NextAuth()に渡す設定は以前と同じ」と書いています。したがって、この段階ではcallbacksやsessionの中身を触らず、場所だけを動かす指示にしておきます。中身まで同時に変えると、失敗の原因が分けられなくなります。
落とし穴
アダプターがEdgeで動かないとProxyが落ちる
アダプターを使うと、セッション方式はdatabaseが既定になります。使っているORMやアダプターがEdge実行環境に対応していなければ、Proxy(旧Middleware)でdatabase方式は使えません。ガイドの回避策は設定を2つに割る形です。auth.config.tsにアダプターを含まない共通設定を置き、auth.tsでアダプターとsession: { strategy: "jwt" }を足し、Proxy側はauth.config.tsだけでNextAuth()を初期化します。
アダプターを使うプロジェクトなら、プランの時点で「Proxyからauth.tsをimportしていないか」を確認項目に入れておくと、この落とし穴を避けられます。
手順2:呼び出し側をauth()に置き換える
置換のパターンは表で決まっているので、Claude Codeに一括で渡しやすい段階です。ガイドの差分例を指示に直すと、次のようになります。
次の置換を全ファイルに適用して。置換後に pnpm tsc --noEmit を実行し、通るまで直す。
- Server Component: getServerSession(authOptions) → auth()
- Pages API Route: getServerSession(req, res, authOptions) と getToken({ req }) → auth(req, res)
- getServerSideProps: getServerSession(ctx.req, ctx.res, authOptions) → auth(ctx)
- middleware: export { default } from "next-auth/middleware" → export { auth as proxy } from "@/auth"
importは next-auth/next と next-auth/jwt から @/auth に変える。Proxyについては、Next.js 16でmiddleware.tsがproxy.tsに改名されています。ガイドの例もproxy.tsで書かれ、旧版ではmiddleware.tsと読み替える注記が付いています。使っているNext.jsのバージョンを、先に指示へ入れておくと迷いません。
API Routeでのセッション取得
v4
const session = await getServerSession(req, res, authOptions)
const token = await getToken({ req })v5
const session = await auth(req, res)置換のあとに挙動の差も1つ残ります。auth()にresを渡すと、セッションの有効期限が更新されます。従来のgetToken()には、この更新がありませんでした。既定の有効期限は30日で、authOptions.session.maxAgeで変えられます。getTokenを使っていた箇所は、置換後に有効期限の更新が起きる点をレビューで拾います。
クライアント側のuseSession()は変更されていません。next-auth/reactのimportは"use client"付きになったため、クライアントコンポーネントでそのまま使えます。コンテキストでセッションを読むなら、<SessionProvider />で囲む必要があります。
手順3:環境変数をAUTH_に揃える
環境変数に破壊的変更はありません。ただし整理された部分があり、改名の方針が示されています。
- 接頭辞は
AUTH_に揃える。NEXTAUTH_は使われなくなった AUTH_GITHUB_IDとAUTH_GITHUB_SECRETのように命名すると、プロバイダーの設定へ明示的に渡さなくても検出されるNEXTAUTH_URL/AUTH_URLは、多くの環境でリクエストヘッダーからホストを推測するため必須でなくなった- プロキシの背後で動かすときは
AUTH_TRUST_HOST(設定のtrustHost: trueと同じ役割)を使う - 本当に必要なのは
AUTH_SECRETだけで、設定にsecretとして渡す必要もない
Claude Codeには、.envの中身ではなく、コード側とデプロイ設定側の参照を直させます。
.envは読まない。コード内の process.env.NEXTAUTH_* の参照を洗い出して、
AUTH_* に対応させる表を作って。NEXTAUTH_URL は不要になったか判断材料を付ける。
実際の値の書き換えは私がやる。値そのものの書き換え(ホスティング側の環境変数、CI、.env.local)は、手元で行うと決めておくと、秘密情報がセッションに載りません。
プロバイダーのシークレットをAUTH_<PROVIDER>_IDの形にすれば、設定側のclientId・clientSecretを消せます。ここは消し忘れても動くので、プランの一覧に入れておきます。
手順4:ロールを載せるcallbacksと型を直す
ロールの埋め込みは、移行で最も壊れやすい部分です。実行時の値と型の宣言が、別々に変わるためです。公式のRBACガイドは、セッション方式で書き分けています。
JWT方式の場合
データベースを使わない構成では、ロールはCookieに入ります。プロバイダーのprofile()でユーザーにロールを持たせ、jwt()でトークンへ、session()でセッションへ渡します。
export const { handlers, auth } = NextAuth({
providers: [
Google({
profile(profile) {
return { role: profile.role ?? "user", ...profile }
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) token.role = user.role
return token
},
session({ session, token }) {
session.user.role = token.role
return session
},
},
})jwt()のuserが渡されるのはサインイン時だけなので、if (user)の分岐は必要です。ロールの判定ロジックは自前で書くか、プロバイダーが返す値を使います。ロールの判定は、Claude Codeに任せず人が決める部分です。
データベース方式の場合
アダプターを使う構成では、Userモデルにrole列を足します。profile()の戻り値がユーザー作成に使われ、session()はtokenではなくuserから読みます。
callbacks: {
session({ session, user }) {
session.user.role = user.role
return session
},
},型の宣言を足す
実行時にロールが載っても、型がないとsession.user.roleは型エラーになります。Auth.jsは、ジェネリクスではなくモジュール拡張で型を拡張する方針です。auth.tsなどtsconfigの対象に含まれるファイルへ、次のように宣言します。以下は公式のaddressの例にならった形です。
import NextAuth, { type DefaultSession } from "next-auth"
declare module "next-auth" {
interface Session {
user: {
role: string
} & DefaultSession["user"]
}
interface User {
role?: string
}
}& DefaultSession["user"]を付けるのは、既定のユーザー項目を残すためです。付けないと、名前やメールの型が上書きで消えます。JWT方式ではnext-auth/jwtのJWTインターフェースも同じ要領で拡張します。公式の例では、declare module "next-auth/jwt"の中でプロパティを足しています。
callbacksでrole を session.user に載せる処理を auth.ts に書いて。
- JWT方式。jwt()で token.role、session()で session.user.role
- declare module "next-auth" と "next-auth/jwt" で Session・User・JWT を拡張
- 終わったら pnpm tsc --noEmit を実行して、出力を貼る最後の1行が、公式のベストプラクティスにある「出力を証拠として出させる」にあたります。「型検査が通りました」だけでなく、実行したコマンドと出力を返させると、読んで確かめられます。
取りこぼしを閉じる確認
置換が終わったら、CLAUDE.mdに書いたgrepを実行します。v4の呼び出しが0件になれば、置換漏れはありません。残った場合は、authOptionsを使うテストや、サンプルコード、ドキュメント内のスニペットに残っていることが多いです。
次に、型検査の通過を完了条件にします。公式は、完了の判定をClaudeに渡す方法として、プロンプトでの指示、/goal、Stopフックを順に挙げています。移行のように途中で止まりやすい作業では、/goal pnpm tsc --noEmit passes and grep for getServerSession returns nothingのように、通る条件を文で渡せます。確実に止めたいならStopフックで型検査を走らせ、終了コード2でClaudeを作業に戻します。
実際の動作確認は、人が行います。ログイン、ログアウト、ロール別のページ到達、Proxyによる保護を、ブラウザーで一通り通します。Cookie名がnext-authからauthjsの接頭辞に変わるので、Cookie名を直接参照しているE2Eテストや、リバースプロキシの設定があれば別に洗います。
つまずきやすい点
OAuthプロバイダーが動かなくなる。v5は@auth/coreの上に作られ、OAuth/OIDCの仕様準拠が厳しくなりました。既存のプロバイダーで壊れるものがあり得ると、ガイドも明記しています。プロバイダーごとにログインを試します。
idToken: falseの意味が変わった。以前は「IDトークンの検証をしない」でしたが、v5ではuserinfo_endpointも見に行く指示になります。IDトークンを完全に無効にするわけではありません。
OAuth 1.0は非推奨。OAuth 1.0のプロバイダーを使っている場合は、移行の対象外として別に扱う必要があります。
アカウントテーブルの列が不要になった。データベーススキーマに破壊的変更はありません。ただしOAuth 1.0が外れたため、oauth_token_secretとoauth_tokenは使っていなければ削れます。GitHubのrefresh_token_expires_inのような列も必須ではなくなりました。使っているなら、新しいaccount()コールバックで返します。
アダプターのパッケージ名。 @next-auth/prisma-adapterから@auth/prisma-adapterへの変更は、package.jsonとimportの両方に及びます。
移行を閉じる条件
v4の呼び出しのgrepが0件、pnpm tsc --noEmitが通過、ロール別のページ到達を手元で確認済み、の3つが揃えば終わりです。CLAUDE.mdの移行専用の節を消し、環境変数の旧名を各環境から外します。
同じ「段階に分ける」型は、テストランナーの移行でも使えます。Claude CodeでJestからVitestへ移行するでは、設定、モック、量産の順で結果を照合しました。データベースの列型を変える場合は、Claude Codeでbigintへ主キー移行するが参考になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。