Auth.js v5(旧NextAuth)完全ガイド - 認証実装の最新ベストプラクティス


はじめに

Auth.js v5(旧NextAuth.js)は、2024年にメジャーアップデートを迎え、Next.js 14+のApp Routerに完全対応した認証ライブラリです。

従来のPages Router時代から大きく進化し、Server Actions対応エッジランタイム完全サポート型安全性の大幅改善が実現されました。

Auth.js v5の主な変化

// v4(NextAuth.js)
import NextAuth from "next-auth"
export default NextAuth({...}) // API Routes前提

// v5(Auth.js)
import NextAuth from "next-auth"
export const { handlers, auth, signIn, signOut } = NextAuth({...}) // モジュラー設計

この記事では、Auth.js v5の実践的な実装方法を、最新のベストプラクティスとともに解説します。

Auth.js v5のインストールとセットアップ

基本インストール

npm install next-auth@beta
npm install @auth/prisma-adapter  # Prismaを使う場合
npm install bcryptjs              # パスワード認証を使う場合

プロジェクト構造

app/
├── api/
│   └── auth/
│       └── [...nextauth]/
│           └── route.ts    ← Auth.jsのAPIルート
├── auth.ts                 ← 認証設定ファイル(ルート)
├── middleware.ts           ← 認証ミドルウェア
└── (protected)/            ← 保護されたルート
    └── dashboard/
        └── page.tsx

基本設定ファイル(auth.ts)

// auth.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"
import type { NextAuthConfig } from "next-auth"

export const authConfig = {
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Google({
      clientId: process.env.GOOGLE_ID!,
      clientSecret: process.env.GOOGLE_SECRET!,
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        // ユーザー認証ロジック
        const user = await getUserFromDb(credentials.email, credentials.password)
        if (!user) throw new Error("Invalid credentials")
        return user
      },
    }),
  ],
  pages: {
    signIn: "/login",
    error: "/auth/error",
  },
  callbacks: {
    async jwt({ token, user, account }) {
      if (user) {
        token.id = user.id
        token.role = user.role
      }
      return token
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        session.user.role = token.role as string
      }
      return session
    },
  },
} satisfies NextAuthConfig

export const { handlers, auth, signIn, signOut } = NextAuth(authConfig)

APIルートの設定

// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"

export const { GET, POST } = handlers

OAuth認証の実装

GitHub OAuth設定

// auth.ts
import GitHub from "next-auth/providers/github"

export const authConfig = {
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
      profile(profile) {
        return {
          id: profile.id.toString(),
          name: profile.name ?? profile.login,
          email: profile.email,
          image: profile.avatar_url,
          role: profile.email?.endsWith("@company.com") ? "admin" : "user",
        }
      },
    }),
  ],
}

Google OAuth + 追加スコープ

import Google from "next-auth/providers/google"

Google({
  clientId: process.env.GOOGLE_ID!,
  clientSecret: process.env.GOOGLE_SECRET!,
  authorization: {
    params: {
      prompt: "consent",
      access_type: "offline",
      response_type: "code",
      scope: "openid email profile https://www.googleapis.com/auth/calendar",
    },
  },
  profile(profile) {
    return {
      id: profile.sub,
      name: profile.name,
      email: profile.email,
      image: profile.picture,
      emailVerified: profile.email_verified ? new Date() : null,
    }
  },
})

Credentials認証(パスワード認証)

bcryptでのパスワード検証

// lib/auth.ts
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"
import Credentials from "next-auth/providers/credentials"

export const credentialsProvider = Credentials({
  name: "Credentials",
  credentials: {
    email: { label: "Email", type: "email" },
    password: { label: "Password", type: "password" },
  },
  async authorize(credentials) {
    if (!credentials?.email || !credentials?.password) {
      throw new Error("メールアドレスとパスワードを入力してください")
    }

    const user = await prisma.user.findUnique({
      where: { email: credentials.email },
    })

    if (!user || !user.hashedPassword) {
      throw new Error("ユーザーが見つかりません")
    }

    const isPasswordValid = await bcrypt.compare(
      credentials.password,
      user.hashedPassword
    )

    if (!isPasswordValid) {
      throw new Error("パスワードが正しくありません")
    }

    return {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
    }
  },
})

ユーザー登録API

// app/api/register/route.ts
import { NextResponse } from "next/server"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"

export async function POST(req: Request) {
  try {
    const { email, password, name } = await req.json()

    if (!email || !password) {
      return NextResponse.json(
        { error: "メールアドレスとパスワードは必須です" },
        { status: 400 }
      )
    }

    const existingUser = await prisma.user.findUnique({
      where: { email },
    })

    if (existingUser) {
      return NextResponse.json(
        { error: "このメールアドレスは既に使用されています" },
        { status: 400 }
      )
    }

    const hashedPassword = await bcrypt.hash(password, 12)

    const user = await prisma.user.create({
      data: {
        email,
        name,
        hashedPassword,
      },
    })

    return NextResponse.json(
      { message: "ユーザーが作成されました", userId: user.id },
      { status: 201 }
    )
  } catch (error) {
    console.error("Registration error:", error)
    return NextResponse.json(
      { error: "ユーザー登録に失敗しました" },
      { status: 500 }
    )
  }
}

