Framework and runtime specific versions of this guide are also available.

Before starting, make sure you’ve setup Lucia and your database.

This guide will cover how to implement GitHub OAuth using Lucia with session cookies. As a general overview of OAuth, the user is redirected to github.com to be authenticated, and GitHub redirects the user back to your application with a code that can be validated and used to get the user’s identity.

Create an OAuth app#

Create a GitHub OAuth app. Set the redirect uri, for example http://localhost:3000/login/github/callback.

Copy and paste the client id and client secret into your .env file:

# .env
GITHUB_CLIENT_ID="..."
GITHUB_CLIENT_SECRET="..."

Update your database#

Add a username column to your table. It should be a string (TEXT, VARCHAR etc) type (optionally unique).

Make sure you update Lucia.DatabaseUserAttributes whenever you add any new columns to the user table.

// app.d.ts

/// <reference types="lucia" />
declare namespace Lucia {
	type Auth = import("./lucia.js").Auth;
	type DatabaseUserAttributes = {
		username: string;
	};
	type DatabaseSessionAttributes = {};
}

Configure Lucia#

Since we’re dealing with the standard Request and Response, we’ll use the web() middleware. We’re also setting sessionCookie.expires to false since we can’t update the session cookie when validating them.

// lucia.ts
import { lucia } from "lucia";
import { web } from "lucia/middleware";

export const auth = lucia({
	adapter: ADAPTER,
	env: "DEV", // "PROD" for production

	middleware: web(),
	sessionCookie: {
		expires: false
	}
});

export type Auth = typeof auth;

We also want to expose the user’s username to the User object returned by Lucia’s APIs. We’ll define getUserAttributes and return the username.

// lucia.ts
import { lucia } from "lucia";
import { web } from "lucia/middleware";

export const auth = lucia({
	adapter: ADAPTER,
	env: "DEV", // "PROD" for production
	middleware: web(),
	sessionCookie: {
		expires: false
	},

	getUserAttributes: (data) => {
		return {
			githubUsername: data.username
		};
	}
});

export type Auth = typeof auth;

Initialize the OAuth integration#

Install the OAuth integration and dotenv.

npm i @lucia-auth/oauth
pnpm add @lucia-auth/oauth
yarn add @lucia-auth/oauth

Import the GitHub OAuth integration, and initialize it using your credentials.

// lucia.ts
import { lucia } from "lucia";
import { web } from "lucia/middleware";

import { github } from "@lucia-auth/oauth/providers";

export const auth = lucia({
	// ...
});

export const githubAuth = github(auth, {
	clientId: GITHUB_CLIENT_ID, // env var
	clientSecret: GITHUB_CLIENT_SECRET // env var
});

export type Auth = typeof auth;

Generate authorization url#

Create a new GitHub authorization url, where the user should be redirected to. When generating an authorization url, Lucia will also create a new state. This should be stored as a http-only cookie to be used later.

You can use serializeCookie() provided by Lucia to get the Set-Cookie header.

import { serializeCookie } from "lucia/utils";
import { auth, githubAuth } from "./lucia.js";

get("/login/github", async () => {
	const [url, state] = await githubAuth.getAuthorizationUrl();
	const stateCookie = serializeCookie("github_oauth_state", state, {
		httpOnly: true,
		secure: false, // `true` for production
		path: "/",
		maxAge: 60 * 60
	});
	return new Response(null, {
		status: 302,
		headers: {
			Location: url.toString(),
			"Set-Cookie": stateCookie
		}
	});
});

For example, the user should be redirected to /login/github when they click “Sign in with GitHub.”

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

Validate callback#

Create your OAuth callback route that you defined when registering an OAuth app with GitHub, and handle GET requests.

When the user authenticates with GitHub, GitHub will redirect back the user to your site with a code and a state. This state should be checked with the one stored as a cookie, and if valid, validate the code with GithubProvider.validateCallback(). This will return GithubUserAuth if the code is valid, or throw an error if not.

After successfully creating a user, we’ll create a new session with Auth.createSession(). This session should be stored as a cookie, which can be created with Auth.createSessionCookie().

You can use parseCookie() provided by Lucia to read the state cookie.

