Auth.js (NextAuth.js v5)
認証ライブラリ
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 },
})
},
}
}