Okupter

Handling authentication and authorization with JWT in SvelteKit

Tuesday, August 16, 2022

SvelteKit is a meta-framework on top of Svelte for building web applications. It comes with a handful of features that make it delightful to work with. Things like the built-in Load function, API routes and shadow endpoints provide a full-fledged experience for end to end web applications development.

I have been using SvelteKit for quite some time now and at some point, I’ve struggled with finding a consistent way to handle all the authentication and authorization flow. Because I’ve not implemented such a thing in a JavaScript context before.

After a handful of trials, reading various repositories code and tutorials, I’ve come up with an implementation that seems satisfying for me and that I’m going to share in this article.

How does SvelteKit handles server side code?

In SvelteKit, you have two main ways to run server side code: API routes (or endpoints, as they officially called in the documentation) and Load functions.

Endpoints are JavaScript or TypeScript files that export regular HTTP methods. They return a request handler with status code, headers and a body object.

Load functions are defined in page or layouts, and they run before the component is created. These functions runs both during server side and client side rendering.

In this tutorial, we’ll create a small project with a sign up, login and authenticated pages. Our authentication and authorization login are going to be implemented in the API routes, and we will, use SvelteKit hooks and sessions to authorize users in the guarded routes.

We will use JSON Web Tokens (JWT) to generate and verify encrypted tokens that will be send along with all authenticated requests. We will also use a SQLite database as our store and Prisma to interact with it.

Creating the project

The final code for this tutorial is available on my GitHub here.

It’s pretty straightforward to create a SvelteKit project, by running the following command:

bash
npm create [email protected] sveltekit-auth-jwt

I’m using the skeleton template with TypeScript, Prettier and ESLint enabled.

Installing necessary dependencies

We will need a few dependencies that will be used along the way. First we install prisma as a development dependency:

bash
npm install prisma

Now we will install bcryptjs to hash and compare passwords, jwt to generate and verify tokens, cookie to parse and serialize cookies, and @prisma/client to interact with our database:

bash
npm install bcryptjs jsonwebtoken cookie @prisma/client

When using TypeScript, some dependencies require type definitions. We install them with the following command:

bash
npm install -D @types/bcryptjs @types/jsonwebtoken @types/cookie

Setting up the database

Let’s now use Prisma, to initialize a SQLite database and create a User model.

bash
npx prisma init --datasource-provider sqlite

In our schema.prisma file, we will create a simple User model with an email and password fields.

prisma
// /prisma/schema.prisma
model User {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
password String
}

We can generate the Prisma client and apply our schema changes to the database by running

bash
npx prisma db push

How does JWT works?

JWT is basically a standard to securely transmit information between parties (in our case, a client and a server) as a JSON object. This information can be verified and trusted because it is digitally signed using a secret or a public/private key pair.

In an authentication-authorization flow, after a user successfully logs in, the server generates a JWT that contains some user’s information and sends it back to the client. The client then stores this token in a cookie or local storage and sends it along with every request to the server; which can then verify the token and grant access to protected resources/information.

Here is a simple diagram of how JWT works:

The Astro language

Authentication pages for our app

Now that we have a basic understanding of how JWT works, let’s create our authentication pages.

The signup page HTML markup is pretty straightforward. It’s simply a form with an email and password input, and a submit button. We use Svelte on:submit element directive to handle the form submission by passing the form data to the handleSignup function. We also add a preventDefault modifier to the directive to prevent the browser from reloading the page after the form submission.

svelte
<section>
<form on:submit|preventDefault={handleSignup}>
<div class="group">
<label for="email">Email</label>
<input type="email" id="email" bind:value={email} required />
</div>
<div class="group">
<label for="password">Password</label>
<input type="password" id="password" bind:value={password} required />
</div>
<div class="submit-container">
<button type="submit">Sign Up</button>
</div>
</form>
<div class="actions">
<a href="/login">Login</a>
</div>
</section>

Here, the handleSignup function is responsible for handling the form submission. It will call the signup endpoint and redirect the user to the login page if the signup is successful.

svelte
<script lang="ts">
import { goto } from '$app/navigation';
let email: string;
let password: string;
const handleSignup = async () => {
const response = await fetch('/api/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password
})
}).then((res) => res.json());
const { error } = response;
if (error) {
alert(error);
} else {
// Go to login page
goto('/login');
}
};
</script>

We also show an alert if any error occurs from our endpoint. We will see in details how this endpoint is implemented soon.

The login page is pretty similar to the signup page. The only difference is that we use the handleLogin function to handle the form submission.

svelte
<section>
<form on:submit|preventDefault={handleLogin}>
<div class="group">
<label for="email">Email</label>
<input type="email" id="email" bind:value={email} required />
</div>
<div class="group">
<label for="password">Password</label>
<input type="password" id="password" bind:value={password} required />
</div>
<div class="submit-container">
<button type="submit">Login</button>
</div>
</form>
<div class="actions">
<a href="/signup">Sign Up</a>
</div>
</section>
svelte
<script lang="ts">
let email: string;
let password: string;
const handleLogin = async () => {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password
})
}).then((res) => res.json());
const { error } = response;
if (error) {
alert(error);
} else {
window.location.href = '/guarded';
}
};
</script>

