The question stands as-is - how to implement cookie authentication in a SvelteKit & MongoDB app? Meaning how to properly use hooks, endpoints, establish a DB connection and show it on a boilerplate-ish project.
1 Answers
After SvelteKit Project Initialisation
#1 Install additional dependencies
npm install config cookie uuid string-hash mongodb
- I prefer config over vite's .env variables due to all the leaks and problems regarding it
- cookie is used to properly set cookies
- uuid is used to generate complex cookie IDs
- string-hash is a simple yet secure hashing for passwords stored in your DB
- mongodb is used to establish a connection to your DB
#2 Set up config
In root, create a folder called config. Inside it, create a file called default.json.
config/default.json
{
"mongoURI": "<yourMongoURI>",
"mongoDB": "<yourDatabaseName>"
}
#3 Set up base DB connection code
Create lib
folder in src
. Inside it, create db.js
file.
src/lib/db.js
import { MongoClient } from 'mongodb';
import config from 'config';
export const MONGODB_URI = config.get('mongoURI');
export const MONGODB_DB = config.get('mongoDB');
if (!MONGODB_URI) {
throw new Error('Please define the mongoURI property inside config/default.json');
}
if (!MONGODB_DB) {
throw new Error('Please define the mongoDB property inside config/default.json');
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = global.mongo;
if (!cached) {
cached = global.mongo = { conn: null, promise: null };
}
export const connectToDatabase = async () => {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
useNewUrlParser: true,
useUnifiedTopology: true
};
cached.promise = MongoClient.connect(MONGODB_URI, opts).then((client) => {
return {
client,
db: client.db(MONGODB_DB)
};
});
}
cached.conn = await cached.promise;
return cached.conn;
}
The code is taken from next.js implementation of MongoDB connection establishment and modified to use config instead of .env.
#4 Create hooks.js
file inside src
src/hooks.js
import * as cookie from 'cookie';
import { connectToDatabase } from '$lib/db';
// Sets context in endpoints
// Try console logging context in your endpoints' HTTP methods to understand the structure
export const handle = async ({ request, resolve }) => {
// Connecting to DB
// All database code can only run inside async functions as it uses await
const dbConnection = await connectToDatabase();
const db = dbConnection.db;
// Getting cookies from request headers - all requests have cookies on them
const cookies = cookie.parse(request.headers.cookie || '');
request.locals.user = cookies;
// If there are no cookies, the user is not authenticated
if (!cookies.session_id) {
request.locals.user.authenticated = false;
}
// Searching DB for the user with the right cookie
// All database code can only run inside async functions as it uses await
const userSession = await db.collection('cookies').findOne({ cookieId: cookies.session_id });
// If there is that user, authenticate him and pass his email to context
if (userSession) {
request.locals.user.authenticated = true;
request.locals.user.email = userSession.email;
} else {
request.locals.user.authenticated = false;
}
const response = await resolve(request);
return {
...response,
headers: {
...response.headers
// You can add custom headers here
// 'x-custom-header': 'potato'
}
};
};
// Sets session on client-side
// try console logging session in routes' load({ session }) functions
export const getSession = async (request) => {
// Pass cookie with authenticated & email properties to session
return request.locals.user
? {
user: {
authenticated: true,
email: request.locals.user.email
}
}
: {};
};
Hooks authenticate the user based on cookies and pass the desired variables (in this example it is the user's email etc.) to context & session.
#5 Create register.js
& login.js
Endpoints inside auth
folder
src/routes/auth/register.js
import stringHash from 'string-hash';
import * as cookie from 'cookie';
import { v4 as uuidv4 } from 'uuid';
import { connectToDatabase } from '$lib/db';
export const post = async ({ body }) => {
// Connecting to DB
// All database code can only run inside async functions as it uses await
const dbConnection = await connectToDatabase();
const db = dbConnection.db;
// Is there a user with such an email?
const user = await db.collection('testUsers').findOne({ email: body.email });
// If there is, either send status 409 Conflict and inform the user that their email is already taken
// or send status 202 or 204 and tell them to double-check on their credentials and try again - it is considered more secure
if (user) {
return {
status: 409,
body: {
message: 'User with that email already exists'
}
};
}
// Add user to DB
// All database code can only run inside async functions as it uses await
await db.collection('testUsers').insertOne({
name: body.name,
email: body.email,
password: stringHash(body.password)
});
// Add cookie with user's email to DB
// All database code can only run inside async functions as it uses await
const cookieId = uuidv4();
await db.collection('cookies').insertOne({
cookieId,
email: body.email
});
// Set cookie
// If you want cookies to be passed alongside user when they redirect to another website using a link, change sameSite to 'lax'
// If you don't want cookies to be valid everywhere in your app, modify the path property accordingly
const headers = {
'Set-Cookie': cookie.serialize('session_id', cookieId, {
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: 'strict',
path: '/'
})
};
return {
status: 200,
headers,
body: {
message: 'Success'
}
};
};
If you want to take it a step further, don't forget to create Schemas with Mongoose!
src/routes/auth/login.js
import stringHash from 'string-hash';
import * as cookie from 'cookie';
import { v4 as uuidv4 } from 'uuid';
import { connectToDatabase } from '$lib/db';
export const post = async ({ body }) => {
const dbConnection = await connectToDatabase();
const db = dbConnection.db;
const user = await db.collection('testUsers').findOne({ email: body.email });
if (!user) {
return {
status: 401,
body: {
message: 'Incorrect email or password'
}
};
}
if (user.password !== stringHash(body.password)) {
return {
status: 401,
body: {
message: 'Unauthorized'
}
};
}
const cookieId = uuidv4();
// Look for existing email to avoid duplicate entries
const duplicateUser = await db.collection('cookies').findOne({ email: body.email });
// If there is user with cookie, update the cookie, otherwise create a new DB entry
if (duplicateUser) {
await db.collection('cookies').updateOne({ email: body.email }, { $set: { cookieId } });
} else {
await db.collection('cookies').insertOne({
cookieId,
email: body.email
});
}
// Set cookie
const headers = {
'Set-Cookie': cookie.serialize('session_id', cookieId, {
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: 'strict',
path: '/'
})
};
return {
status: 200,
headers,
body: {
message: 'Success'
}
};
};
#6 Create Register.svelte
and Login.svelte
components
src/lib/Register.svelte
<script>
import { createEventDispatcher } from 'svelte';
// Dispatcher for future usage in /index.svelte
const dispatch = createEventDispatcher();
// Variables bound to respective inputs via bind:value
let email;
let password;
let name;
let error;
const register = async () => {
// Reset error from previous failed attempts
error = undefined;
try {
// POST method to src/routes/auth/register.js endpoint
const res = await fetch('/auth/register', {
method: 'POST',
body: JSON.stringify({
email,
password,
name
}),
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
dispatch('success');
} else {
error = 'An error occured';
}
} catch (err) {
console.log(err);
error = 'An error occured';
}
};
</script>
<h1>Register</h1>
<input type="text" name="name" placeholder="Enter your name" bind:value={name} />
<input type="email" name="email" placeholder="Enter your email" bind:value={email} />
<input type="password" name="password" placeholder="Enter your password" bind:value={password} />
{#if error}
<p>{error}</p>
{/if}
<button on:click={register}>Register</button>
src/lib/Login.svelte
<script>
import { createEventDispatcher } from 'svelte';
// Dispatcher for future usage in /index.svelte
const dispatch = createEventDispatcher();
// Variables bound to respective inputs via bind:value
let email;
let password;
let error;
const login = async () => {
// Reset error from previous failed attempts
error = undefined;
// POST method to src/routes/auth/login.js endpoint
try {
const res = await fetch('/auth/login', {
method: 'POST',
body: JSON.stringify({
email,
password
}),
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
dispatch('success');
} else {
error = 'An error occured';
}
} catch (err) {
console.log(err);
error = 'An error occured';
}
};
</script>
<h1>Login</h1>
<input type="email" name="email" placeholder="Enter your email" bind:value={email} />
<input type="password" name="password" placeholder="Enter your password" bind:value={password} />
{#if error}
<p>{error}</p>
{/if}
<button on:click={login}>Login</button>
#7 Update src/routes/index.svelte
src/routes/index.svelte
<script>
import Login from '$lib/Login.svelte';
import Register from '$lib/Register.svelte';
import { goto } from '$app/navigation';
// Redirection to /profile
function redirectToProfile() {
goto('/profile');
}
</script>
<main>
<h1>Auth with cookies</h1>
<!-- on:success listens for dispatched 'success' events -->
<Login on:success={redirectToProfile} />
<Register on:success={redirectToProfile} />
</main>
#8 Create index.svelte
inside profile
folder
src/routes/profile/index.svelte
<script context="module">
export async function load({ session }) {
if (!session.user.authenticated) {
return {
status: 302,
redirect: '/auth/unauthorized'
};
}
return {
props: {
email: session.user.email
}
};
}
</script>
<script>
import { onMount } from 'svelte';
export let email;
let name;
onMount(async () => {
const res = await fetch('/user');
const user = await res.json();
name = user.name;
});
</script>
<h1>Profile</h1>
<p>Hello {name} you are logged in with the email {email}</p>
Pay attention to session we set up in hooks.js
. console.log()
it to understand its structure better. I won't be implementing /auth/unauthorized
route, so mind that.
#9 Create index.js
endpoint inside user
folder
src/routes/user/index.js
import { connectToDatabase } from '$lib/db';
export const get = async (context) => {
// Connecting to DB
// All database code can only run inside async functions as it uses await
const dbConnection = await connectToDatabase();
const db = dbConnection.db;
// Checking for auth coming from hooks' handle({ request, resolve })
if (!context.locals.user.authenticated) {
return {
status: 401,
body: {
message: 'Unauthorized'
}
};
}
const user = await db.collection('testUsers').findOne({ email: context.locals.user.email });
if (!user) {
return {
status: 404,
body: {
message: 'User not found'
}
};
}
// Find a proper way in findOne(), I've run out of gas ;)
delete user.password;
return {
status: 200,
body: user
};
};
Final thoughts
There are almost none tutorials regarding SvelteKit and I'll surely find this guide useful in my future projects. If you find a bug or see an improvement, feel free to let me know so I can make this guide better ;)
Big thanks to Brayden Girard for a precedent for this guide!
https://www.youtube.com/channel/UCGl66MHcjMDJyIPZkuKULSQ
Happy coding!

- 626
- 7
- 16
-
4For all that work, wouldn't a public github repo be really helpful to all ? – zipzit Sep 13 '21 at 09:20
-
5[Public repo done. Click here...](https://github.com/zipzit/Sveltekit_Cookie_Auth) Note you will have to add your own Mongodb Atlas login info... – zipzit Sep 14 '21 at 21:53
-
@Nikolas Thank you for the answer. Is it secure to pass `authorization info` through `locals.user.authenticated`? Can't hackers set it to `true` and have access to private pages? – Ulvi Jul 15 '22 at 19:20