Lucia
Build with Lucia for lightweight, self-hosted authentication. Use this skill when
You are an auth specialist who integrates Lucia into projects. Lucia is a minimal, open-source auth library focused on session management. It's not a framework — it gives you the primitives to build auth exactly how you want it, with zero vendor lock-in and no opinionated abstractions. ## Key Points - Use Argon2 for password hashing — it's the current best practice over bcrypt - Always refresh session cookies on validation (Lucia does this with `fresh` sessions) - Use `generateIdFromEntropySize()` for user IDs — cryptographically secure - Use Arctic for OAuth — it handles the OAuth flow without a heavy abstraction - Store the session cookie as httpOnly, secure, sameSite: lax - Invalidate sessions on logout — don't just clear the cookie - Using MD5/SHA for password hashing — use Argon2 or bcrypt - Not validating sessions on every request — expired sessions should be caught - Storing session data in the cookie instead of the database - Not clearing the session cookie on failed validation — user stays in limbo - Building OAuth flows from scratch when Arctic handles it - Skipping CSRF protection on auth forms ## Quick Example ```bash npm install lucia npm install arctic # For OAuth providers npm install @node-rs/argon2 # For password hashing ```
skilldb get auth-services-skills/LuciaFull skill: 321 linesLucia Authentication Integration
You are an auth specialist who integrates Lucia into projects. Lucia is a minimal, open-source auth library focused on session management. It's not a framework — it gives you the primitives to build auth exactly how you want it, with zero vendor lock-in and no opinionated abstractions.
Core Philosophy
Sessions, not magic
Lucia manages sessions — creating them, validating them, invalidating them. It doesn't dictate how users sign in. You build the login flow (password, OAuth, magic link), and Lucia handles the session lifecycle after authentication.
Zero dependencies, full control
Lucia has no external dependencies. It doesn't call third-party services, doesn't require an API key, and doesn't phone home. Your auth runs entirely in your infrastructure. You own every line of the flow.
Database-agnostic
Lucia works with any database through a simple adapter interface. Provide four functions (create session, get session, update session expiry, delete session) and Lucia handles the rest.
Setup
Install
npm install lucia
npm install arctic # For OAuth providers
npm install @node-rs/argon2 # For password hashing
Initialize
// lib/auth.ts
import { Lucia } from 'lucia';
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
import { db } from './db';
import { sessions, users } from './db/schema';
const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === 'production',
},
},
getUserAttributes: (attributes) => ({
email: attributes.email,
name: attributes.name,
}),
});
declare module 'lucia' {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: {
email: string;
name: string;
};
}
}
Database schema (Drizzle)
// db/schema.ts
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
hashedPassword: text('hashed_password'),
});
export const sessions = pgTable('sessions', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id),
expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'date' }).notNull(),
});
Key Techniques
Session validation middleware
// lib/auth.ts
import { cookies } from 'next/headers';
import type { Session, User } from 'lucia';
export async function validateRequest(): Promise<
{ user: User; session: Session } | { user: null; session: null }
> {
const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) return { user: null, session: null };
const result = await lucia.validateSession(sessionId);
try {
if (result.session?.fresh) {
const sessionCookie = lucia.createSessionCookie(result.session.id);
(await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!result.session) {
const blankCookie = lucia.createBlankSessionCookie();
(await cookies()).set(blankCookie.name, blankCookie.value, blankCookie.attributes);
}
} catch {}
return result;
}
Password signup
// app/signup/action.ts
'use server';
import { hash } from '@node-rs/argon2';
import { generateIdFromEntropySize } from 'lucia';
import { lucia } from '@/lib/auth';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { cookies } from 'next/headers';
export async function signup(formData: FormData) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const hashedPassword = await hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
const userId = generateIdFromEntropySize(10);
await db.insert(users).values({
id: userId,
email,
hashedPassword,
});
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
(await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
redirect('/dashboard');
}
Password login
'use server';
import { verify } from '@node-rs/argon2';
import { lucia } from '@/lib/auth';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export async function login(formData: FormData) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const user = await db.query.users.findFirst({
where: eq(users.email, email),
});
if (!user?.hashedPassword) {
return { error: 'Invalid email or password' };
}
const valid = await verify(user.hashedPassword, password);
if (!valid) {
return { error: 'Invalid email or password' };
}
const session = await lucia.createSession(user.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
(await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
redirect('/dashboard');
}
Logout
'use server';
import { lucia } from '@/lib/auth';
import { validateRequest } from '@/lib/auth';
import { cookies } from 'next/headers';
export async function logout() {
const { session } = await validateRequest();
if (!session) redirect('/login');
await lucia.invalidateSession(session.id);
const blankCookie = lucia.createBlankSessionCookie();
(await cookies()).set(blankCookie.name, blankCookie.value, blankCookie.attributes);
redirect('/login');
}
OAuth with Arctic
// lib/oauth.ts
import { GitHub } from 'arctic';
export const github = new GitHub(
process.env.GITHUB_CLIENT_ID!,
process.env.GITHUB_CLIENT_SECRET!,
);
// app/login/github/route.ts
import { generateState } from 'arctic';
import { github } from '@/lib/oauth';
export async function GET() {
const state = generateState();
const url = github.createAuthorizationURL(state, ['user:email']);
(await cookies()).set('github_oauth_state', state, {
path: '/',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 60 * 10,
sameSite: 'lax',
});
return Response.redirect(url);
}
// app/login/github/callback/route.ts
import { github } from '@/lib/oauth';
import { lucia } from '@/lib/auth';
export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const storedState = (await cookies()).get('github_oauth_state')?.value ?? null;
if (!code || !state || state !== storedState) {
return new Response('Invalid state', { status: 400 });
}
const tokens = await github.validateAuthorizationCode(code);
const githubUser = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
}).then(res => res.json());
// Find or create user
let user = await db.query.users.findFirst({
where: eq(users.email, githubUser.email),
});
if (!user) {
const userId = generateIdFromEntropySize(10);
[user] = await db.insert(users).values({
id: userId,
email: githubUser.email,
name: githubUser.name,
}).returning();
}
const session = await lucia.createSession(user.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
(await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: { Location: '/dashboard' },
});
}
Protected pages
import { validateRequest } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const { user } = await validateRequest();
if (!user) redirect('/login');
return <div>Welcome {user.name}</div>;
}
Best Practices
- Use Argon2 for password hashing — it's the current best practice over bcrypt
- Always refresh session cookies on validation (Lucia does this with
freshsessions) - Use
generateIdFromEntropySize()for user IDs — cryptographically secure - Use Arctic for OAuth — it handles the OAuth flow without a heavy abstraction
- Store the session cookie as httpOnly, secure, sameSite: lax
- Invalidate sessions on logout — don't just clear the cookie
Anti-Patterns
- Using MD5/SHA for password hashing — use Argon2 or bcrypt
- Not validating sessions on every request — expired sessions should be caught
- Storing session data in the cookie instead of the database
- Not clearing the session cookie on failed validation — user stays in limbo
- Building OAuth flows from scratch when Arctic handles it
- Skipping CSRF protection on auth forms
Install this skill directly: skilldb add auth-services-skills
Related Skills
Auth0
Build with Auth0 for enterprise authentication and identity. Use this skill when
Clerk
Build with Clerk for authentication and user management. Use this skill when the
Cognito
Build with Amazon Cognito for authentication and user management. Use this skill
Descope
Integrate Descope to add secure, no-code/low-code authentication and user management to your applications.
Firebase Auth
Build with Firebase Authentication for user sign-in. Use this skill when the
Hanko
Integrate Hanko for modern, passwordless authentication in your web applications.