概要
このガイドでは、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
テーブルに独自フィールドを簡単に追加可能 - プロバイダーの追加も容易
トラブルシューティング
よくある問題
-
型エラー:
next-auth.d.ts
ファイルが正しく読み込まれているか確認 - DB接続エラー: 環境変数とスキーマ定義を確認
- セッションデータが空: コールバック関数の実行順序を確認
この実装により、サードパーティ認証を使いつつ、完全にアプリ独自のセッションデータを管理できます。