Lucia v3 was the most popular auth library for TypeScript. Then the creator did something radical: deprecated the library and wrote a guide instead.
The result? The Copenhagen Book — a free, framework-agnostic guide to implementing authentication from scratch. No magic. No black boxes.
Why Deprecate a Popular Library?
The Lucia team realized: auth libraries create a false sense of security. Developers use them without understanding what happens under the hood. When something breaks, they are helpless.
The Copenhagen Book teaches you to build auth yourself — so you actually understand it.
Session-Based Auth (The Right Way)
// 1. Generate session token
import { sha256 } from "@oslojs/crypto/sha2";
import { encodeHexLowerCase } from "@oslojs/encoding";
function generateSessionToken(): string {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
return encodeHexLowerCase(bytes);
}
// 2. Create session in database
async function createSession(token: string, userId: string) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
await db.insert(sessions).values({
id: sessionId,
userId,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
});
return sessionId;
}
// 3. Validate session
async function validateSession(token: string) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session = await db.query.sessions.findFirst({
where: eq(sessions.id, sessionId),
with: { user: true },
});
if (!session || session.expiresAt < new Date()) {
if (session) await db.delete(sessions).where(eq(sessions.id, sessionId));
return { session: null, user: null };
}
// Extend session if close to expiry
if (session.expiresAt.getTime() - Date.now() < 15 * 24 * 60 * 60 * 1000) {
session.expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
await db.update(sessions).set({ expiresAt: session.expiresAt });
}
return { session, user: session.user };
}
Password Hashing
import { hash, verify } from "@node-rs/argon2";
// Hash password on registration
const passwordHash = await hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
// Verify on login
const valid = await verify(passwordHash, password);
Always use Argon2id — not bcrypt, not scrypt, not SHA-256.
OAuth2 (Google, GitHub, etc.)
import { Google } from "arctic";
const google = new Google(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
"http://localhost:3000/auth/google/callback"
);
// 1. Generate authorization URL
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "email"]);
// 2. Handle callback
const tokens = await google.validateAuthorizationCode(code, codeVerifier);
const response = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
});
const googleUser = await response.json();
Libraries to Use
- @oslojs/crypto — cryptographic utilities
- @oslojs/encoding — encoding helpers
- arctic — OAuth2 providers (Google, GitHub, Discord, etc.)
- @node-rs/argon2 — password hashing
All by the Lucia team. Small, focused, no magic.
The Session Cookie
function setSessionCookie(token: string) {
return {
name: "session",
value: token,
attributes: {
httpOnly: true,
secure: true,
sameSite: "lax" as const,
path: "/",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
};
}
httpOnly + secure + sameSite = lax — the holy trinity of session cookie security.
Need secure authentication for your app? I build web tools and developer solutions. Email spinov001@gmail.com or check my Apify tools.
Top comments (0)