import { auth, githubAuth } from "./lucia.js";
import { parseCookie } from "lucia/utils";
import { OAuthRequestError } from "@lucia-auth/oauth";

get("/login/github/callback", async (request: Request) => {
	const cookies = parseCookie(request.headers.get("Cookie") ?? "");
	const storedState = cookies.github_oauth_state;
	const url = new URL(request.url);
	const state = url.searchParams.get("state");
	const code = url.searchParams.get("code");
	// validate state
	if (!storedState || !state || storedState !== state || !code) {
		return new Response(null, {
			status: 400
		});
	}
	try {
		const { getExistingUser, githubUser, createUser } =
			await githubAuth.validateCallback(code);

		const getUser = async () => {
			const existingUser = await getExistingUser();
			if (existingUser) return existingUser;
			const user = await createUser({
				attributes: {
					username: githubUser.login
				}
			});
			return user;
		};

		const user = await getUser();
		const session = await auth.createSession({
			userId: user.userId,
			attributes: {}
		});
		const sessionCookie = auth.createSessionCookie(session);
		// redirect to profile page
		return new Response(null, {
			headers: {
				Location: "/",
				"Set-Cookie": sessionCookie.serialize() // store session cookie
			},
			status: 302
		});
	} catch (e) {
		if (e instanceof OAuthRequestError) {
			// invalid code
			return new Response(null, {
				status: 400
			});
		}
		return new Response(null, {
			status: 500
		});
	}
});

Authenticate user with Lucia#

You can check if the user has already registered with your app by checking GithubUserAuth.getExistingUser. Internally, this is done by checking if a key with the GitHub user id already exists.

If they’re a new user, you can create a new Lucia user (and key) with GithubUserAuth.createUser(). The type for attributes property is Lucia.DatabaseUserAttributes, which we added username to previously. You can access the GitHub user data with GithubUserAuth.githubUser, as well as the access tokens with GithubUserAuth.githubTokens.

const { getExistingUser, githubUser, createUser } =
	await githubAuth.validateCallback(code);

const getUser = async () => {
	const existingUser = await getExistingUser();
	if (existingUser) return existingUser;
	const user = await createUser({
		attributes: {
			username: githubUser.login
		}
	});
	return user;
};

const user = await getUser();

Redirect authenticated users#

Authenticated users should be redirected to the profile page whenever they try to access the sign in page. You can validate requests by creating a new AuthRequest instance with Auth.handleRequest() and calling AuthRequest.validate(). This method returns a Session if the user is authenticated or null if not.

Since we’re using the web() middleware, Auth.handleRequest() expects the standard Request.

import { auth } from "./lucia.js";

get("/signup", async (request: Request) => {
	const authRequest = auth.handleRequest(request);
	const session = await authRequest.validate();
	if (session) {
		// redirect to profile page
		return new Response(null, {
			headers: {
				Location: "/"
			},
			status: 302
		});
	}
	return renderPage();
});

Get authenticated user#

You can validate requests and get the current session/user by using AuthRequest.validate(). It returns a Session if the user is authenticated or null if not.

You can see that User.username exists because we defined it with getUserAttributes() configuration.

import { auth } from "./lucia.js";

get("/", async (request: Request) => {
	const authRequest = auth.handleRequest(request);
	const session = await authRequest.validate();
	if (session) {
		const user = session.user;
		const username = user.username;
		// ...
	}
	// ...
});

Sign out users#

When logging out users, it’s critical that you invalidate the user’s session. This can be achieved with Auth.invalidateSession(). You can delete the session cookie by overriding the existing one with a blank cookie that expires immediately. This can be created by passing null to Auth.createSessionCookie().

import { auth } from "./lucia.js";

post("/logout", async (request: Request) => {
	const authRequest = auth.handleRequest(request);
	// check if user is authenticated
	const session = await authRequest.validate();
	if (!session) {
		return new Response("Unauthorized", {
			status: 401
		});
	}
	// make sure to invalidate the current session!
	await auth.invalidateSession(session.sessionId);
	// create blank session cookie
	const sessionCookie = auth.createSessionCookie(null);
	return new Response(null, {
		headers: {
			Location: "/login", // redirect to login page
			"Set-Cookie": sessionCookie.serialize() // delete session cookie
		},
		status: 302
	});
});