NextAuth.js

Next.jsReact認証ライブラリOAuthJWTセッション管理フルスタックTypeScript

認証ライブラリ

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>
  )
}