👌

Prisma × NextAuth.js:知っておくべき3つのポイント

に公開

Next.js アプリケーションで認証機能を実装する際、NextAuth.js は強力な選択肢です。そして、データベースとの連携には Prisma が非常に役立ちます。この2つを組み合わせることで、安全で効率的な認証システムを構築できますが、スムーズに進めるためにはいくつかの重要なポイントがあります。

1. 認証情報を保存する「必須テーブル」の定義

NextAuth.js は、ユーザーの認証情報をデータベースに保存するために、特定の構造を持つテーブルを必要とします。Prisma をデータレイヤーとして使う場合、prisma/schema.prisma ファイルに以下の3つのモデルを定義することが不可欠です。

・Account: ユーザーがどのプロバイダー(Googleなど)でログインしたかの情報。
・Session: ユーザーのセッション情報(ログイン状態を維持するための情報)。
・VerificationToken: メールアドレス認証などに使われる一時的なトークン情報。

これらのモデルが正しく定義されていないと、NextAuth.js は認証情報をデータベースに保存できず、ログインが完了しません。

prisma/schema.prismaでの定義例:

// 既存の User モデルの下に追加
model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

2. User モデルへの「追加フィールド」と「リレーション」

OAuth プロバイダー(Googleなど)からの認証情報には、ユーザー名やメールアドレスだけでなく、プロフィール画像(image)やメールアドレスの認証状況(emailVerified)といったデータも含まれています。これらの情報をデータベースに保存したい場合は、あなたのUserモデルにこれらのフィールドを追加する必要があります。

また、Prisma はデータベースのリレーションシップ(関連性)を厳密に管理します。AccountSessionモデルがUserモデルを参照するように定義されているなら、その逆も必要です。つまり、Userモデルにも、自身に関連するAccountSessionのリスト(例: accounts Account[], sessions Session[])をリレーションとして追加しなければなりません。これが欠けていると、スキーマのバリデーションエラーが発生します。

prisma/schema.prisma での定義例(Userモデル):

model User {
  id            String    @id @default(cuid())
  email         String    @unique
  name          String?
  image         String?   // プロフィール画像用に追加
  emailVerified DateTime? @map("email_verified") // メール認証日時用に追加(Null許容)
  xp            Int         @default(0)
  level         Int         @default(1)
  habits        Habit[]
  checkIns      CheckIn[]
  memos         DailyMemo[]
  createdAt     DateTime  @default(now())

  // Account と Session からのリレーションを受け取るため、逆リレーションを追加
  accounts  Account[]
  sessions  Session[]
}

3. PrismaClient の「適切な初期化」

NextAuth.js がPrisma アダプターを通じてデータベース操作を行う際、PrismaClientのインスタンスを使用します。特に Next.js の開発環境では、ホットリロードの仕組み上、PrismaClientのインスタンスが複数生成されてしまうと問題が起こることがあります。

これを避けるために、src/app/api/auth/[...nextauth]/route.tsのような NextAuth.js の設定ファイル内で、PrismaClient のインスタンスをグローバルな変数として一度だけ初期化するパターンを採用することが推奨されます。これにより、アダプターに常に単一で有効な PrismaClientのインスタンスが渡され、findUniqueなどのデータベース操作が正しく実行されるようになります。

src/app/api/auth/[...nextauth]/route.tsでの初期化例:

// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";

// --- PrismaClientのグローバル初期化 ---
declare global {
  var prisma: PrismaClient | undefined;
}

const prisma = global.prisma || new PrismaClient();
if (process.env.NODE_ENV === "development") global.prisma = prisma;
// --- 初期化ここまで ---

export const authOptions = {
  // ここで初期化したprismaインスタンスをアダプターに渡す
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    }),
  ],
  // ... その他の設定(session, pages, callbacksなど)
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

まとめ

Prisma と NextAuth.js の連携でつまずきやすいのは、主にスキーマ定義とPrismaClientの初期化方法です。この3つのポイントを理解し、上記のコード例を参考に適切に設定することで、エラーを回避し、認証システムをスムーズに構築できると思います。

Discussion