In this post, we look into how we can build our own Authentication and Authorization solution for SvelteKit. We will store and hash user credentials, create sessions with Cookies, and make the session info available to the app through hooks and locals.
Of course there are many off the shelf solutions to do authentication and authorization in SvelteKit. But I think it makes sense sometimes to build an own implementation of something to learn how it works and to understand the underlying concepts.
I do want to point out, that storing credentials yourself leads to great risks. Although we salt and hash the passwords, any sensible information you store comes with the burden of protecting it. You need to make sure that no data leaks to the outside and your systems are always patched. You won’t have that problem with a third-party service like Social-Logins, Auth0, Supabase or Firebase etc.
I use SvelteKit with TypeScript and highly recommend you do this as well. We can type our data shapes in the backend and later reuse them in the frontend to safely access the data in the UI and make our lives easier in case of type changes.
I combined SvelteKit with SQLite as a database as it is lightweight and easy to maintain and fast. You can read more about SvelteKit + SQLite in my previous post. I will also expand the app from that post to include authentication and authorization. As a node SQLite driver I use better-sqlite3 as I like its synchronous API.
We start off by creating a small table to store our users. We will use the username as the primary key and store the password as a salted and hashed string. We also store the roles of users to differentiate between privileges.
Now we can add a method that creates a new user in the database. We will use the bcrypt library to hash the password as it is terrible to store plain text passwords. A hashing algorithm is a one-way function that transforms the input into a fixed-size cryptographic output. If we later look at the hashes, we can’t tell what the original password was. When the user logs in, we can hash the password they entered and compare it to the stored hash. If they match, we know the user entered the correct password.1
We also want to add a salt before we hash. It is a random value added to the password to ensure that even if multiple users have the same password, the hashed values will be different.2 We set the saltRound to 12 which takes some computing time (~1 sec) which makes brute forcing harder.
The password hashing library bcrypt also offers a function to compare a plain text password with a hashed one. We can use this to validate the credentials. Note that when the user enters an invalid username, we do a hash to “waste” some time. This makes brute forcing harder.
We need to call this function in our form handler for the login button. When the credentials are valid, we call the function performLogin which creates a session. In there, we define a maximum session length of 30 days. We call a createSession function which we will implement later that returns an ID. We store this ID in a cookie with cookies.set.
Now that we stored the session ID in a cookie, we can check if the user is logged in. We also want to store some information about the session, like the username and his roles. For this, we will create a sessionStore module. We will use a simple in-memory store for now. In a big production app you might want to use a cache like Redis.
The in-memory store is just a Map of the type SessionInfo. The key will be the session ID also stored in the user cookie.
In the createSession function we start by creating a random session ID. For that, we use the randomBytes function from node’s internal crybto module. Now we can fetch all the user info we want to store in our session map. In our case, we already get the username from the login form and only need to query his roles from the users table.
We also need to expire the session server-side. For that, we create a small function that loops over all sessions and deletes the ones that are expired. We call this function every hour when a session is created. We do this in a setTimeout to not block the server.
Now that the users have a session cookie, and we have a store that records which session belongs to which user, we might want to use this info in some of our components.
To make this work globally in our app, we can use hooks and locals. We can define our hook in a file called hooks.server.ts in the src folder.
In there, we grab the session ID from the cookie, and get the session data for it. We then store the username and roles in the event.locals object. This object is available in all components and pages.
For the getSession function, we just return the data of our map if the session is still valid. If the session is invalid, we delete it from the map and return undefined and delete the cookie in the function above.
We also to define the types for the event.locals object. We do this in a file called types.d.ts in the src folder.
The most obvious thing to do is to greet the user in the header so he knows he is logged in. As the header sits in our global +layout.svelte file, we first need to add a +layout.server.ts next to it. In there, we can access the locals and return the username in the load function.
We can then just display the username in the header like with any other value from the load function.
As we defined user-roles in our table, we might want to lock some pages behind a role. We can do this by using the load function of the page. In there we can check if the user has the required role and return an error if not.
You can of course also check if the username local is empty to only check if the user is logged in. And you can reuse this check in any form or API action handler to check if the user is allowed to do something.
Like we did with the header component, we can also hide specific regions of a page with an if block when we return something like the username or an isAdmin boolean from the load function. But make sure to never return sensitive data from the load function when the user is not supposed to see it.
To log out, we just need to delete the cookie and redirect the user to the home page. We don’t even need a .svelte file in the logout folder.
We can link to this page from our header. Note that I added the data-sveltekit-preload-data and data-sveltekit-reload attributes to the link.
The first tells SvelteKit to not preload the data for this link. This can improve loading times for data rich pages, but would result in a logout if the user hovers over the button for the logout page.
The second tells SvelteKit to reload the page after the link is clicked. This is needed because svelte won’t do the redirect when the user is already on the home page, but we want to refresh the header to again show the login button.
Unfortunately, the only in-memory solution of the session store can cause some trouble while developing the app. The server seems to restart occasionally, resulting in the session store to be reset, and you being logged out.
This was frustrating me a lot so I additionally store the session data in a database table. This way, the session is persisted and the user will stay logged in even if the server restarts.
This is the table:
We need to add the following database functions:
We also need to add these types:
And at last, we always need to handle the DB next to our in-memory session store:
With that, we are done and implemented a simple login system from scratch (except the hashing). You can check the full code in the GitHub repo.
I hope you liked this tutorial and learned something new. If you need any clarification or have suggestions, please leave a comment below.