Discord oAuth with Sveltekit

How to log in to a website with discord

Work in progress!

Based on the amazing Sveltesnaps example

Setup

Discord App

Create a discord app on The Discord Developer Portal

Environment variables

Specify the following environment variables

  • DISCORD_CLIENT_ID
  • DISCORD_CLIENT_SECRET

Login and logout actions

/src/routes/auth/+page.server.ts

import { redirect } from '@sveltejs/kit';
import { DISCORD_CLIENT_ID } from '$env/static/private';

export const actions = {
	login: async ({ cookies, url }) => {
		const auth = new URL('https://discord.com/oauth2/authorize');

		const state = crypto.randomUUID();
		cookies.set('discord_state', state, { path: '/' });

		auth.searchParams.set('response_type', 'code');
		auth.searchParams.set('client_id', DISCORD_CLIENT_ID);
		auth.searchParams.set('scope', 'identify');
		auth.searchParams.set('prompt', 'none');
		auth.searchParams.set('redirect_uri', `${url.origin}/auth/callback`);
		auth.searchParams.set('state', state);

		throw redirect(303, auth.href);
	},

	logout: async ({ cookies, locals }) => {
		const session_id = cookies.get('session');
		if (session_id) {
			// delete session id from database here
			cookies.delete('session', { path: '/' });
		}

		locals.user = undefined;
	}
};

To log in use the following code on any page:

<form method="POST" action="/auth?/login" use:enhance>
	<button class="text-pink-600">log in</button>
</form>
<form method="POST" action="/auth?/logout" use:enhance>
    <button>log out</button>
</form>

Auth callback route

(thats the url you put as the redirect url in the discord dashboard)

src/routes/auth/callback/+server.ts

import { DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET } from '$env/static/private';
import { error, redirect } from '@sveltejs/kit';

export async function GET({ url, cookies }) {
	const state = url.searchParams.get('state');

	if (state !== cookies.get('discord_state')) {
		throw error(403);
	}

	const code = url.searchParams.get('code');

	if (!code) {
		throw new Error('callback URL was called without a code');
	}

	const { access_token } = await get_token(code, url.origin);
    // more info about the tokens: https://discord.com/developers/docs/topics/oauth2#authorization-code-grant-access-token-response
	const user = await get_user(access_token);

	const avatar = user.avatar
		? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp`
		: null;

    // create/update user in database (discord id, name, avatar, etc)
    // create session in database (session id, discord account id)

    const session = //session id from database

	cookies.set('session', session, { path: '/' });

	throw redirect(307, '/');
}

async function get_token(code: string, origin: string) {
	const data = new URLSearchParams({
		code,
		grant_type: 'authorization_code',
		redirect_uri: `${origin}/auth/callback`,
		client_id: DISCORD_CLIENT_ID,
		client_secret: DISCORD_CLIENT_SECRET
	});

	const response = await fetch('https://discord.com/api/oauth2/token', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded'
		},
		body: data.toString()
	});

	const json = await response.json();

	if (json.error) {
		throw new Error(json.error_description);
	}

	return json;
}

async function get_user(access_token: string) {
	const response = await fetch('https://discord.com/api/users/@me', {
		headers: {
			Authorization: `Bearer ${access_token}`
		}
	});

	const user = await response.json();

	return user;
}

Hooks

src/hooks.server.ts

export async function handle({ event, resolve }) {
	const session_id = event.cookies.get('session');

	if (session_id) {
		const account = // check if session token is valid and return user
		event.locals.user = account
	}

	return resolve(event);
}