Lucia Auth

認証ライブラリLucia AuthJavaScriptTypeScriptセッション管理軽量認証データベース統合フレームワーク非依存

認証ライブラリ

Lucia Auth

概要

Lucia Authは、2025年現在注目を集めている軽量でオープンソースの認証ライブラリです。セッションベースの認証に特化し、シンプルでありながら柔軟性の高い設計が特徴です。TypeScript完全対応、データベース非依存、フレームワーク中立的なアーキテクチャにより、Next.js、SvelteKit、Astro、Solid、Nuxt等の多様なフレームワークで利用可能です。複雑な設定を排除し、開発者が認証ロジックをフルコントロールできるミニマルなAPIを提供しながら、セキュリティベストプラクティスを内蔵しています。

詳細

Lucia Authは「認証は複雑であってはならない」という哲学の下に設計された次世代認証ライブラリです。パスワードハッシュ化、セッション管理、CSRF保護、セキュアクッキー生成等の認証基盤をコアとして提供し、データベースアダプター(Prisma、Drizzle、MongoDB、Firebase等)により柔軟なデータ永続化を実現します。従来のライブラリと異なり、認証フローやUI要素を押し付けない設計により、開発者が要件に応じて認証システムを自由にカスタマイズできます。セッションベースの認証により、JWT管理の複雑さを回避し、より直感的で安全な認証体験を提供します。

主な特徴

  • 軽量設計: 最小限のバンドルサイズで高性能な認証機能
  • フレームワーク中立: React、Vue、Svelte等で統一API使用可能
  • データベース非依存: 豊富なデータベースアダプター対応
  • TypeScript完全対応: エンドツーエンドの型安全性
  • セッションベース: JWT不要のシンプルなセッション管理
  • 開発者フレンドリー: 最小限の設定で迅速な導入可能

メリット・デメリット

メリット

  • シンプルで理解しやすいAPIによる学習コストの低減
  • フレームワーク中立で多様な環境での再利用可能性
  • 軽量設計によるバンドルサイズの最小化と高速動作
  • TypeScript完全対応による開発時の優れた型安全性
  • 豊富なデータベースアダプターによる柔軟な統合
  • 認証フローの完全制御によるカスタマイズ性の高さ

デメリット

  • 比較的新しいライブラリのため実績とコミュニティが限定的
  • 高度な機能(SSO、MFA等)は自前実装が必要
  • ドキュメントが発展途上で学習リソースが少ない
  • エンタープライズサポートやSLAが提供されていない
  • OAuth2.0/OpenID Connectサポートが限定的
  • 大規模プロジェクトでの長期サポートに不安

参考ページ

書き方の例

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

# Lucia Authのインストール
npm install lucia

# データベースアダプターのインストール(例:Prisma)
npm install @lucia-auth/adapter-prisma prisma
npm install bcrypt
npm install @types/bcrypt # TypeScript使用時

# その他のアダプター例
npm install @lucia-auth/adapter-drizzle  # Drizzle ORM
npm install @lucia-auth/adapter-mongodb  # MongoDB
npm install @lucia-auth/adapter-sqlite   # SQLite

Prismaとの基本設定

// lib/lucia.ts - Lucia Auth設定
import { lucia } from "lucia";
import { prisma } from "@lucia-auth/adapter-prisma";
import { PrismaClient } from "@prisma/client";
import { webcrypto } from "crypto";

// Node.js環境でのWebCrypto polyfill
globalThis.crypto = webcrypto as Crypto;

const client = new PrismaClient();

export const auth = lucia({
	adapter: prisma(client, {
		user: "user", // Prismaモデル名
		key: "key",   // Prismaモデル名
		session: "session" // Prismaモデル名
	}),
	env: process.env.NODE_ENV === "development" ? "DEV" : "PROD",
	middleware: "node", // Next.js使用時
	sessionCookie: {
		expires: false // セッション永続化
	},
	getUserAttributes: (data) => {
		return {
			// ユーザー属性をセッションに含める
			email: data.email,
			name: data.name,
			role: data.role
		};
	}
});

export type Auth = typeof auth;
// prisma/schema.prisma - Prismaスキーマ定義
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql" // MySQL, SQLiteも使用可能
  url      = env("DATABASE_URL")
}

model User {
  id       String    @id @unique
  email    String    @unique
  name     String?
  role     String    @default("user")
  
  auth_session Session[]
  key          Key[]
  
  @@map("users")
}

model Session {
  id             String @id @unique
  user_id        String
  active_expires BigInt
  idle_expires   BigInt
  user           User   @relation(references: [id], fields: [user_id], onDelete: Cascade)

  @@index([user_id])
  @@map("sessions")
}

