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.
The entire cookie-based authentication works in the following manner: The user gives a username and password at the time of login. Once the user fills in the login form, the browser (client) sends a login request to the server. The server verifies the user by querying the user data.
Cookie authentication uses HTTP cookies to authenticate client requests and maintain session information. It works as follows: The client sends a login request to the server.
By default, Cookie-based authentication does not have solid protection against attacks, and they are mainly vulnerable to cross-site scripting (XSS) and cross-site request forgery (CSRF)attacks. But, we can explicitly modify Cookie headers to make them protected against such attacks.
npm install config cookie uuid string-hash mongodb
config
In root, create a folder called config. Inside it, create a file called default.json.
config/default.json
{
"mongoURI": "<yourMongoURI>",
"mongoDB": "<yourDatabaseName>"
}
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.
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.
register.js
& login.js
Endpoints inside auth
foldersrc/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'
}
};
};
Register.svelte
and Login.svelte
componentssrc/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>
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>
index.svelte
inside profile
foldersrc/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.
index.js
endpoint inside user
foldersrc/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
};
};
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 ;)
https://www.youtube.com/channel/UCGl66MHcjMDJyIPZkuKULSQ
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With