Okupter

Handling authentication and authorization with JWT in SvelteKit

Justin's profile pic

Justin Ahinon

Last updated on

Handling authentication and authorization with JWT in SvelteKit

SvelteKit version: 1.0.0-next.572

GitHub repository: https://github.com/JustinyAhin/sveltekit-auth-jwt

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 load functions, API routes, server functions, form actions and progressive enhancement 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 server functions. Server functions are created in +page.server.ts or +layout.server.ts files or their JS equivalents.

Endpoints are JavaScript or TypeScript files that export regular HTTP methods. They take a request event in parameter and return a Response object.

Server functions can either be load functions or form actions. load functions run both during server side and client side rendering, while form actions are triggered as a result of a form submission.

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

We will use JSON Web Tokens (JWT) to generate and verify encrypted tokens that will be sent 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:

pnpm create svelte@latest 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:

pnpm install prisma

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

pnpm install bcryptjs jsonwebtoken @prisma/client

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

pnpm install -D @types/bcryptjs @types/jsonwebtoken

Setting up the database

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

pnpx prisma init --datasource-provider sqlite

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

model <span class="hljs-title class_">User</span> {
  id        <span class="hljs-title class_">String</span>   @id @<span class="hljs-title function_">default</span>(<span class="hljs-title function_">cuid</span>())
  createdAt <span class="hljs-title class_">DateTime</span> @<span class="hljs-title function_">default</span>(<span class="hljs-title function_">now</span>())
  updatedAt <span class="hljs-title class_">DateTime</span> @updatedAt
  email     <span class="hljs-title class_">String</span>   @unique
  password  <span class="hljs-title class_">String</span>
}

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

pnpx 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:

JWT auth flow

Authentication pages for our app

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

Sign up

The signup page HTML markup is pretty straightforward. It’s simply a form with an email and password input, and a submit button.

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">method</span>=<span class="hljs-string">&quot;post&quot;</span>&gt;</span>
		<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;group&quot;</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">&quot;email&quot;</span>&gt;</span>Email<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;email&quot;</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;email&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;email&quot;</span> <span class="hljs-attr">required</span> /&gt;</span>
		<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

		<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;group&quot;</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">&quot;password&quot;</span>&gt;</span>Password<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;password&quot;</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;password&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;password&quot;</span> <span class="hljs-attr">required</span> /&gt;</span>
		<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

		<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;submit-container&quot;</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;submit&quot;</span>&gt;</span>Login<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
		<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
	<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>

	<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;actions&quot;</span>&gt;</span>
		<span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;/signup&quot;</span>&gt;</span>Sign Up<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
	<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span></span>

Here, we add method="post" to our form for posting signup data. But how are we going to handle that?