model Key {
  id              String  @id @unique
  hashed_password String?
  user_id         String
  user            User    @relation(references: [id], fields: [user_id], onDelete: Cascade)

  @@index([user_id])
  @@map("keys")
}

ユーザー登録機能

// lib/auth.ts - 認証ヘルパー関数
import { auth } from "./lucia";
import { generateId } from "lucia";
import bcrypt from "bcrypt";

export const createUser = async (email: string, password: string, name?: string) => {
	try {
		// パスワードハッシュ化
		const hashedPassword = await bcrypt.hash(password, 12);
		
		// ユーザー作成
		const userId = generateId(15);
		const user = await auth.createUser({
			userId,
			key: {
				providerId: "email", // プロバイダーID
				providerUserId: email.toLowerCase(), // プロバイダーユーザーID
				password: hashedPassword
			},
			attributes: {
				email: email.toLowerCase(),
				name: name || "",
				role: "user"
			}
		});
		
		return user;
	} catch (error) {
		throw new Error("ユーザー作成に失敗しました");
	}
};

export const signInUser = async (email: string, password: string) => {
	try {
		// ユーザー認証
		const key = await auth.useKey("email", email.toLowerCase(), password);
		const session = await auth.createSession({
			userId: key.userId,
			attributes: {}
		});
		
		return session;
	} catch (error) {
		throw new Error("認証に失敗しました");
	}
};

export const signOutUser = async (sessionId: string) => {
	try {
		await auth.invalidateSession(sessionId);
	} catch (error) {
		throw new Error("ログアウトに失敗しました");
	}
};

Next.js App Router統合

// app/api/auth/signup/route.ts - ユーザー登録API
import { NextRequest, NextResponse } from "next/server";
import { createUser } from "@/lib/auth";
import { auth } from "@/lib/lucia";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
	try {
		const { email, password, name } = await request.json();
		
		// バリデーション
		if (!email || !password) {
			return NextResponse.json(
				{ error: "メールアドレスとパスワードは必須です" },
				{ status: 400 }
			);
		}
		
		if (password.length < 8) {
			return NextResponse.json(
				{ error: "パスワードは8文字以上である必要があります" },
				{ status: 400 }
			);
		}
		
		// ユーザー作成
		const user = await createUser(email, password, name);
		
		// セッション作成
		const session = await auth.createSession({
			userId: user.userId,
			attributes: {}
		});
		
		// セッションクッキー設定
		const sessionCookie = auth.createSessionCookie(session);
		cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
		
		return NextResponse.json({
			success: true,
			user: {
				id: user.userId,
				email: user.email,
				name: user.name
			}
		});
	} catch (error) {
		return NextResponse.json(
			{ error: "アカウント作成に失敗しました" },
			{ status: 500 }
		);
	}
}
// app/api/auth/signin/route.ts - ログインAPI
import { NextRequest, NextResponse } from "next/server";
import { signInUser } from "@/lib/auth";
import { auth } from "@/lib/lucia";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
	try {
		const { email, password } = await request.json();
		
		if (!email || !password) {
			return NextResponse.json(
				{ error: "メールアドレスとパスワードは必須です" },
				{ status: 400 }
			);
		}
		
		// ユーザー認証
		const session = await signInUser(email, password);
		
		// セッションクッキー設定
		const sessionCookie = auth.createSessionCookie(session);
		cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
		
		return NextResponse.json({
			success: true,
			session: {
				id: session.sessionId,
				userId: session.user.userId
			}
		});
	} catch (error) {
		return NextResponse.json(
			{ error: "認証に失敗しました" },
			{ status: 401 }
		);
	}
}
// app/api/auth/signout/route.ts - ログアウトAPI
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/lucia";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
	try {
		const sessionId = cookies().get(auth.sessionCookieName)?.value ?? null;
		
		if (!sessionId) {
			return NextResponse.json({ error: "セッションが見つかりません" }, { status: 401 });
		}
		
		// セッション無効化
		await auth.invalidateSession(sessionId);
		
		// クッキー削除
		const sessionCookie = auth.createBlankSessionCookie();
		cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
		
		return NextResponse.json({ success: true });
	} catch (error) {
		return NextResponse.json({ error: "ログアウトに失敗しました" }, { status: 500 });
	}
}

ミドルウェアによる認証保護

// middleware.ts - Next.js Middleware
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/lucia";

