SvelteKit Tutorial

Tutorial: GitHub OAuth in SvelteKit

Before starting, make sure you've set up your database and middleware as described in the Getting started page.

Create an OAuth App

Create a GitHub OAuth app. Set the redirect URI to http://localhost:5173/login/github/callback. Copy and paste the client ID and secret to your .env file.

# .env
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""

Update database

Add a github_id and username column to your user table.

column
type
attributes

github_id

number

unique

username

string

Create a DatabaseUserAttributes interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the User type. To add a githubId and username field to it, use the getUserAttributes() option.

// src/lib/server/auth.ts
import { Authora } from "authora";
import { dev } from "$app/environment";
export const authora = new Authora(adapter, {
	sessionCookie: {
		attributes: {
			secure: !dev
		}
	},
	getUserAttributes: (attributes) => {
		return {
			// attributes has the type of DatabaseUserAttributes
			githubId: attributes.github_id,
			username: attributes.username
		};
	}
});

declare module "authora" {
	interface Register {
		Authora: typeof authora;
		DatabaseUserAttributes: DatabaseUserAttributes;
	}
}

interface DatabaseUserAttributes {
	github_id: number;
	username: string;
}

Setup Arctic

We recommend using Arctic for implementing OAuth. 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.

npm install arctic

Initialize the GitHub provider with the client ID and secret.

// src/lib/server/auth.ts
// ...
import { GitHub } from "arctic";
import { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } from "$env/static/private";

export const github = new GitHub(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET);

Sign in page

Create routes/login/+page.svelte and add a basic sign in button, which should be a link to /login/github.

<!-- routes/login/+page.svelte -->
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>

Create authorization URL

Create an API route in routes/login/github/+server.ts. 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.

// routes/login/github/+server.ts
import { redirect } from "@sveltejs/kit";
import { generateState } from "arctic";
import { github } from "$lib/server/auth";

import type { RequestEvent } from "@sveltejs/kit";

export async function GET(event: RequestEvent): Promise<Response> {
	const state = generateState();
	const url = await github.createAuthorizationURL(state);

	event.cookies.set("github_oauth_state", state, {
		path: "/",
		secure: import.meta.env.PROD,
		httpOnly: true,
		maxAge: 60 * 10,
		sameSite: "lax"
	});

	redirect(302, url.toString());
}

Validate callback

Create an API route in routes/login/github/callback/+server.ts to handle the callback. 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.

// routes/login/github/callback/+server.ts
import { OAuth2RequestError } from "arctic";
import { generateIdFromEntropySize } from "lucia";
import { github, authora } from "$lib/server/auth";

import type { RequestEvent } from "@sveltejs/kit";

export async function GET(event: RequestEvent): Promise<Response> {
	const code = event.url.searchParams.get("code");
	const state = event.url.searchParams.get("state");
	const storedState = event.cookies.get("github_oauth_state") ?? null;

	if (!code || !state || !storedState || state !== storedState) {
		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 githubUser: GitHubUser = await githubUserResponse.json();

		// Replace this with your own DB client.
		const existingUser = await db.table("user").where("github_id", "=", githubUser.id).get();

		if (existingUser) {
			const session = await authora.createSession(existingUser.id, {});
			const sessionCookie = authora.createSessionCookie(session.id);
			event.cookies.set(sessionCookie.name, sessionCookie.value, {
				path: ".",
				...sessionCookie.attributes
			});
		} else {
			const userId = generateIdFromEntropySize(10); // 16 characters long

			// Replace this with your own DB client.
			await db.table("user").insert({
				id: userId,
				github_id: githubUser.id,
				username: githubUser.login
			});

			const session = await authora.createSession(userId, {});
			const sessionCookie = authora.createSessionCookie(session.id);
			event.cookies.set(sessionCookie.name, sessionCookie.value, {
				path: ".",
				...sessionCookie.attributes
			});
		}
		return new Response(null, {
			status: 302,
			headers: {
				Location: "/"
			}
		});
	} catch (e) {
		// the specific error message depends on the provider
		if (e instanceof OAuth2RequestError) {
			// invalid code
			return new Response(null, {
				status: 400
			});
		}
		return new Response(null, {
			status: 500
		});
	}
}

interface GitHubUser {
	id: number;
	login: string;
}

Validate requests Without Middleware