Authentication endpoints

As you can guess from the previous section, we will implement our authentication endpoints in the api directory in two files: signup.ts and login.ts.

ts
// /api/signup.ts
import type { RequestHandler } from "@sveltejs/kit";
import { createUser } from "$lib/user.model";
export const POST: RequestHandler = async ({ request }) => {
const { email, password } = await request.json();
// Create a new user
const { error } = await createUser(email, password);
if (error) {
return {
status: 500,
body: {
error,
},
};
}
return {
status: 201,
body: {
message: "User created",
},
};
};

What we do here is to create a new user in the database using the createUser function, and we return a success response if the user is created successfully.

ts
// /api/login.ts
import type { RequestHandler } from "@sveltejs/kit";
import cookie from "cookie";
import { loginUser } from "$lib/user.model";
export const POST: RequestHandler = async ({ request }) => {
const { email, password } = await request.json();
const { error, token } = await loginUser(email, password);
if (error) {
return {
status: 401,
body: {
error,
},
};
}
const authCookie = cookie.serialize("AuthorizationToken", `Bearer ${token}`, {
httpOnly: true,
path: "/",
secure: true,
sameSite: "strict",
maxAge: 60 * 60 * 24, // 1 day
});
return {
status: 200,
headers: {
"set-cookie": authCookie,
location: "/",
},
};
};

For the login endpoint, we use the loginUser function to authenticate the user and return a JWT token if the authentication is successful. We then set the AuthorizationToken cookie with the JWT token and return a success response.

Note here a few things:

You can read the MDN documentation for more information about the Set-Cookie header.

I am not going to detail here how the createUser and loginUser functions are implemented. These can be found in this post GitHub repository here. It’s basically some Prisma queries with some error handling and validation.

However here is a quick overview of the how the token is generated after a successful login:

ts
const jwtUser = {
id,
user,
};
const token = jwt.sign(jwtUser, "JWT_SECRET", {
expiresIn: "1d",
});

Implementing authorization hook

If you come from a framework like Express, you might be familiar with the concept of middleware. In SvelteKit, we have something similar called hooks. These are functions that are called before a request is handled by a route.

The most used hook is the handle hook, which simply take a request and return a response. We will also use the getSession hook to save user data in a session that will be accessible in on the client.

Per convention, hooks are defined in a /src/hooks.js or /src/hooks.ts file.

ts
// /src/hooks.ts
const handle: Handle = async ({ event, resolve }) => {
const { headers } = event.request;
const cookies = parse(headers.get("cookie") ?? "");
if (cookies.AuthorizationToken) {
// Remove Bearer prefix
const token = cookies.AuthorizationToken.split(" ")[1];
try {
const jwtUser = jwt.verify(token, (Object.assign(import.meta.env,{_:process.env._,})).VITE_JWT_ACCESS_SECRET);
if (typeof jwtUser === "string") {
throw new Error("Something went wrong");
}
const user = await db.user.findUnique({
where: {
id: jwtUser.id,
},
});
if (!user) {
throw new Error("User not found");
}
const sessionUser: SessionUser = {
id: user.id,
email: user.email,
};
event.locals.user = sessionUser;
} catch (error) {
console.error(error);
}
}
return await resolve(event);
};

Here are the things that are happening in the handle hook:

We are using here a SessionUser interface to define the user object that will be saved in the session. We define it as follows:

ts
export type SessionUser = {
id: string;
email: string;
};

We can now save the user in the session using the getSession hook:

ts
// /src/hooks.ts
const getSession: GetSession = ({ locals }) => {
return {
user: locals.user,
};
};

Protecting routes

For our protected routes, we will make use of SvelteKit Load function. Since it runs before the component is created, we can use it to get the session, and either displaying the component or redirecting the user to the login page.

svelte
<!-- /src/routes/guarded.svelte -->
<script lang="ts" context="module">
import type { Load } from '@sveltejs/kit';
import type { SessionUser } from 'src/hooks';
export const load: Load = ({ session }) => {
const user = session.user;
if (!user) {
return {
status: 302,
message: 'You must be logged in to view this page',
redirect: '/login'
};
}
return {
props: {
user: session.user
}
};
};
</script>
<script lang="ts">
export let user: SessionUser;
</script>
<svelte:head>
<title>Guarded page</title>
</svelte:head>
<h1>Guarded page</h1>
<p>This page is guarded and will only be accessible to authenticated users.</p>
<p>Hello {user.email}</p>

Wrapping up

This tutorial just shows one of the many ways to implement authentication and authorization in Svelte. There are many others aspects that I haven’t covered here. Things like implementing refreshing the token when it expires, implementing a logout or password reset endpoints. I’ve also used a very basic validation system both on the client and on the server.

I’ll write more about these in future articles.