Claude Media
Claude CodeでAuth.js v5へ移行する — auth.ts集約からロールの型まで

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パッケージの大きな書き直しですが、破壊的変更はできるだけ少なく抑えられています。移行ガイドが挙げる主な変更を、直す場所の側から並べ直すと次のようになります。

場所v4v5
設定v4authOptionsを各所へ渡すv5ルートのauth.tsがauth・handlers・signIn・signOutをエクスポート
Server Componentv4getServerSession(authOptions)v5auth()
Proxy(旧Middleware)v4withAuth(...)ラッパーv5authのエクスポートかauth()ラッパー
API Route(Node.js)v4getServerSession(req, res, authOptions)とgetToken(req)v5auth(req, res)
getServerSidePropsv4getServerSession(ctx.req, ctx.res, authOptions)v5auth(ctx)
Client Componentv4useSession()v5useSession()(変更なし)
環境変数v4NEXTAUTH_接頭辞v5AUTH_接頭辞
型v4NextAuthOptionsv5NextAuthConfig
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. 1

    auth.tsに集約する

    設定をauth.tsへ移し、API Routeをhandlersの再エクスポートだけにします。

  2. 2

    呼び出し側を置換する

    getServerSession・getToken・withAuthをauth()に置き換えます。

  3. 3

    環境変数を改名する

    NEXTAUTH_系をAUTH_系へ。変更はプロバイダー側の設定にも及びます。

  4. 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でのセッション取得

BEFORE

v4

const session = await getServerSession(req, res, authOptions)
const token = await getToken({ req })
AFTER

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へ主キー移行するが参考になります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. authjs.dev/getting-started/migrating-to-v5
  2. authjs.dev/guides/role-based-access-control
  3. authjs.dev/getting-started/typescript
  4. code.claude.com/docs/en/common-workflows
  5. code.claude.com/docs/en/best-practices
  6. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn