Lucia

TypeScriptAuthentication LibrarySession ManagementFramework AgnosticSecurityJavaScript

Authentication Library

Lucia

Overview

Lucia is an open-source authentication library for TypeScript and JavaScript that abstracts away the complexity of handling users and sessions, providing a simple and flexible authentication system.

Details

Lucia is a lightweight, type-safe authentication library for TypeScript/JavaScript. As of 2024, there's been a significant change: Lucia v3 will be deprecated by March 2025, and Lucia is transitioning to become a learning resource for implementing authentication from scratch. This allows developers to recreate Lucia v3 functionality with very short and simple code, without the complexity that came with supporting various runtimes and databases.

The framework-agnostic design makes it compatible with Astro, Express, Next.js, Nuxt, SolidStart, SvelteKit, and many other frameworks. It works with all major JavaScript runtimes including Node.js, Deno, Bun, and Cloudflare Workers. Database flexibility is another key feature, supporting various databases, libraries, frameworks, and ORMs without compromising performance.

For authentication methods, it supports OAuth and email/password authentication, plus passwordless authentication systems. From a security perspective, it adheres to the latest security standards and protects user data from unnecessary third-party exposure. Developers can create, validate, and destroy sessions themselves, naturally minimizing the risk of session hijacking.

Unlike traditional third-party authentication libraries (Clerk, NextAuth, Supabase auth), Lucia takes a different approach. Unlike Auth.js, it's much more low-level and simple, giving developers full control over authentication. The library has gained significant community adoption in 2024, with developers appreciating its flexibility and control over traditional authentication providers.

Pros and Cons

Pros

  • Complete Control: Full control over the authentication system
  • Lightweight and Flexible: Minimal dependencies with high customizability
  • Framework Agnostic: Supports major JS frameworks and runtimes
  • Database Freedom: Use any database or ORM of choice
  • Type Safety: Full TypeScript support with excellent type inference
  • Security Focused: Adheres to latest security standards
  • Simple API: Easy to understand and extend API design
  • Session Management: Built-in advanced session management features

Cons

  • Learning Curve: Complex for beginners due to low-level API
  • Deprecation Notice: v3 scheduled for deprecation in March 2025
  • Manual Implementation: Requires writing more code manually
  • Community Size: Smaller community compared to other auth libraries
  • Documentation: Some documentation may be outdated due to transition
  • Maintenance Burden: Developers responsible for maintenance of custom implementation

Key Links

Code Examples

Basic Session Management Implementation

// Basic session management functions
function generateSessionId(): string {
  const bytes = new Uint8Array(25);
  crypto.getRandomValues(bytes);
  const token = encodeBase32LowerCaseNoPadding(bytes);
  return token;
}

const sessionExpiresInSeconds = 60 * 60 * 24 * 30; // 30 days

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;
}

Session Validation

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)
  };
  
  // Expiration check
  if (now.getTime() >= session.expiresAt.getTime()) {
    await executeQuery(dbPool, "DELETE FROM user_session WHERE id = ?", [session.id]);
    return null;
  }
  
  // Session renewal (when half time has passed)
  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-based Session Management

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 Implementation Example

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

// Usage in page component
export default async function Page() {
  const { user } = await getCurrentSession();
  
  if (user === null) {
    return redirect("/login");
  }
  
  return <h1>Hello, {user.name}!</h1>;
}

OAuth Implementation (GitHub Integration)

// GitHub OAuth callback handler
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: "/" }
  });
}

Session Invalidation (Sign Out)

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

// Sign out handler
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");
}

Constant-Time Comparison (Security)

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;
}