Auth.js (NextAuth.js v5)

認証ライブラリAuth.jsNextAuth.jsJavaScriptTypeScriptOAuth 2.0OpenID Connectランタイム非依存ユニバーサル認証

認証ライブラリ

Auth.js (NextAuth.js v5)

概要

Auth.js(旧NextAuth.js v5)は、2025年現在最も注目されるランタイム非依存のモダン認証ライブラリです。Next.js専用だったNextAuth.jsから大幅にリニューアルされ、Next.js、SvelteKit、Solid、Express、Fastify等の多様なフレームワークで動作します。OAuth 2.0、OpenID Connect、Magic Links、Credentials認証に対応し、80以上のプロバイダー(Google、GitHub、Discord、Apple等)との事前統合を提供します。Edge Runtime最適化、TypeScript完全対応、セキュリティベストプラクティス内蔵、データベース非依存設計により、あらゆるJavaScript環境でエンタープライズグレードの認証システムを構築できます。

詳細

Auth.js v5は、ランタイム非依存アーキテクチャによりNode.js、Deno、Bun、Edge Runtime、Cloudflare Workers等の多様なJavaScript実行環境で動作します。コアAPIは統一されており、フレームワーク固有のアダプター(@auth/nextjs、@auth/sveltekit、@auth/express等)を通じて各環境に最適化された統合を提供します。JWT/JWE、セッション管理、CSRF保護、State parameter、PKCE対応が標準装備され、セキュリティ脆弱性に対する包括的な保護を実現します。データベースアダプター(Prisma、Drizzle、TypeORM等)により柔軟なデータ永続化が可能で、サーバーレス環境での最適化も完備しています。

主な特徴

  • ランタイム非依存: Node.js、Deno、Bun、Edge Runtime対応
  • フレームワーク横断: Next.js、SvelteKit、Solid、Express等で統一API
  • 豊富なプロバイダー: 80以上の認証プロバイダー事前統合
  • セキュリティファースト: CSRF、XSS、セッション固定攻撃の包括的保護
  • TypeScript完全対応: エンドツーエンドの型安全性
  • Edge最適化: Vercel Edge、Cloudflare Workers等での高速動作

メリット・デメリット

メリット

  • NextAuth.js v5からの大幅進化でフレームワーク制約から解放
  • ランタイム非依存によりあらゆるJavaScript環境で利用可能
  • 80以上のプロバイダー事前統合で迅速な認証実装
  • TypeScript完全対応による開発時の優れた型安全性
  • セキュリティベストプラクティス内蔵で安全な認証システム
  • アクティブなオープンソースコミュニティによる継続的改善

デメリット

  • NextAuth.js v4からの移行に学習コストと変更作業が必要
  • 新しいライブラリのため一部の環境で安定性に課題の可能性
  • 高度なカスタマイズには内部アーキテクチャの理解が必要
  • ドキュメントが豊富だが情報の散在により学習時間が増加
  • 複雑な認証フローでは設定の複雑化
  • エンタープライズサポートが限定的(コミュニティベース)

参考ページ

書き方の例

インストールとセットアップ

# Auth.js コアライブラリとフレームワークアダプターのインストール

# Next.js用
npm install next-auth@beta

# SvelteKit用  
npm install @auth/sveltekit

# Express用
npm install @auth/express

# Solid用
npm install @auth/solid-start

# データベースアダプター(例:Prisma)
npm install @auth/prisma-adapter prisma

Next.js App Router 設定

// auth.ts - Auth.js設定
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import Discord from "next-auth/providers/discord"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID!,
      clientSecret: process.env.AUTH_GITHUB_SECRET!,
    }),
    Discord({
      clientId: process.env.AUTH_DISCORD_ID!,
      clientSecret: process.env.AUTH_DISCORD_SECRET!,
    }),
  ],
  session: {
    strategy: "database",
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60,   // 24 hours
  },
  callbacks: {
    authorized: async ({ auth }) => {
      // ログインが必要なページの保護
      return !!auth
    },
    session: async ({ session, user }) => {
      // セッションにカスタムデータを追加
      session.user.id = user.id
      session.user.role = user.role || "user"
      return session
    },
    jwt: async ({ token, user, account, profile }) => {
      // JWT内にカスタムクレームを追加
      if (user) {
        token.role = user.role || "user"
      }
      return token
    },
  },
  pages: {
    signIn: "/auth/signin",
    signOut: "/auth/signout",
    error: "/auth/error",
    verifyRequest: "/auth/verify-request",
  },
  events: {
    async signIn(message) {
      console.log("User signed in:", message.user.email)
    },
    async signOut(message) {
      console.log("User signed out:", message.token?.email)
    },
  },
  debug: process.env.NODE_ENV === "development",
})

// middleware.ts - Next.js Middleware
import { auth } from "@/auth"

export default auth((req) => {
  // 保護されたルートのチェック
  if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) {
    const newUrl = new URL("/auth/signin", req.nextUrl.origin)
    return Response.redirect(newUrl)
  }
})

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
// app/api/auth/[...nextauth]/route.ts - API Route ハンドラー
import { handlers } from "@/auth"

