NextAuth(Auth.js)v5を個人開発2本(Web + iOSアプリ)で運用しています。動いてはいるものの、セッションがJWTなのかDBなのか聞かれたら即答できない時期が長くありました。
調べて整理したら、押さえるべきルールは実質2つでした。
- セッション戦略は「adapterを設定したかどうか」で暗黙に切り替わる(なし=jwt、あり=database)
- Credentialsプロバイダはdatabase戦略ではセッションが作られないので、
strategy: "jwt"の明示が必要
この記事では、この分岐がどこで効いてくるのかを、実際に運用している設定を貼りながら整理します。
よくある症状
NextAuthを使っていて、こういう状態になったことはないでしょうか。
- チュートリアル通りに書いたら動いたが、セッションがどこに保存されているのか答えられない
-
sessionコールバックの引数にtokenを書いている記事とuserを書いている記事があり、どちらが正しいのか分からない - Credentialsプロバイダを追加したら、ログインできたはずなのにセッションが取れない
全部、冒頭の2ルールから説明がつきます。
前提: セッション戦略は2つある
| jwt戦略 | database戦略 | |
|---|---|---|
| セッションの実体 | 暗号化JWT(JWE)そのものがCookie | DBのsessionsテーブルの行 |
| Cookieの中身 | クレームを含む暗号化トークン | ランダムなセッショントークン(DBへの参照キー) |
| サーバー側の照会 | 不要(復号だけ) | 毎回DBを引く |
| 即時失効(強制ログアウト) | 難しい | DBの行を消すだけ |
| sessionコールバックの引数 | { session, token } |
{ session, user } |
重要なのはデフォルト値の決まり方です。明示しなければ、adapterを渡していないときはjwt、渡しているときはdatabaseになります。
つまりDrizzleAdapterを繋いだ瞬間に、セッションの保存場所はCookieからDBへ静かに切り替わります。ここに気付かないまま古い記事からコールバックだけコピペすると、tokenがundefinedで首をかしげることになります。私はなりました。
Credentialsだけは例外
もう1つのハマりどころがCredentialsプロバイダです。Credentialsによるログインではdatabaseセッションが作成されません。adapterを設定した状態(デフォルトがdatabase戦略)でCredentialsログインすると、認証自体は通るのにセッションが保存されず、「ログインしたのにされていない」状態になります。
対処はsession: { strategy: "jwt" }を明示することです。ただしこれは全プロバイダに効くグローバル設定なので、「OAuthはDBセッション、Credentialsも併用」という構成は素直にはできません。
本番でどう使い分けているか
私のプロダクト(squadnote)では、この制約を逆手に取ってこう運用しています。
- 本番: Google / Apple OAuthのみ。database戦略(デフォルトのまま)
- E2Eテストと開発環境のみ: テストログイン用Credentialsを有効化して、jwt戦略に切り替え
実際の設定の抜粋です。
export function getAuthConfig(env?: { ENABLE_TEST_LOGIN?: string /* ... */ }): NextAuthConfig {
const enableTestLogin = env?.ENABLE_TEST_LOGIN === "true";
const providers: NextAuthConfig["providers"] = [
Google({ /* ... */ }),
Apple({ /* ... */ }),
];
if (enableTestLogin) {
providers.push(
Credentials({
id: "test-login",
async authorize(credentials) { /* テストユーザーを返す */ },
}),
);
}
return {
providers,
adapter: drizzleAdapter, // これがある時点でデフォルトはdatabase戦略
// Credentials ProviderはJWTセッション戦略が必要
session: enableTestLogin ? { strategy: "jwt" } : undefined,
callbacks: {
// 戦略によってsessionコールバックの「相方」が変わる
jwt: enableTestLogin
? ({ token, user }) => {
if (user) token.id = user.id; // 初回ログイン時のみuserが渡ってくる
return token;
}
: undefined,
session: enableTestLogin
? ({ session, token }) => ({
// jwt戦略: DBを引かないのでtokenから復元する
...session,
user: { ...session.user, id: (token as { id?: string }).id ?? "" },
})
: ({ session, user }) => ({
// database戦略: sessionsとJOINされたuserがそのまま渡ってくる
...session,
user: { ...session.user, id: user.id },
}),
},
};
}
この構成で押さえている点を挙げます。
sessionにundefinedを渡すと、デフォルトの分岐(adapterあり=database)にそのまま任せられます。条件付きで戦略を切り替えたいときに便利です。
jwt戦略のときだけjwtコールバックが必要になります。注意したいのは、userが渡ってくるのはサインイン直後の1回だけということです。以降のリクエストでは、tokenに自分で書き込んだ値だけが頼りになります。
そしてsessionコールバックは戦略ごとに引数が違うので、両対応するなら2系統書くしかありません。型を見るとtokenもuserも生えているように見えるのが罠で、実行時にどちらが入っているかは戦略次第です。
いまどちらの戦略で動いているか確認する方法
自分のアプリがどちらで動いているかは、次の2点を見れば分かります。
- DBのsessions(相当の)テーブルに行が増えるか。増えるならdatabase戦略
- セッションCookie(
authjs.session-token)の中身。eyJ...形式の長いトークン(JWE)ならjwt戦略、短いランダム文字列ならdatabase戦略の参照トークン
Drizzle + D1で運用している場合はwrangler d1 execute <db> --command "select * from session"あたりで確認できます。
まとめ
| 状況 | 戦略 | やること |
|---|---|---|
| adapterなし | jwt(デフォルト) | jwt/sessionコールバックでtokenを扱う |
| adapterあり・OAuthのみ | database(デフォルト) |
session({ session, user })でuserを使う |
| Credentials使用 | jwt必須 |
strategy: "jwt"を明示。DBセッションとの併用は不可 |
| OAuth=DB、Credentialsはテストのみ | 環境で切替 | この記事の構成(環境変数でstrategyとコールバックを出し分け) |
「adapterを入れた瞬間に戦略が切り替わる」「Credentialsはjwt必須」。この2つを押さえてから、セッション周りで首をかしげる回数は目に見えて減りました。
環境
- next-auth 5.0.0-beta.25 / @auth/drizzle-adapter 1.7.x
- Next.js 15(App Router)+ Drizzle ORM + Cloudflare D1(@opennextjs/cloudflare)