セッション管理とミドルウェア

ミドルウェアで保護されたルート

// middleware.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isProtectedRoute = req.nextUrl.pathname.startsWith("/dashboard")

  if (isProtectedRoute && !isLoggedIn) {
    return NextResponse.redirect(new URL("/login", req.url))
  }

  return NextResponse.next()
})

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}

ロールベースアクセス制御(RBAC)

// middleware.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const user = req.auth?.user
  const pathname = req.nextUrl.pathname

  // 管理者専用ルート
  if (pathname.startsWith("/admin")) {
    if (!user || user.role !== "admin") {
      return NextResponse.redirect(new URL("/", req.url))
    }
  }

  // 認証が必要なルート
  if (pathname.startsWith("/dashboard") && !user) {
    return NextResponse.redirect(new URL("/login", req.url))
  }

  return NextResponse.next()
})

Server ActionsでのログインとSign Out

ログインAction

// app/actions/auth.ts
"use server"

import { signIn, signOut } from "@/auth"
import { AuthError } from "next-auth"

export async function authenticate(
  prevState: string | undefined,
  formData: FormData
) {
  try {
    await signIn("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: true,
      redirectTo: "/dashboard",
    })
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case "CredentialsSignin":
          return "メールアドレスまたはパスワードが正しくありません"
        default:
          return "認証エラーが発生しました"
      }
    }
    throw error
  }
}

export async function signOutAction() {
  await signOut({ redirectTo: "/" })
}

ログインフォーム

// app/login/page.tsx
"use client"

import { useFormState, useFormStatus } from "react-dom"
import { authenticate } from "@/app/actions/auth"

export default function LoginPage() {
  const [errorMessage, dispatch] = useFormState(authenticate, undefined)

  return (
    <form action={dispatch} className="space-y-4">
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input
          id="email"
          name="email"
          type="email"
          required
          className="w-full border rounded px-3 py-2"
        />
      </div>
      <div>
        <label htmlFor="password">パスワード</label>
        <input
          id="password"
          name="password"
          type="password"
          required
          className="w-full border rounded px-3 py-2"
        />
      </div>
      {errorMessage && (
        <p className="text-red-500 text-sm">{errorMessage}</p>
      )}
      <LoginButton />
    </form>
  )
}

function LoginButton() {
  const { pending } = useFormStatus()
  return (
    <button
      type="submit"
      disabled={pending}
      className="w-full bg-blue-500 text-white py-2 rounded"
    >
      {pending ? "ログイン中..." : "ログイン"}
    </button>
  )
}

Prisma Adapterでのデータベース連携

Prismaスキーマ

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  hashedPassword String?
  role          String    @default("user")
  accounts      Account[]
  sessions      Session[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}

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

Auth.jsでPrisma Adapterを使用

// auth.ts
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import NextAuth from "next-auth"

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: "jwt" }, // JWTセッション推奨
  providers: [
    // プロバイダー設定
  ],
})

TypeScript型定義の拡張

カスタムユーザー型

// types/next-auth.d.ts
import { DefaultSession } from "next-auth"

declare module "next-auth" {
  interface Session {
    user: {
      id: string
      role: string
    } & DefaultSession["user"]
  }

  interface User {
    role: string
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id: string
    role: string
  }
}

エッジランタイム対応

Auth.js v5は完全なエッジランタイム対応を実現しています。

// middleware.ts(エッジランタイムで動作)
export const runtime = "edge"

import { auth } from "@/auth"

export default auth((req) => {
  // 高速な認証チェック
  if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) {
    return Response.redirect(new URL("/login", req.url))
  }
})

まとめ

Auth.js v5は、Next.js App Routerに最適化された次世代認証ライブラリです。

主な利点

  • Server Actionsネイティブ対応 - フォーム送信が簡潔に
  • エッジランタイム完全サポート - 高速な認証チェック
  • 型安全性の向上 - TypeScriptとの親和性が大幅改善
  • モジュラー設計 - 必要な機能だけインポート可能

導入時のチェックリスト

  • 環境変数(OAuth ID/Secret, NEXTAUTH_SECRET)を設定
  • Prismaスキーマでユーザーテーブル定義
  • middleware.tsで保護ルート設定
  • カスタム型定義でTypeScript対応
  • Server Actionsでログイン/ログアウト実装

Auth.js v5を使えば、堅牢でスケーラブルな認証システムを短時間で構築できます。