export const { GET, POST } = handlers
# .env.local - 環境変数設定
AUTH_SECRET="your-auth-secret" # openssl rand -base64 33 で生成
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
AUTH_GITHUB_ID="your-github-client-id"
AUTH_GITHUB_SECRET="your-github-client-secret"
AUTH_DISCORD_ID="your-discord-client-id"
AUTH_DISCORD_SECRET="your-discord-client-secret"
DATABASE_URL="postgresql://username:password@localhost:5432/authjs"

React コンポーネントでの認証利用

// components/SignInButton.tsx - サインインボタン
import { signIn, signOut } from "@/auth"
import { auth } from "@/auth"

export default async function SignInButton() {
  const session = await auth()

  if (session?.user) {
    return (
      <div className="flex items-center space-x-4">
        <img
          src={session.user.image || "/default-avatar.png"}
          alt="Profile"
          className="w-8 h-8 rounded-full"
        />
        <span>こんにちは、{session.user.name}さん</span>
        <form
          action={async () => {
            "use server"
            await signOut()
          }}
        >
          <button
            type="submit"
            className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
          >
            サインアウト
          </button>
        </form>
      </div>
    )
  }

  return (
    <div className="space-x-2">
      <form
        action={async () => {
          "use server"
          await signIn("google")
        }}
      >
        <button
          type="submit"
          className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
        >
          Googleでサインイン
        </button>
      </form>
      
      <form
        action={async () => {
          "use server"
          await signIn("github")
        }}
      >
        <button
          type="submit"
          className="bg-gray-800 hover:bg-gray-900 text-white px-4 py-2 rounded"
        >
          GitHubでサインイン
        </button>
      </form>
    </div>
  )
}

// components/UserProfile.tsx - ユーザープロファイル表示
import { auth } from "@/auth"
import { redirect } from "next/navigation"

export default async function UserProfile() {
  const session = await auth()

  if (!session?.user) {
    redirect("/auth/signin")
  }

  return (
    <div className="max-w-md mx-auto bg-white shadow-lg rounded-lg p-6">
      <div className="text-center">
        <img
          src={session.user.image || "/default-avatar.png"}
          alt="Profile"
          className="w-24 h-24 rounded-full mx-auto mb-4"
        />
        <h1 className="text-2xl font-bold mb-2">{session.user.name}</h1>
        <p className="text-gray-600 mb-4">{session.user.email}</p>
        
        <div className="bg-gray-50 p-4 rounded-lg text-left">
          <h2 className="font-semibold mb-2">セッション情報</h2>
          <div className="space-y-2 text-sm">
            <div>
              <span className="font-medium">ユーザーID:</span>
              <span className="ml-2">{session.user.id}</span>
            </div>
            <div>
              <span className="font-medium">ロール:</span>
              <span className="ml-2">{session.user.role}</span>
            </div>
            <div>
              <span className="font-medium">セッション期限:</span>
              <span className="ml-2">
                {new Date(session.expires).toLocaleDateString('ja-JP')}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

SvelteKit 統合

// src/hooks.server.ts - SvelteKit フック
import { SvelteKitAuth } from "@auth/sveltekit"
import Google from "@auth/sveltekit/providers/google"
import GitHub from "@auth/sveltekit/providers/github"
import { GOOGLE_ID, GOOGLE_SECRET, GITHUB_ID, GITHUB_SECRET, AUTH_SECRET } from "$env/static/private"

export const { handle, signIn, signOut } = SvelteKitAuth({
  providers: [
    Google({ 
      clientId: GOOGLE_ID, 
      clientSecret: GOOGLE_SECRET 
    }),
    GitHub({ 
      clientId: GITHUB_ID, 
      clientSecret: GITHUB_SECRET 
    }),
  ],
  secret: AUTH_SECRET,
  trustHost: true,
})

// src/app.d.ts - 型定義
import type { DefaultSession } from "@auth/sveltekit"

declare module "@auth/sveltekit" {
  interface Session {
    user: {
      id: string
      role?: string
    } & DefaultSession["user"]
  }
}

// src/routes/+layout.server.ts - レイアウトサーバー
import type { LayoutServerLoad } from "./$types"

export const load: LayoutServerLoad = async (event) => {
  return {
    session: await event.locals.auth(),
  }
}

// src/routes/+layout.svelte - レイアウトコンポーネント
<script lang="ts">
  import { page } from "$app/stores"
  import { signIn, signOut } from "@auth/sveltekit/client"
  
  export let data
  
  $: session = data.session
</script>

<header class="bg-white shadow">
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
    <div class="flex justify-between h-16 items-center">
      <h1 class="text-xl font-semibold">My SvelteKit App</h1>
      
      {#if session?.user}
        <div class="flex items-center space-x-4">
          <img
            src={session.user.image || "/default-avatar.png"}
            alt="Profile"
            class="w-8 h-8 rounded-full"
          />
          <span>こんにちは、{session.user.name}さん</span>
          <button
            on:click={() => signOut()}
            class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
          >
            サインアウト
          </button>
        </div>
      {:else}
        <div class="space-x-2">
          <button
            on:click={() => signIn("google")}
            class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
          >
            Googleでサインイン
          </button>
          <button
            on:click={() => signIn("github")}
            class="bg-gray-800 hover:bg-gray-900 text-white px-4 py-2 rounded"
          >
            GitHubでサインイン
          </button>
        </div>
      {/if}
    </div>
  </div>
</header>

<main>
  <slot />
</main>

Express.js 統合

// server.js - Express.js アプリケーション
import express from "express"
import { ExpressAuth } from "@auth/express"
import Google from "@auth/express/providers/google"
import GitHub from "@auth/express/providers/github"

const app = express()

// Auth.js middleware
app.use("/auth/*", ExpressAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
  ],
  secret: process.env.AUTH_SECRET,
  trustHost: true,
}))

