Lucia

OAuth basics

For a step-by-step, framework-specific tutorial, see the GitHub OAuth tutorial.

We recommend using Arctic for implementing OAuth 2.0. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers. This page will use GitHub, and while most providers have similar APIs, there might be some minor differences between them.

npm install arctic

For this guide, the callback URL is <domain>/login/github/callback, for example http://localhost:3000/login/github/callback.

Update database

Add a username and a unique github_id column to the user table.

column type attributes
username string
github_id number unique

Declare the type with DatabaseUserAttributes and add the attributes to the user object using the getUserAttributes() configuration.

// auth.ts
import { Lucia } from "lucia";

export const lucia = new Lucia(adapter, {
	sessionCookie: {
		attributes: {
			secure: env === "PRODUCTION" // set `Secure` flag in HTTPS
		}
	},
	getUserAttributes: (attributes) => {
		return {
			githubId: attributes.github_id,
			username: attributes.username
		};
	}
});

declare module "lucia" {
	interface Register {
		Lucia: typeof lucia;
		DatabaseUserAttributes: {
			github_id: number;
			username: string;
		};
	}
}

Initialize OAuth provider

Import GitHub from Arctic and initialize it with the client ID and secret.

// auth.ts
import { GitHub } from "arctic";

export const github = new GitHub(clientId, clientSecret);

Creating authorization URL

Create a route to handle authorization. Generate a new state, create a new authorization URL with createAuthorizationURL(), store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub.

import { github } from "./auth.js";
import { generateState } from "arctic";
import { serializeCookie } from "oslo/cookie";

app.get("/login/github", async (): Promise<Response> => {
	const state = generateState();
	const url = await github.createAuthorizationURL(state);
	return new Response(null, {
		status: 302,
		headers: {
			Location: url.toString(),
			"Set-Cookie": serializeCookie("github_oauth_state", state, {
				httpOnly: true,
				secure: env === "PRODUCTION", // set `Secure` flag in HTTPS
				maxAge: 60 * 10, // 10 minutes
				path: "/"
			})
		}
	});
});

You can now create a sign in button with just an anchor tag.

<a href="/login/github">Sign in with GitHub</a>

Validate callback

In the callback route, first get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with validateAuthorizationCode(). This will throw an OAuth2RequestError if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID, and create a new user if they aren't. Finally, create a new session and set the session cookie.

import { github, lucia } from "./auth.js";
import { OAuth2RequestError } from "arctic";
import { generateIdFromEntropySize } from "lucia";
import { parseCookies } from "oslo/cookie";

app.get("/login/github/callback", async (request: Request): Promise<Response> => {
	const cookies = parseCookies(request.headers.get("Cookie") ?? "");
	const stateCookie = cookies.get("github_oauth_state") ?? null;

	const url = new URL(request.url);
	const state = url.searchParams.get("state");
	const code = url.searchParams.get("code");

	// verify state
	if (!state || !stateCookie || !code || stateCookie !== state) {
		return new Response(null, {
			status: 400
		});
	}

	try {
		const tokens = await github.validateAuthorizationCode(code);
		const githubUserResponse = await fetch("https://api.github.com/user", {
			headers: {
				Authorization: `Bearer ${tokens.accessToken}`
			}
		});
		const githubUserResult: GitHubUserResult = await githubUserResponse.json();

		const existingUser = await db
			.table("user")
			.where("github_id", "=", githubUserResult.id)
			.get();

		if (existingUser) {
			const session = await lucia.createSession(existingUser.id, {});
			const sessionCookie = lucia.createSessionCookie(session.id);
			return new Response(null, {
				status: 302,
				headers: {
					Location: "/",
					"Set-Cookie": sessionCookie.serialize()
				}
			});
		}

		const userId = generateIdFromEntropySize(10); // 16 characters long
		await db.table("user").insert({
			id: userId,
			username: githubUserResult.login,
			github_id: githubUserResult.id
		});

		const session = await lucia.createSession(userId, {});
		const sessionCookie = lucia.createSessionCookie(session.id);
		return new Response(null, {
			status: 302,
			headers: {
				Location: "/",
				"Set-Cookie": sessionCookie.serialize()
			}
		});
	} catch (e) {
		console.log(e);
		if (e instanceof OAuth2RequestError) {
			// bad verification code, invalid credentials, etc
			return new Response(null, {
				status: 400
			});
		}
		return new Response(null, {
			status: 500
		});
	}
});

interface GitHubUserResult {
	id: number;
	login: string; // username
}