Lucia
認証ライブラリ
Lucia
概要
Luciaは、TypeScriptとJavaScriptのためのオープンソース認証ライブラリで、ユーザーとセッションの管理の複雑さを抽象化し、シンプルで柔軟な認証システムを提供します。
詳細
Lucia(ルシア)は、TypeScript/JavaScript向けの軽量で型安全な認証ライブラリです。2024年の重要な変更として、Lucia v3は2025年3月に非推奨となり、Luciaは認証を一から実装するための学習リソースへと移行しています。これにより、開発者は様々なランタイムやデータベースをサポートする複雑さを取り除いた、非常に短くシンプルなコードでLucia v3と同等の機能を再現できます。
フレームワーク非依存の設計により、Astro、Express、Next.js、Nuxt、SolidStart、SvelteKit、その他多くのフレームワークで使用可能です。Node.js、Deno、Bun、Cloudflare Workersを含む全ての主要なJSランタイムに対応しています。データベースの柔軟性も特徴の一つで、様々なデータベース、ライブラリ、フレームワーク、ORMをサポートし、パフォーマンスを犠牲にすることなく好みのデータベースを選択できます。
認証方法においては、OAuthとメール/パスワード認証に加え、パスワードレス認証システムも提供しています。セキュリティ面では、最新のセキュリティ標準に準拠し、不要なサードパーティへのデータ露出からユーザーデータを保護します。開発者はセッションの作成、検証、破棄を自分で行うことができ、これによりセッションハイジャックのリスクを最小化できます。
従来のサードパーティ認証ライブラリ(Clerk、NextAuth、Supabase auth)と異なり、Luciaはより低レベルでシンプルなアプローチを採用し、認証に関する完全な制御を開発者に与えます。Auth.jsと比較して、はるかに低レベルでシンプルでありながら、完全な制御を提供するのが特徴です。
メリット・デメリット
メリット
- 完全な制御権: 認証システムを完全にコントロール可能
- 軽量で柔軟: 最小限の依存関係で高いカスタマイズ性
- フレームワーク非依存: 主要なJSフレームワークとランタイムに対応
- データベース選択の自由: 任意のデータベースやORMを使用可能
- 型安全性: TypeScript完全対応で優れた型推論
- セキュリティ重視: 最新のセキュリティ標準に準拠
- シンプルなAPI: 理解しやすく拡張しやすいAPI設計
- セッション管理: 高度なセッション管理機能を内蔵
デメリット
- 学習曲線: 低レベルAPIのため初心者には複雑
- 廃止予定: v3は2025年3月に非推奨となる予定
- 手動実装: より多くのコードを手動で書く必要がある
- コミュニティ: 他の認証ライブラリと比較してコミュニティが小さい
- ドキュメント: 移行期のため一部ドキュメントが古い可能性
- 保守負担: 自前実装のため保守責任が開発者にある
主要リンク
書き方の例
セッション管理の基本実装
// セッション管理の基本関数
function generateSessionId(): string {
const bytes = new Uint8Array(25);
crypto.getRandomValues(bytes);
const token = encodeBase32LowerCaseNoPadding(bytes);
return token;
}
const sessionExpiresInSeconds = 60 * 60 * 24 * 30; // 30日
export function createSession(dbPool: DBPool, userId: number): Promise<Session> {
const now = new Date();
const sessionId = generateSessionId();
const session: Session = {
id: sessionId,
userId,
expiresAt: new Date(now.getTime() + 1000 * sessionExpiresInSeconds)
};
await executeQuery(
dbPool,
"INSERT INTO user_session (id, user_id, expires_at) VALUES (?, ?, ?)",
[session.id, session.userId, Math.floor(session.expiresAt.getTime() / 1000)]
);
return session;
}
export interface Session {
id: string;
userId: number;
expiresAt: Date;
}
セッション検証
export function validateSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {
const now = Date.now();
const result = dbPool.executeQuery(
dbPool,
"SELECT id, user_id, expires_at FROM session WHERE id = ?",
[sessionId]
);
if (result.rows.length < 1) {
return null;
}
const row = result.rows[0];
const session: Session = {
id: row[0],
userId: row[1],
expiresAt: new Date(row[2] * 1000)
};
// 期限切れチェック
if (now.getTime() >= session.expiresAt.getTime()) {
await executeQuery(dbPool, "DELETE FROM user_session WHERE id = ?", [session.id]);
return null;
}
// セッション更新(半分経過時)
if (now.getTime() >= session.expiresAt.getTime() - (1000 * sessionExpiresInSeconds) / 2) {
session.expiresAt = new Date(Date.now() + 1000 * sessionExpiresInSeconds);
await executeQuery(dbPool, "UPDATE session SET expires_at = ? WHERE id = ?", [
Math.floor(session.expiresAt.getTime() / 1000),
session.id
]);
}
return session;
}
Cookieベースのセッション管理
export function setSessionCookie(response: HTTPResponse, sessionId: string, expiresAt: Date): void {
if (env === ENV.PROD) {
response.headers.add(
"Set-Cookie",
`session=${sessionId}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure;`
);
} else {
response.headers.add(
"Set-Cookie",
`session=${sessionId}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/`
);
}
}
export function deleteSessionCookie(response: HTTPResponse): void {
if (env === ENV.PROD) {
response.headers.add(
"Set-Cookie",
"session=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure;"
);
} else {
response.headers.add("Set-Cookie", "session=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/");
}
}
Next.jsでの実装例
import { cookies } from "next/headers";
import { cache } from "react";
export const getCurrentSession = cache(async (): Promise<SessionResult> => {
const cookieStore = await cookies();
const token = cookieStore.get("session")?.value ?? null;
if (token === null) {
return { session: null, user: null };
}
const result = await validateSessionToken(token);
return result;
});
// ページコンポーネントでの使用
export default async function Page() {
const { user } = await getCurrentSession();
if (user === null) {
return redirect("/login");
}
return <h1>こんにちは、{user.name}さん!</h1>;
}
OAuth実装(GitHub連携)
// GitHub OAuth callback処理
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const cookieStore = await cookies();
const storedState = cookieStore.get("github_oauth_state")?.value ?? null;
if (code === null || state === null || storedState === null) {
return new Response(null, { status: 400 });
}
if (state !== storedState) {
return new Response(null, { status: 400 });
}
let tokens: OAuth2Tokens;
try {
tokens = await github.validateAuthorizationCode(code);
} catch (e) {
return new Response(null, { status: 400 });
}
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: { Authorization: `Bearer ${tokens.accessToken()}` }
});
const githubUser = await githubUserResponse.json();
const githubUserId = githubUser.id;
const githubUsername = githubUser.login;
const existingUser = await getUserFromGitHubId(githubUserId);
if (existingUser !== null) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: { Location: "/" }
});
}
const user = await createUser(githubUserId, githubUsername);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: { Location: "/" }
});
}
セッション無効化(サインアウト)
export async function invalidateSession(dbPool: DBPool, sessionId: string): Promise<void> {
await executeQuery(dbPool, "DELETE FROM user_session WHERE id = ?", [sessionId]);
}
export async function invalidateAllSessions(dbPool: DBPool, userId: number): Promise<void> {
await executeQuery(dbPool, "DELETE FROM user_session WHERE user_id = ?", [userId]);
}
// サインアウト処理
async function logout(): Promise<ActionResult> {
"use server";
const { session } = await getCurrentSession();
if (!session) {
return { error: "Unauthorized" };
}
await invalidateSession(session.id);
await deleteSessionTokenCookie();
return redirect("/login");
}
定数時間比較(セキュリティ)
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.byteLength !== b.byteLength) {
return false;
}
let c = 0;
for (let i = 0; i < a.byteLength; i++) {
c |= a[i] ^ b[i];
}
return c === 0;
}