export async function middleware(request: NextRequest) {
	const sessionId = request.cookies.get(auth.sessionCookieName)?.value ?? null;
	
	// 保護されたルートの定義
	const protectedPaths = ["/dashboard", "/profile", "/admin"];
	const isProtectedRoute = protectedPaths.some(path => 
		request.nextUrl.pathname.startsWith(path)
	);
	
	if (isProtectedRoute) {
		if (!sessionId) {
			// 未認証の場合ログインページにリダイレクト
			return NextResponse.redirect(new URL("/login", request.url));
		}
		
		try {
			// セッション検証
			const { session, user } = await auth.validateSession(sessionId);
			
			if (!session) {
				// 無効なセッションの場合ログインページにリダイレクト
				return NextResponse.redirect(new URL("/login", request.url));
			}
			
			// 有効なセッションの場合、ユーザー情報をヘッダーに追加
			const response = NextResponse.next();
			response.headers.set("x-user-id", user.userId);
			response.headers.set("x-user-email", user.email);
			
			return response;
		} catch (error) {
			return NextResponse.redirect(new URL("/login", request.url));
		}
	}
	
	return NextResponse.next();
}

export const config = {
	matcher: ["/dashboard/:path*", "/profile/:path*", "/admin/:path*"]
};

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

// components/LoginForm.tsx - ログインフォーム
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function LoginForm() {
	const [email, setEmail] = useState("");
	const [password, setPassword] = useState("");
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState("");
	const router = useRouter();
	
	const handleSubmit = async (e: React.FormEvent) => {
		e.preventDefault();
		setLoading(true);
		setError("");
		
		try {
			const response = await fetch("/api/auth/signin", {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				body: JSON.stringify({ email, password }),
			});
			
			const data = await response.json();
			
			if (response.ok) {
				router.push("/dashboard");
				router.refresh();
			} else {
				setError(data.error || "ログインに失敗しました");
			}
		} catch (error) {
			setError("ネットワークエラーが発生しました");
		} finally {
			setLoading(false);
		}
	};
	
	return (
		<form onSubmit={handleSubmit} className="space-y-4">
			<div>
				<label htmlFor="email" className="block text-sm font-medium text-gray-700">
					メールアドレス
				</label>
				<input
					type="email"
					id="email"
					value={email}
					onChange={(e) => setEmail(e.target.value)}
					required
					className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
				/>
			</div>
			
			<div>
				<label htmlFor="password" className="block text-sm font-medium text-gray-700">
					パスワード
				</label>
				<input
					type="password"
					id="password"
					value={password}
					onChange={(e) => setPassword(e.target.value)}
					required
					className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
				/>
			</div>
			
			{error && (
				<div className="text-red-600 text-sm">{error}</div>
			)}
			
			<button
				type="submit"
				disabled={loading}
				className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
			>
				{loading ? "ログイン中..." : "ログイン"}
			</button>
		</form>
	);
}

サーバーコンポーネントでの認証状態取得

// app/dashboard/page.tsx - ダッシュボードページ
import { auth } from "@/lib/lucia";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
	const sessionId = cookies().get(auth.sessionCookieName)?.value ?? null;
	
	if (!sessionId) {
		redirect("/login");
	}
	
	try {
		const { session, user } = await auth.validateSession(sessionId);
		
		if (!session) {
			redirect("/login");
		}
		
		return (
			<div className="min-h-screen bg-gray-50">
				<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
					<div className="px-4 py-6 sm:px-0">
						<div className="border-4 border-dashed border-gray-200 rounded-lg p-6">
							<h1 className="text-3xl font-bold text-gray-900 mb-4">
								ダッシュボード
							</h1>
							
							<div className="bg-white shadow overflow-hidden sm:rounded-lg">
								<div className="px-4 py-5 sm:p-6">
									<h3 className="text-lg leading-6 font-medium text-gray-900">
										ユーザー情報
									</h3>
									<div className="mt-5 border-t border-gray-200">
										<dl className="divide-y divide-gray-200">
											<div className="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
												<dt className="text-sm font-medium text-gray-500">
													ユーザーID
												</dt>
												<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
													{user.userId}
												</dd>
											</div>
											<div className="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
												<dt className="text-sm font-medium text-gray-500">
													メールアドレス
												</dt>
												<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
													{user.email}
												</dd>
											</div>
											<div className="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
												<dt className="text-sm font-medium text-gray-500">
													名前
												</dt>
												<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
													{user.name || "未設定"}
												</dd>
											</div>
											<div className="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
												<dt className="text-sm font-medium text-gray-500">
													ロール
												</dt>
												<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
													{user.role}
												</dd>
											</div>
										</dl>
									</div>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		);
	} catch (error) {
		redirect("/login");
	}
}