Why Migrate
The author of Lucia
plans to stop maintaining Lucia 3
on January 1, 2025.
He believes that Lucia
should become an open-source resource for JavaScript authentication.
Because its underlying implementation is very simple, based on two libraries: @oslojs/encoding
and @oslojs/crypto
.
So the author hopes that we can learn how to implement permission control from scratch, rather than installing this library.
You can refer to:
https://github.com/lucia-auth/lucia/discussions/1707
Why I Still Use Lucia?
I tried using next-auth
before, but the documentation was still a big problem. I needed to look at its source code to meet my needs, and the author’s views on password
were also a concern. Ultimately, I decided to use Lucia.
Here are the reasons why I use Lucia:
- Lucia is still a great original library for implementing authentication from scratch.
- Independent of paid third-party services
- Can learn authentication and take control
Install Dependencies
pnpm add @oslojs/encoding @oslojs/crypto
If you have previously installed Lucia and related adapters, please delete them first. I deleted:
pnpm remove lucia @lucia-auth/adapter-drizzle
lib/lucia/crypto.ts
First, create the lib/lucia/crypto.ts
file, which mainly contains the logic for generating session tokens and hashing tokens.
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
export function generateIdFromEntropySize(size: number): string {
const buffer = crypto.getRandomValues(new Uint8Array(size));
return encodeBase32LowerCaseNoPadding(buffer);
}
export function hashToken(token: string) {
return encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
}
lib/lucia/index.ts
Create the lib/lucia/index.ts
file, which mainly contains the logic for creating sessions and validating session tokens.
I use drizzle-orm
here, so I need to import the types of drizzle-orm
for session
and user
.
If you use prisma
or other databases, please replace them yourself.
Basic Types
This part mainly defines the result type of session verification.
It defines the expiration time of the session as 30 days, which you can adjust according to your actual situation.
And it defines that if the session reaches 15 days, it needs to be refreshed.
import { Session } from '@/zod/session';
import { SelectUser } from '@/zod/user';
export type SessionValidationResult =
| { session: Session; user: SelectUser; fresh: boolean }
| { session: null; user: null; fresh: boolean };
const SESSION_REFRESH_INTERVAL_MS = 1000 * 60 * 60 * 24 * 15; // 15 days
const SESSION_MAX_DURATION_MS = SESSION_REFRESH_INTERVAL_MS * 2; // 30 days
Generate Session Token
import { generateIdFromEntropySize, hashToken } from './crypto';
export function generateSessionToken(): string {
const token = generateIdFromEntropySize(20);
return hashToken(token);
}
Create Session
My db
is drizzle-orm
, so the logic for creating sessions is as follows:
import { db } from '@/db';
import { sessionTable } from '@/db/schema';
export async function createSession(sessionToken: string, userId: string): Promise<Session> {
const session = {
id: sessionToken,
userId,
expiresAt: new Date(Date.now() + SESSION_MAX_DURATION_MS),
};
const [dbSession] = await db.insert(sessionTable).values(session).returning();
return dbSession;
}
Validate Session Token
This part mainly verifies whether the session token is valid. If it is valid, it returns the session and user.
If the session expires, it deletes the session.
If the session is about to expire and reaches the previously set refresh time, it refreshes the session.
import { db } from '@/db';
import { sessionTable } from '@/db/schema';
import { eq } from 'drizzle-orm';
export async function validateSessionToken(sessionToken: string): Promise<SessionValidationResult> {
let fresh = false;
const result = await db.query.sessionTable.findFirst({
where: eq(sessionTable.id, sessionToken),
with: {
user: true,
},
});
if (!result) {
return { session: null, user: null, fresh };
}
const { user, ...session } = result;
// 如果 session 过期,删除 session
if (Date.now() >= session.expiresAt.getTime()) {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionToken));
return { session: null, user: null, fresh };
}
// 如果 session 快要过期,刷新 session
if (Date.now() >= session.expiresAt.getTime() - SESSION_REFRESH_INTERVAL_MS) {
session.expiresAt = new Date(Date.now() + SESSION_MAX_DURATION_MS);
// 设置为 fresh 表示 session 被刷新了
fresh = true;
await db.update(sessionTable).set({ expiresAt: session.expiresAt }).where(eq(sessionTable.id, sessionToken));
}
return { session, user, fresh };
}
Invalidate Session
import { db } from '@/db';
import { sessionTable } from '@/db/schema';
import { eq } from 'drizzle-orm';
export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionId));
}
lib/lucia/cookie.ts
This part mainly contains the logic for generating cookie
.
Because we need to store the session
’s token
in the cookie
.
If you use Nextjs
, you can use next/headers
to set the cookie
.
Basic Types
export interface CookieAttributes {
secure?: boolean;
path?: string;
domain?: string;
sameSite?: 'lax' | 'strict' | 'none';
httpOnly?: boolean;
maxAge?: number;
expires?: Date;
}
export interface SessionCookie {
name: string;
value: string;
attributes: CookieAttributes;
}
export const SESSION_COOKIE_NAME = 'session';
const cookieOptions: CookieAttributes = {
httpOnly: true,
sameSite: 'lax' as const,
secure: process.env.NODE_ENV === 'production',
path: '/',
};
Generate Cookie
export function createCookie(cookieValue: string, expiresAt: Date): SessionCookie {
return {
name: SESSION_COOKIE_NAME,
value: cookieValue,
attributes: { ...cookieOptions, expires: expiresAt },
};
}
export function createBlankCookie(): SessionCookie {
return {
name: SESSION_COOKIE_NAME,
value: '',
attributes: { ...cookieOptions, maxAge: 0 },
};
}
Conclusion
Isn’t it simple? The author has provided a very detailed guide for migrating Lucia
.
https://lucia-auth.com/sessions/migrate-lucia-v3
If you want to integrate third-party login, you can refer to the author’s other library: Arctic
https://arcticjs.dev/
Complete code for lucia/index.ts
import { db } from '@/db';
import { sessionTable } from '@/db/schema';
import { Session } from '@/zod/session';
import { SelectUser } from '@/zod/user';
import { eq } from 'drizzle-orm';
import { generateIdFromEntropySize, hashToken } from './crypto';
export type SessionValidationResult =
| { session: Session; user: SelectUser; fresh: boolean }
| { session: null; user: null; fresh: boolean };
const SESSION_REFRESH_INTERVAL_MS = 1000 * 60 * 60 * 24 * 15; // 15 days
const SESSION_MAX_DURATION_MS = SESSION_REFRESH_INTERVAL_MS * 2; // 30 days
export function generateSessionToken(): string {
const token = generateIdFromEntropySize(20);
return hashToken(token);
}
export async function createSession(sessionToken: string, userId: string): Promise<Session> {
const session = {
id: sessionToken,
userId,
expiresAt: new Date(Date.now() + SESSION_MAX_DURATION_MS),
};
const [dbSession] = await db.insert(sessionTable).values(session).returning();
return dbSession;
}
export async function validateSessionToken(sessionToken: string): Promise<SessionValidationResult> {
let fresh = false;
const result = await db.query.sessionTable.findFirst({
where: eq(sessionTable.id, sessionToken),
with: {
user: true,
},
});
if (!result) {
return { session: null, user: null, fresh };
}
const { user, ...session } = result;
// if the session is expired, delete it
if (Date.now() >= session.expiresAt.getTime()) {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionToken));
return { session: null, user: null, fresh };
}
// if the session is about to expire, refresh it
if (Date.now() >= session.expiresAt.getTime() - SESSION_REFRESH_INTERVAL_MS) {
session.expiresAt = new Date(Date.now() + SESSION_MAX_DURATION_MS);
fresh = true;
await db.update(sessionTable).set({ expiresAt: session.expiresAt }).where(eq(sessionTable.id, sessionToken));
}
return { session, user, fresh };
}
export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionId));
}