You can validate requests by checking locals.user. The field user.username is available since we defined the getUserAttributes() option. You can protect pages, such as /, by redirecting unauthenticated users to the login page.

// routes/+page.server.ts
import type { PageServerLoad } from "./$types";
import { authora } from "$lib/server/auth";

export const load: PageServerLoad = async (event) => {
        // get the cookie with the session cookie name (for those using cookie)
        const sessionCookieName = authora.sessionCookieName;
        
        // get the jwt from the header for those using header
        
        const { decoded, status, error } = authora.verifyJWTToken(token);
	
	// handle the rest

	return {
		user: {}
	};
};

Validate requests With Middleware Included

To Be completed soon. in progress

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { github, authora } from "$lib/server/auth";

export const handle: Handle = async ({ event, resolve }) => {
        const sessionCookieName = authora.sessionCookieName;
	event.locals.user = await getUserInformation(event.cookies.get(sessionCookieName));

	const response = await resolve(event);
	response.headers.set('x-custom-header', 'potato');

	return response;
};

// getUserInformation to be completed

More details on hooks https://kit.svelte.dev/docs/hooks https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks

Sign out user !For Session Only

Sign out users by invalidating their session with Authora.invalidateSession(). Make sure to remove their session cookie by setting a blank session cookie created with Authora.createBlankSessionCookie().

// routes/+page.server.ts
import { fail, redirect } from "@sveltejs/kit";
import { authora } from "$lib/server/auth";

import type { Actions, PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ locals }) => {
	// ...
};

export const actions: Actions = {
	default: async (event) => {
		if (!event.locals.session) {
			return fail(401);
		}
		await authora.invalidateSession(event.locals.session.id);
		const sessionCookie = authora.createBlankSessionCookie();
		event.cookies.set(sessionCookie.name, sessionCookie.value, {
			path: ".",
			...sessionCookie.attributes
		});
		redirect(302, "/login");
	}
};
<!-- routes/+page.svelte -->
<script lang="ts">
	import { enhance } from "$app/forms";
</script>

<form method="post" use:enhance>
    <button>Sign out</button>
</form>

Tutorial: Email and Password Auth in SvelteKit

Before starting, make sure you've set up your database and middleware as described in the Getting started page.

Update database

Add a username and password_hash column to your user table.

column
type
attributes

email

string

unique

password_hash

string

Create a DatabaseUserAttributes interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the User type. To add a email field to it, use the getUserAttributes() option.

// src/lib/server/auth.ts
import { Authora } from "authora";
import { dev } from "$app/environment";
// src/lib/server/auth.ts
export const authora = new Authora(adapter, {
	sessionCookie: {
		attributes: {
			secure: !dev
		}
	},
	getUserAttributes: (attributes) => {
		return {
			// attributes has the type of DatabaseUserAttributes
			email: attributes.email
		};
	}
});

declare module "authora" {
	interface Register {
		Authora: typeof authora;
		DatabaseUserAttributes: DatabaseUserAttributes;
	}
}

interface DatabaseUserAttributes {
	email: string;
}

Sign up user

Create routes/signup/+page.svelte and set up a basic form.

<!-- routes/signup/+page.svelte -->
<script lang="ts">
	import { enhance } from "$app/forms";
</script>

<h1>Sign up</h1>
<form method="post" use:enhance>
	<label for="email">Email</label>
	<input type="email" name="email" id="email" /><br />
	<label for="password">Password</label>
	<input type="password" name="password" id="password" /><br />
    <button>Continue</button>
</form>

Create a form action in routes/signup/+page.server.ts. First, do a very basic input validation. Hash the password, generate a new user ID, and create a new user. If successful, create a new session with Authora.createSession() and set a new session cookie.

// routes/signup/+page.server.ts
import { authora } from "$lib/server/auth";
import { fail, redirect } from "@sveltejs/kit";
import { generateIdFromEntropySize } from "authora";
import { hash } from "@node-rs/argon2";

import type { Actions } from "./$types";