// 認証状態確認ミドルウェア
const requireAuth = async (req, res, next) => {
  const session = await getSession(req, res)
  if (!session?.user) {
    return res.status(401).json({ error: "Authentication required" })
  }
  req.user = session.user
  next()
}

// 保護されたルート
app.get("/api/profile", requireAuth, (req, res) => {
  res.json({
    message: "Authenticated user profile",
    user: req.user,
  })
})

// 公開ルート
app.get("/", (req, res) => {
  res.send(`
    <html>
      <body>
        <h1>Express + Auth.js</h1>
        <a href="/auth/signin">Sign In</a>
      </body>
    </html>
  `)
})

const port = process.env.PORT || 3000
app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`)
})

カスタムプロバイダーとアダプター

// lib/custom-provider.ts - カスタム認証プロバイダー
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers"

interface CustomProfile {
  id: string
  email: string
  name: string
  avatar_url: string
}

export default function CustomProvider<P extends CustomProfile>(
  options: OAuthUserConfig<P>
): OAuthConfig<P> {
  return {
    id: "custom-provider",
    name: "Custom Provider",
    type: "oauth",
    authorization: {
      url: "https://api.custom-provider.com/oauth/authorize",
      params: {
        scope: "user:email user:profile",
        response_type: "code",
      },
    },
    token: "https://api.custom-provider.com/oauth/token",
    userinfo: "https://api.custom-provider.com/user",
    profile(profile) {
      return {
        id: profile.id,
        name: profile.name,
        email: profile.email,
        image: profile.avatar_url,
      }
    },
    ...options,
  }
}

// lib/custom-adapter.ts - カスタムデータベースアダプター
import type { Adapter } from "next-auth/adapters"

export function CustomDatabaseAdapter(client: any): Adapter {
  return {
    async createUser(user) {
      const result = await client.user.create({
        data: {
          name: user.name,
          email: user.email,
          image: user.image,
          emailVerified: user.emailVerified,
        },
      })
      return result
    },
    
    async getUser(id) {
      const user = await client.user.findUnique({
        where: { id },
      })
      return user
    },
    
    async getUserByEmail(email) {
      const user = await client.user.findUnique({
        where: { email },
      })
      return user
    },
    
    async getUserByAccount({ providerAccountId, provider }) {
      const account = await client.account.findUnique({
        where: {
          provider_providerAccountId: {
            provider,
            providerAccountId,
          },
        },
        include: { user: true },
      })
      return account?.user ?? null
    },
    
    async updateUser({ id, ...user }) {
      const result = await client.user.update({
        where: { id },
        data: user,
      })
      return result
    },
    
    async deleteUser(userId) {
      await client.user.delete({
        where: { id: userId },
      })
    },
    
    async linkAccount(account) {
      const result = await client.account.create({
        data: {
          userId: account.userId,
          provider: account.provider,
          type: account.type,
          providerAccountId: account.providerAccountId,
          access_token: account.access_token,
          expires_at: account.expires_at,
          id_token: account.id_token,
          refresh_token: account.refresh_token,
          scope: account.scope,
          session_state: account.session_state,
          token_type: account.token_type,
        },
      })
      return result
    },
    
    async unlinkAccount({ providerAccountId, provider }) {
      await client.account.delete({
        where: {
          provider_providerAccountId: {
            provider,
            providerAccountId,
          },
        },
      })
    },
    
    async createSession({ sessionToken, userId, expires }) {
      const result = await client.session.create({
        data: {
          sessionToken,
          userId,
          expires,
        },
      })
      return result
    },
    
    async getSessionAndUser(sessionToken) {
      const userAndSession = await client.session.findUnique({
        where: { sessionToken },
        include: { user: true },
      })
      
      if (!userAndSession) return null
      
      const { user, ...session } = userAndSession
      return { user, session }
    },
    
    async updateSession({ sessionToken, ...session }) {
      const result = await client.session.update({
        where: { sessionToken },
        data: session,
      })
      return result
    },
    
    async deleteSession(sessionToken) {
      await client.session.delete({
        where: { sessionToken },
      })
    },
  }
}