0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NextAuth.js + DrizzleORM カスタムセッション実装ガイド

Posted at

概要

このガイドでは、NextAuth.jsとDrizzleORMを使用して、サードパーティ認証(Google、GitHub等)でログインしつつ、アプリ独自のユーザーデータをセッションに保存する方法を解説します。

アーキテクチャ

  • 認証: NextAuth.js + OAuth プロバイダー
  • データベース: DrizzleORM
  • セッション: アプリ独自データのみ(OAuth情報は除外)
  • ユーザー管理: 初回ログイン時に独自ユーザー作成、2回目以降は既存データを取得

1. 依存関係のインストール

npm install next-auth drizzle-orm @auth/drizzle-adapter
npm install -D drizzle-kit

2. データベーススキーマ定義

// lib/schema.ts
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'

export const appUsers = pgTable('app_users', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  email: text('email').unique().notNull(),
  username: text('username').notNull(),
  displayName: text('display_name').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow(),
})

// NextAuth.js用のテーブルも必要(Adapter用)
export const users = pgTable('users', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  name: text('name'),
  email: text('email').notNull(),
  emailVerified: timestamp('emailVerified'),
  image: text('image'),
})

export const accounts = pgTable('accounts', {
  userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
  type: text('type').notNull(),
  provider: text('provider').notNull(),
  providerAccountId: text('providerAccountId').notNull(),
  refresh_token: text('refresh_token'),
  access_token: text('access_token'),
  expires_at: integer('expires_at'),
  token_type: text('token_type'),
  scope: text('scope'),
  id_token: text('id_token'),
  session_state: text('session_state'),
}, (account) => ({
  compoundKey: primaryKey({
    columns: [account.provider, account.providerAccountId],
  }),
}))

export const sessions = pgTable('sessions', {
  sessionToken: text('sessionToken').primaryKey(),
  userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
  expires: timestamp('expires').notNull(),
})

3. TypeScript型定義

// types/next-auth.d.ts
import NextAuth from "next-auth"

declare module "next-auth" {
  interface User {
    appUserId?: string
    username?: string
    displayName?: string
  }

  interface Session {
    user: {
      appUserId: string
      username: string
      displayName: string
    }
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    appUserId?: string
    username?: string
    displayName?: string
  }
}

4. NextAuth.js設定

// pages/api/auth/[...nextauth].ts または app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import { db } from '@/lib/db'
import { appUsers } from '@/lib/schema'
import { eq } from 'drizzle-orm'

// ヘルパー関数
async function getOrCreateAppUser(email: string) {
  // 既存ユーザーを検索
  const [existingUser] = await db
    .select()
    .from(appUsers)
    .where(eq(appUsers.email, email))
  
  if (existingUser) {
    return existingUser // 既存ユーザーの最新データを返す
  }
  
  // 初回ログイン時のみ作成
  const [newUser] = await db
    .insert(appUsers)
    .values({
      email,
      username: generateUsername(),
      displayName: email.split('@')[0] // メールアドレスの@前を表示名に
    })
    .returning()
  
  return newUser
}

function generateUsername(): string {
  return `user_${Date.now()}`
}

export default NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    // 他のプロバイダーも追加可能
  ],
  session: {
    strategy: 'database', // または 'jwt'
  },
  callbacks: {
    signIn: async ({ user, account, profile }) => {
      try {
        // 毎回DBから最新のアプリユーザーデータを取得
        const appUser = await getOrCreateAppUser(profile?.email || user.email!)
        
        // userオブジェクトにアプリ独自データを設定
        user.appUserId = appUser.id
        user.username = appUser.username
        user.displayName = appUser.displayName
        
        return true
      } catch (error) {
        console.error('SignIn callback error:', error)
        return false
      }
    },
    jwt: ({ token, user }) => {
      // signInで設定されたデータをJWTに保存
      if (user) {
        token.appUserId = user.appUserId
        token.username = user.username
        token.displayName = user.displayName
      }
      return token
    },
    session: ({ session, token, user }) => {
      // JWT戦略の場合
      if (token) {
        return {
          ...session,
          user: {
            appUserId: token.appUserId!,
            username: token.username!,
            displayName: token.displayName!,
          }
        }
      }
      
      // Database戦略の場合
      if (user) {
        return {
          ...session,
          user: {
            appUserId: user.appUserId!,
            username: user.username!,
            displayName: user.displayName!,
          }
        }
      }
      
      return session
    }
  },
  pages: {
    signIn: '/auth/signin',
    error: '/auth/error',
  },
})

5. 環境変数設定

# .env.local
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key

# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

# Database
DATABASE_URL=your-database-url

6. クライアントサイドでの使用

// pages/profile.tsx または app/profile/page.tsx
import { useSession } from 'next-auth/react'

export default function Profile() {
  const { data: session, status } = useSession()

  if (status === 'loading') return <p>Loading...</p>
  if (status === 'unauthenticated') return <p>Access Denied</p>

  return (
    <div>
      <h1>プロフィール</h1>
      <p>ユーザーID: {session?.user.appUserId}</p>
      <p>ユーザー名: {session?.user.username}</p>
      <p>表示名: {session?.user.displayName}</p>
    </div>
  )
}

7. SessionProvider設定

// pages/_app.tsx または app/layout.tsx
import { SessionProvider } from 'next-auth/react'

export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

8. データベースマイグレーション

# マイグレーションファイル生成
npx drizzle-kit generate

# マイグレーション実行
npx drizzle-kit migrate

ポイント

セキュリティ

  • OAuth情報はNextAuth.jsが管理するテーブルに保存
  • アプリ独自データは分離して管理
  • セッションにはアプリデータのみ含める

パフォーマンス

  • signInコールバックで毎回最新データを取得
  • 2回目以降のログインでもユーザー情報の変更が反映される

拡張性

  • appUsersテーブルに独自フィールドを簡単に追加可能
  • プロバイダーの追加も容易

トラブルシューティング

よくある問題

  1. 型エラー: next-auth.d.tsファイルが正しく読み込まれているか確認
  2. DB接続エラー: 環境変数とスキーマ定義を確認
  3. セッションデータが空: コールバック関数の実行順序を確認

この実装により、サードパーティ認証を使いつつ、完全にアプリ独自のセッションデータを管理できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?