export const actions: Actions = {
	default: async (event) => {
		const formData = await event.request.formData();
		const username = formData.get("email");
		const password = formData.get("password");
		
		if (
			typeof email !== "string" ||
		        email.length < 7 ||
		        email.length > 31 ||
		        !email.includes("@")
		) {
			return fail(400, {
				message: "Invalid email"
			});
		}
		if (typeof password !== "string" || password.length < 6 || password.length > 255) {
			return fail(400, {
				message: "Invalid password"
			});
		}

		const userId = generateIdFromEntropySize(10); // 16 characters long
		const passwordHash = await hash(password, {
			// recommended minimum parameters
			memoryCost: 19456,
			timeCost: 2,
			outputLen: 32,
			parallelism: 1
		});

		// TODO: check if username is already used
		await db.table("user").insert({
			id: userId,
			email: email,
			password_hash: passwordHash
		});

		const { token, status, error } = authora.createJWTToken(existingUser)
		    if (error || !status) {
		       // handle the error
		    }
		//. if you want to save it on cookie
		const sessionCookie = authora.createSessionCookie(existingUser.id);
		event.cookies.set(sessionCookie.name, token, {
				path: ".",
				...sessionCookie.attributes
		});
		redirect(302, "/");
		// for api
		// return the token
	}
};

For API SignUp

follow the above signup process, the only difference will be that you will return an object(json) instead of a redirect.

Argon2id should be your first choice for hashing passwords, followed by Scrypt and Bcrypt. Hashing is by definition computationally expensive so you should use the most performant option for your runtime.

  • For Node.js we recommend using @node-rs/argon2.

  • For Bun, we recommend using Bun.password.

  • Use Deno-specific packages for Deno.

  • For other runtimes (e.g. Cloudflare Workers), your choice is very limited. @noble/hashes provides pure-js implementations of various hashing algorithms, but because it's written in JS, you may hit into CPU limitations of your service. If possible, avoid these runtimes when you need to hash passwords.

Make sure to check the recommended minimum parameters for your hashing algorithm.

Sign in user

Create routes/login/+page.svelte and set up a basic form.

<!-- routes/login/+page.svelte -->
<script lang="ts">
	import { enhance } from "$app/forms";
</script>

<h1>Sign in</h1>
<form method="post" use:enhance>
	<label for="email">Email</label>
	<input name="email" id="email" /><br />
	<label for="password">Password</label>
	<input type="password" name="password" id="password" /><br />
	<button>Continue</button>
</form>

Create a form action in routes/login/+page.server.ts. First, do a very basic input validation. Get the user with the username and verify the password. If successful, create a new session with Authora.createSession() and set a new session cookie.

// routes/login/+page.server.ts
import { authora } from "$lib/server/auth";
import { fail, redirect } from "@sveltejs/kit";
import { verify } from "@node-rs/argon2";

import type { Actions } from "./$types";

export const actions: Actions = {
	default: async (event) => {
		const formData = await event.request.formData();
		const email = formData.get("email");
		const password = formData.get("password");

		if (
			typeof email !== "string" ||
		        email.length < 7 ||
		        email.length > 31 ||
		        !email.includes("@")
		) {
			return fail(400, {
				message: "Invalid email"
			});
		}
		if (typeof password !== "string" || password.length < 6 || password.length > 255) {
			return fail(400, {
				message: "Invalid password"
			});
		}

		const existingUser = await db
			.table("email")
			.where("email", "=", email.toLowerCase())
			.get();
		if (!existingUser) {
			// NOTE:
			// Returning immediately allows malicious actors to figure out valid usernames from response times,
			// allowing them to only focus on guessing passwords in brute-force attacks.
			// As a preventive measure, you may want to hash passwords even for invalid usernames.
			// However, valid usernames can be already be revealed with the signup page among other methods.
			// It will also be much more resource intensive.
			// Since protecting against this is non-trivial,
			// it is crucial your implementation is protected against brute-force attacks with login throttling etc.
			// If usernames are public, you may outright tell the user that the username is invalid.
			return fail(400, {
				message: "Incorrect username or password"
			});
		}

		const validPassword = await verify(existingUser.password_hash, password, {
			memoryCost: 19456,
			timeCost: 2,
			outputLen: 32,
			parallelism: 1
		});
		if (!validPassword) {
			return fail(400, {
				message: "Incorrect email or password"
			});
		}

		const session = await authora.createSession(existingUser.id, {});
		const sessionCookie = authora.createSessionCookie(session.id);
		event.cookies.set(sessionCookie.name, sessionCookie.value, {
			path: ".",
			...sessionCookie.attributes
		});

		redirect(302, "/");
	}
};

Validate requests Without Middleware -email

check the above solution

Sign out user -email

check the above solution

Last updated