This is where SvelteKit form actions come in handy. In a +page.server.ts file, we can export some actions, allowing us to post data through a form. Here is what it looks like:

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { <span class="hljs-title class_">Actions</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./$types&#x27;</span>;
<span class="hljs-keyword">import</span> { invalid, redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@sveltejs/kit&#x27;</span>;
<span class="hljs-keyword">import</span> { createUser } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;$lib/user.model&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">actions</span>: <span class="hljs-title class_">Actions</span> = {
	<span class="hljs-attr">default</span>: <span class="hljs-keyword">async</span> (event) =&gt; {
		<span class="hljs-keyword">const</span> formData = <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">fromEntries</span>(<span class="hljs-keyword">await</span> event.<span class="hljs-property">request</span>.<span class="hljs-title function_">formData</span>());

		<span class="hljs-comment">// Verify that we have an email and a password</span>
		<span class="hljs-keyword">if</span> (!formData.<span class="hljs-property">email</span> || !formData.<span class="hljs-property">password</span>) {
			<span class="hljs-keyword">return</span> <span class="hljs-title function_">invalid</span>(<span class="hljs-number">400</span>, {
				<span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;Missing email or password&#x27;</span>
			});
		}

		<span class="hljs-keyword">const</span> { email, password } = formData <span class="hljs-keyword">as</span> { <span class="hljs-attr">email</span>: <span class="hljs-built_in">string</span>; <span class="hljs-attr">password</span>: <span class="hljs-built_in">string</span> };

		<span class="hljs-comment">// Create a new user</span>
		<span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> <span class="hljs-title function_">createUser</span>(email, password);

		<span class="hljs-comment">// If there was an error, return an invalid response</span>
		<span class="hljs-keyword">if</span> (error) {
			<span class="hljs-keyword">return</span> <span class="hljs-title function_">invalid</span>(<span class="hljs-number">500</span>, {
				error
			});
		}

		<span class="hljs-comment">// Redirect to the login page</span>
		<span class="hljs-keyword">throw</span> <span class="hljs-title function_">redirect</span>(<span class="hljs-number">302</span>, <span class="hljs-string">&#x27;/login&#x27;</span>);
	}
};

The default action for a form is called default . We could also use named actions in case we have many actions on a page.

What we are doing here is validating the data we get from the form, create a new user, return a validation error in case there's an error, and finally redirecting to the login page if everything is successful.

The code for creating a user can be found here.

In the current state, the signup form works without client side JavaScript. SvelteKit allows making the experience smoother with progressive enhancement. We do that by adding the use:enhance argument to our form element.

With that added, the form will be submitted with client side JavaScript, making it a better user experience.

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript">
	<span class="hljs-keyword">import</span> </span></span><span class="language-javascript">{ enhance }</span><span class="language-xml"><span class="language-javascript"> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;$app/forms&#x27;</span>;
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">use:enhance</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span></span>

Let's also add a few things to make the experience even better. We can display the validation error we returned in our server code on the frontend.

When we return something from a form action, it comes to the frontend as ActionData . We will use that to conditionally errors messages like this:

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript">
	<span class="hljs-keyword">import</span> type </span></span><span class="language-javascript">{ <span class="hljs-title class_">ActionData</span> }</span><span class="language-xml"><span class="language-javascript"> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./$types&#x27;</span>;

	<span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> <span class="hljs-attr">form</span>: <span class="hljs-title class_">ActionData</span>;
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">use:enhance</span>&gt;</span>
	</span><span class="language-javascript">{</span><span class="hljs-keyword">#if</span><span class="language-javascript"> form?.<span class="hljs-property">error</span>}</span><span class="language-xml">
		<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;notice error&quot;</span>&gt;</span>
			</span><span class="language-javascript">{form.<span class="hljs-property">error</span>}</span><span class="language-xml">
		<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
	</span><span class="language-javascript">{</span><span class="hljs-keyword">/if</span><span class="language-javascript">}</span><span class="language-xml">
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span></span>

Note that we are using export let form: ActionData here. The form contains the data. For this to work, it needs to be named form .

Now, we can have this alert when there is an error:

Form error in the frontend

Login

The process for logging in is a bit similar with signing in. We use a form action to authenticate our user, and redirect to the guarded page. The main difference resides in setting up the JWT token as a cookie.

Let's see how it goes:

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript">
	<span class="hljs-keyword">import</span> type </span></span><span class="language-javascript">{ <span class="hljs-title class_">ActionData</span> }</span><span class="language-xml"><span class="language-javascript"> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./$types&#x27;</span>;
	<span class="hljs-keyword">import</span> </span></span><span class="language-javascript">{ enhance }</span><span class="language-xml"><span class="language-javascript"> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;$app/forms&#x27;</span>;

	<span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> <span class="hljs-attr">form</span>: <span class="hljs-title class_">ActionData</span>;
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">svelte:head</span>&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Login<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">svelte:head</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Login<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">use:enhance</span>&gt;</span>
		<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;group&quot;</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">&quot;email&quot;</span>&gt;</span>Email<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;email&quot;</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;email&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;email&quot;</span> <span class="hljs-attr">required</span> /&gt;</span>
		<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

		<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;group&quot;</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">&quot;password&quot;</span>&gt;</span>Password<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;password&quot;</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;password&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;password&quot;</span> <span class="hljs-attr">required</span> /&gt;</span>
		<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

		<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;submit-container&quot;</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;submit&quot;</span>&gt;</span>Login<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
		<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

		</span><span class="language-javascript">{</span><span class="hljs-keyword">#if</span><span class="language-javascript"> form?.<span class="hljs-property">error</span>}</span><span class="language-xml">
			<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;notice error&quot;</span>&gt;</span>
				</span><span class="language-javascript">{form.<span class="hljs-property">error</span>}</span><span class="language-xml">
			<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
		</span><span class="language-javascript">{</span><span class="hljs-keyword">/if</span><span class="language-javascript">}</span><span class="language-xml">
	<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>

	<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;actions&quot;</span>&gt;</span>
		<span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;/signup&quot;</span>&gt;</span>Sign Up<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
	<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span></span>

Now on the server side:

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { <span class="hljs-title class_">PageServerLoad</span>, <span class="hljs-title class_">Actions</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./$types&#x27;</span>;
<span class="hljs-keyword">import</span> { redirect, invalid } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@sveltejs/kit&#x27;</span>;
<span class="hljs-keyword">import</span> { loginUser } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;$lib/user.model&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">load</span>: <span class="hljs-title class_">PageServerLoad</span> = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
	<span class="hljs-keyword">const</span> user = event.<span class="hljs-property">locals</span>.<span class="hljs-property">user</span>;

	<span class="hljs-keyword">if</span> (user) {
		<span class="hljs-keyword">throw</span> <span class="hljs-title function_">redirect</span>(<span class="hljs-number">302</span>, <span class="hljs-string">&#x27;/guarded&#x27;</span>);
	}
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">actions</span>: <span class="hljs-title class_">Actions</span> = {
	<span class="hljs-attr">default</span>: <span class="hljs-keyword">async</span> (event) =&gt; {
		<span class="hljs-keyword">const</span> formData = <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">fromEntries</span>(<span class="hljs-keyword">await</span> event.<span class="hljs-property">request</span>.<span class="hljs-title function_">formData</span>());

		<span class="hljs-keyword">if</span> (!formData.<span class="hljs-property">email</span> || !formData.<span class="hljs-property">password</span>) {
			<span class="hljs-keyword">return</span> <span class="hljs-title function_">invalid</span>(<span class="hljs-number">400</span>, {
				<span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;Missing email or password&#x27;</span>
			});
		}

		<span class="hljs-keyword">const</span> { email, password } = formData <span class="hljs-keyword">as</span> { <span class="hljs-attr">email</span>: <span class="hljs-built_in">string</span>; <span class="hljs-attr">password</span>: <span class="hljs-built_in">string</span> };

		<span class="hljs-keyword">const</span> { error, token } = <span class="hljs-keyword">await</span> <span class="hljs-title function_">loginUser</span>(email, password);

		<span class="hljs-keyword">if</span> (error) {
			<span class="hljs-keyword">return</span> <span class="hljs-title function_">invalid</span>(<span class="hljs-number">401</span>, {
				error
			});
		}

		<span class="hljs-comment">// Set the cookie</span>
		event.<span class="hljs-property">cookies</span>.<span class="hljs-title function_">set</span>(<span class="hljs-string">&#x27;AuthorizationToken&#x27;</span>, <span class="hljs-string">`Bearer <span class="hljs-subst">${token}</span>`</span>, {
			<span class="hljs-attr">httpOnly</span>: <span class="hljs-literal">true</span>,
			<span class="hljs-attr">path</span>: <span class="hljs-string">&#x27;/&#x27;</span>,
			<span class="hljs-attr">secure</span>: <span class="hljs-literal">true</span>,
			<span class="hljs-attr">sameSite</span>: <span class="hljs-string">&#x27;strict&#x27;</span>,
			<span class="hljs-attr">maxAge</span>: <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">24</span> <span class="hljs-comment">// 1 day</span>
		});

		<span class="hljs-keyword">throw</span> <span class="hljs-title function_">redirect</span>(<span class="hljs-number">302</span>, <span class="hljs-string">&#x27;/guarded&#x27;</span>);
	}
};

We set the AuthorizationToken cookie with the JWT token and return a success response.

Note here a few things:

  • The httpOnly option is set to true to prevent the cookie from being accessed by JavaScript. This is a good practice to prevent XSS attacks.

  • We set the secure option to true to ensure that the cookie is only sent over HTTPS.

  • The sameSite option set to strict for preventing the cookie from being sent in cross-site requests.

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

With SvelteKit, there is no need to use third parties cookies packages, because this is handled natively by the framework.

We are also exporting a load function in this file. We're basically redirecting to the guarded page if the user is already logged in. You'll understand more how this part is handled in the hook section.

You can give a look to the loginUser() helper here.

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.

Hooks can be defined on the server side or on the client side, or both if necessary. For this tutorial, we will define our hooks on the server side, since we are doing authentication logic.

The most used hook is the handle hook, which simply take a request and return a response.

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

<span class="hljs-comment">// /src/hooks.ts</span>
<span class="hljs-keyword">const</span> <span class="hljs-attr">handle</span>: <span class="hljs-title class_">Handle</span> = <span class="hljs-keyword">async</span> ({ event, resolve }) =&gt; {
  <span class="hljs-keyword">const</span> { headers } = event.<span class="hljs-property">request</span>;
  <span class="hljs-keyword">const</span> cookies = <span class="hljs-title function_">parse</span>(headers.<span class="hljs-title function_">get</span>(<span class="hljs-string">&quot;cookie&quot;</span>) ?? <span class="hljs-string">&quot;&quot;</span>);

  <span class="hljs-keyword">if</span> (cookies.<span class="hljs-property">AuthorizationToken</span>) {
    <span class="hljs-comment">// Remove Bearer prefix</span>
    <span class="hljs-keyword">const</span> token = cookies.<span class="hljs-property">AuthorizationToken</span>.<span class="hljs-title function_">split</span>(<span class="hljs-string">&quot; &quot;</span>)[<span class="hljs-number">1</span>];

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> jwtUser = jwt.<span class="hljs-title function_">verify</span>(token, <span class="hljs-keyword">import</span>.<span class="hljs-property">meta</span>.<span class="hljs-property">env</span>.<span class="hljs-property">VITE_JWT_ACCESS_SECRET</span>);
      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> jwtUser === <span class="hljs-string">&quot;string&quot;</span>) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">&quot;Something went wrong&quot;</span>);
      }

      <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> db.<span class="hljs-property">user</span>.<span class="hljs-title function_">findUnique</span>({
        <span class="hljs-attr">where</span>: {
          <span class="hljs-attr">id</span>: jwtUser.<span class="hljs-property">id</span>,
        },
      });

      <span class="hljs-keyword">if</span> (!user) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">&quot;User not found&quot;</span>);
      }

      <span class="hljs-keyword">const</span> <span class="hljs-attr">sessionUser</span>: <span class="hljs-title class_">SessionUser</span> = {
        <span class="hljs-attr">id</span>: user.<span class="hljs-property">id</span>,
        <span class="hljs-attr">email</span>: user.<span class="hljs-property">email</span>,
      };

      event.<span class="hljs-property">locals</span>.<span class="hljs-property">user</span> = sessionUser;
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(error);
    }
  }

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-title function_">resolve</span>(event);
};

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

  • We check if there is a AuthorizationToken cookie.

  • In case there is one, we extract the JWT token from it.

  • We verify the token using the jwt.verify function.

  • If there is any error, we simply throw it and return the response from the route.

  • If the token is valid, we get the user from the database and save it in the event.locals object.

With that in place, for every requests that come to our application, we either save or not the user ID in the locals . This gives us enough flexibility to handle authorization and necessary redirects per route.

Protecting guarded routes

For our protected routes, we will make use of SvelteKit load function. Since it runs before the component is rendered, we can use it to get the logged-in user from the locals , and either displaying the component or redirecting the user to the login page.

Our load function will be implemented in the +page.server.ts file of our guarded route.

&lt;!-- <span class="hljs-regexp">/src/</span>routes/guarded.<span class="hljs-property">svelte</span> --&gt;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">&quot;ts&quot;</span> <span class="hljs-attr">context</span>=<span class="hljs-string">&quot;module&quot;</span>&gt;</span><span class="language-javascript">
	<span class="hljs-keyword">import</span> type { <span class="hljs-title class_">Load</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@sveltejs/kit&#x27;</span>;
	<span class="hljs-keyword">import</span> type { <span class="hljs-title class_">SessionUser</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;src/hooks&#x27;</span>;

	<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">load</span>: <span class="hljs-title class_">Load</span> = <span class="hljs-function">(<span class="hljs-params">{ session }</span>) =&gt;</span> {
		<span class="hljs-keyword">const</span> user = session.<span class="hljs-property">user</span>;

		<span class="hljs-keyword">if</span> (!user) {
			<span class="hljs-keyword">return</span> {
				<span class="hljs-attr">status</span>: <span class="hljs-number">302</span>,
				<span class="hljs-attr">message</span>: <span class="hljs-string">&#x27;You must be logged in to view this page&#x27;</span>,
				<span class="hljs-attr">redirect</span>: <span class="hljs-string">&#x27;/login&#x27;</span>
			};
		}

		<span class="hljs-keyword">return</span> {
			<span class="hljs-attr">props</span>: {
				<span class="hljs-attr">user</span>: session.<span class="hljs-property">user</span>
			}
		};
	};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript">
	<span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> <span class="hljs-attr">user</span>: <span class="hljs-title class_">SessionUser</span>;
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">svelte:head</span>&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Guarded page<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">svelte:head</span>&gt;</span></span>

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Guarded page<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span></span>
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This page is guarded and will only be accessible to authenticated users.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello {user.email}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>

When there is no user in the locals, we throw a 401 error, else we return the user to be used on the frontend.

In case there is an error, we will then have this when trying to access the guarded route:

Unauthorized error page

This is a custom error page that was generated from this error template. You can read more about custom error page here.

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.