NextAuth.js
認証ライブラリ
NextAuth.js
概要
NextAuth.jsは、Next.jsアプリケーション向けの完全な認証ソリューションで、OAuth、メール、クレデンシャル認証などの複数の認証方法をサポートし、セキュアで使いやすい認証システムを提供します。
詳細
NextAuth.js(現在はAuth.jsとしても知られる)は、Next.jsアプリケーション専用に設計された認証ライブラリです。2024年現在、バージョン5.0(Auth.js v5)として大幅にアップデートされ、より柔軟で強力な認証機能を提供しています。
50以上のOAuthプロバイダーを内蔵サポートしており、Google、GitHub、Facebook、Twitter、Microsoft、Apple、Discord、Spotifyなど主要なサービスとの統合が容易です。メール/パスワード認証、マジックリンク認証、カスタムクレデンシャル認証にも対応しており、様々な認証ニーズに対応できます。
フレームワーク統合としては、Next.js App Router(v13+)とPages Router両方をサポートし、React、SvelteKit、Express、Qwik、SolidStartなど他のフレームワークでも使用可能です。データベース統合では、Prisma、Drizzle、TypeORM、Supabase、PlanetScale、Mongooseなど主要なORMとデータベースアダプターを提供しています。
セキュリティ機能として、CSRF保護、JWTトークンサポート、セッション管理、リフレッシュトークンローテーション、multi-factor authentication (MFA)、Passkey/WebAuthnサポートなど、現代的なセキュリティ要件を満たしています。また、TypeScript完全サポート、サーバーサイドレンダリング(SSR)対応、エッジランタイムサポートなど、最新のWeb開発トレンドにも対応しています。
Auth.js v5では、よりモジュラーな設計となり、カスタマイゼーションが容易になりました。新しいUnified APIにより、異なるフレームワーク間でのコード共有が可能になり、開発体験が大幅に向上しています。また、WebAuth/Passkey認証の正式サポート、改良されたエラーハンドリング、パフォーマンスの最適化なども含まれています。
メリット・デメリット
メリット
- 包括的なプロバイダーサポート: 50以上のOAuthプロバイダーを内蔵サポート
- フレームワーク統合: Next.js App Router完全対応、他フレームワークもサポート
- 簡単なセットアップ: 最小限の設定で認証システムを構築可能
- セキュリティファースト: CSRF保護、JWT、セッション管理などを標準搭載
- TypeScript完全サポート: 型安全で優れた開発体験
- データベース柔軟性: 主要なORMとデータベースアダプターを提供
- 現代的機能: WebAuthn/Passkey、MFA、エッジランタイム対応
- 活発なコミュニティ: 豊富なドキュメントとコミュニティサポート
デメリット
- Next.js依存: 他フレームワークでは一部機能が制限される場合がある
- 設定の複雑さ: 高度なカスタマイゼーションには学習コストが必要
- バンドルサイズ: 包括的な機能のためバンドルサイズが大きくなる可能性
- アップデート影響: v4からv5への移行で破壊的変更がある
- プロバイダー依存: 外部サービスに依存する機能が多い
- パフォーマンス: 多機能ゆえに軽量ライブラリと比較してパフォーマンスが劣る場合
- カスタマイゼーション制限: 高度なカスタム認証フローでは制限がある場合
主要リンク
書き方の例
基本設定(App Router - v5)
// app/auth.ts
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
],
pages: {
signIn: '/auth/signin',
error: '/auth/error',
},
callbacks: {
authorized: async ({ auth }) => {
// ログインしているかチェック
return !!auth
},
},
})
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth" // 上記のauth.tsを参照
export const { GET, POST } = handlers
ミドルウェア設定
// middleware.ts
import { auth } from "@/auth"
export default auth((req) => {
if (!req.auth && req.nextUrl.pathname !== '/login') {
const newUrl = new URL('/login', req.nextUrl.origin)
return Response.redirect(newUrl)
}
})
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
クライアントサイドでの使用
// app/components/SignInButton.tsx
'use client'
import { signIn, signOut, useSession } from "next-auth/react"
export function SignInButton() {
const { data: session, status } = useSession()
if (status === "loading") return <p>読み込み中...</p>
if (session) {
return (
<div>
<p>サインイン中: {session.user?.email}</p>
<button onClick={() => signOut()}>サインアウト</button>
</div>
)
}
return (
<div>
<button onClick={() => signIn()}>サインイン</button>
<button onClick={() => signIn('google')}>Googleでサインイン</button>
<button onClick={() => signIn('github')}>GitHubでサインイン</button>
</div>
)
}
セッションプロバイダーの設定
// app/providers.tsx
'use client'
import { SessionProvider } from "next-auth/react"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
{children}
</SessionProvider>
)
}
// app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
)
}
サーバーサイドでのセッション取得
// app/profile/page.tsx
import { auth } from "@/auth"
import { redirect } from "next/navigation"
export default async function ProfilePage() {
const session = await auth()
if (!session) {
redirect('/api/auth/signin')
}
return (
<div>
<h1>プロフィール</h1>
<p>ようこそ、{session.user?.name}さん!</p>
<p>メールアドレス: {session.user?.email}</p>
<img src={session.user?.image} alt="プロフィール画像" />
</div>
)
}
クレデンシャル認証(カスタム認証)
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { compare } from "bcryptjs"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
credentials: {
email: { label: "メールアドレス", type: "email" },
password: { label: "パスワード", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null
}
// データベースからユーザーを検索
const user = await getUserByEmail(credentials.email)
if (!user) {
return null
}
// パスワードを検証
const isPasswordValid = await compare(credentials.password, user.password)
if (!isPasswordValid) {
return null
}
return {
id: user.id,
email: user.email,
name: user.name,
}
}
})
],
})
データベース設定(Prisma)
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
// プロバイダー設定
],
session: {
strategy: "database",
},
})
JWT設定
import NextAuth from "next-auth"
import type { JWT } from "next-auth/jwt"
export const { handlers, auth, signIn, signOut } = NextAuth({
session: {
strategy: "jwt",
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
}
return token
},
async session({ session, token }) {
if (token) {
session.user.id = token.id as string
}
return session
},
},
})
カスタムサインインページ
// app/auth/signin/page.tsx
import { signIn, getProviders } from "next-auth/react"
import { getServerSession } from "next-auth"
export default async function SignIn() {
const providers = await getProviders()
return (
<div className="signin-page">
<h1>サインイン</h1>
{Object.values(providers ?? {}).map((provider) => (
<div key={provider.name}>
<button
onClick={() => signIn(provider.id)}
className="provider-button"
>
{provider.name}でサインイン
</button>
</div>
))}
</div>
)
}
API保護
// app/api/protected/route.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
export async function GET() {
const session = await auth()
if (!session) {
return new NextResponse("認証が必要です", { status: 401 })
}
return NextResponse.json({
message: "保護されたデータ",
user: session.user,
})
}
カスタムコールバック
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
// プロバイダー設定
],
callbacks: {
async signIn({ user, account, profile }) {
// サインイン時の処理
if (account?.provider === "google") {
return profile?.email_verified && profile?.email?.endsWith("@example.com")
}
return true
},
async redirect({ url, baseUrl }) {
// リダイレクト先のカスタマイズ
if (url.startsWith("/")) return `${baseUrl}${url}`
else if (new URL(url).origin === baseUrl) return url
return baseUrl
},
async session({ session, token }) {
// セッション情報のカスタマイズ
session.user.id = token.sub!
return session
},
async jwt({ token, user, account }) {
// JWT トークンのカスタマイズ
if (user) {
token.id = user.id
}
return token
},
},
})
メール認証の設定
import NextAuth from "next-auth"
import EmailProvider from "next-auth/providers/email"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
EmailProvider({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
},
},
from: process.env.EMAIL_FROM,
}),
],
})
カスタムエラーハンドリング
// app/auth/error/page.tsx
'use client'
import { useSearchParams } from 'next/navigation'
export default function AuthError() {
const searchParams = useSearchParams()
const error = searchParams.get('error')
return (
<div className="error-page">
<h1>認証エラー</h1>
{error === 'Configuration' && (
<p>サーバー設定に問題があります。</p>
)}
{error === 'AccessDenied' && (
<p>アクセスが拒否されました。</p>
)}
{error === 'Verification' && (
<p>確認トークンが期限切れまたは無効です。</p>
)}
<a href="/api/auth/signin">再度サインインする</a>
</div>
)
}