While developing a SvelteKit app, I found myself contemplating the intricacies of authentication, specifically regarding the proper storage of user session data and securing access to private routes. In my case, I was working with a Rails API handling user registration and authentication through Devise JWT. I opted not to use any ORMs (such as Prisma or Sequelize).
As I delved into the available resources, I came across articles detailing route protection using cookies and hooks. However, adapting their code to meet my specific needs proved challenging. My requirements included integrating a Rails API for user management, avoiding the use of ORMs, and ensuring secure handling of sensitive data.
In this article, I'll guide you through handling user session data and implementing routing protection in such a scenario.
Understanding HTTP Cookies
HTTP Cookies are familiar components for anyone who has traversed websites. Essentially, cookies are small files stored in browsers, typically containing information like authentication tokens or session data. To enhance security, we prefer not to store sensitive data directly in cookies. This concern led to the introduction of HTTPOnly Cookies, which ensures that the server receives the cookie with each specific request, while JavaScript remains unable to access it.
Unpacking SvelteKit Hooks
SvelteKit Hooks, according to the documentation, are app-wide functions that you declare, and SvelteKit calls them in response to specific events. This grants you fine-grained control over the framework's behavior. Leveraging hooks allows us to exercise control over each request passing through our application, enabling us to capture the pathname of the URL the user is attempting to access. This capability proves crucial for redirecting unauthenticated users.
Implementation Steps
Before diving into the implementation details, I assume you already have an external API application, like a Rails or Node.js server, up and running. This allows us to focus more on the SvelteKit implementation. Let's proceed by creating a new SvelteKit app using npm.
npm create svelte@latest sveltekit-auth
cd sveltekit-auth
npm install
Now that we have the initial structure in place, let's create some basic forms for testing authentication. We'll create three new routes: signin, signup, and logout.
on the signin and signup i will create two files.
- +page.svelte
- +page.server.js
and on the logout i will only create one file, the +page.server.js
.
the structure it will be like that:
Before delving into the code for these files, let's create a SvelteKit store to maintain user data in runtime memory. Inside the src/lib
folder, create a store folder, and within it, a file named user.js
.
Inside user.js
, add the following code:
// src/stores/user.js
import { writable } from "svelte/store";
export const user = writable({
email: "",
name: "",
})
This store provides a global object to access and manipulate user data.
Next, let's create basic forms for signup and signin. These forms are simple for testing purposes and lack styling for simplicity.
<!-- signup/+page.svelte -->
<h1>This is the Signup page</h1>
<form action="/signup" method="POST">
<input type="text" name="name" placeholder="Name">
<input type="text" name="email" placeholder="email" />
<input type="password" name="password" placeholder="Password" />
<button type="submit">Sign up</button>
</form>
signin page:
<!-- signin/+page.svelte -->
<h1>This is the signin page</h1>
<form action="/signin" method="POST">
<input type="text" name="email" placeholder="email" />
<input type="password" name="password" placeholder="Password" />
<button type="submit">Sign in</button>
</form>
Notice that we retained the initial +page.svelte in the root route (http://localhost:5173/). Why? We'll treat it as a "private" page that doesn't accept access from unauthenticated users. In this file, we'll set up the following code:
<!-- routes/+page.svelte -->
<h1>This will be our protected page</h1>
<form action="/logout" method="post">
<button>Logout</button>
</form>
Now, let's set up the server files for each route. For the signup route, create +page.server.js
:
// signup/+page.server.js
import { user } from '$lib/stores/user.js'
import { redirect } from '@sveltejs/kit'
//create a new action called default passing the cookies, request and fetch as parameters
export const actions = {
default: async ({ cookies, request, fetch}) => {
//retrieve the form data from the request and set it to the variables
const data = await request.formData()
const email = data.get('email')
const password = data.get('password')
const name = data.get('name')
//check if the variables are valid and if not, return an error
if (
typeof email !== 'string' ||
typeof password !== 'string' ||
!email ||
!password
) {
return {
status: 400,
body: {
success: false,
}
}
}
//send the data to the backend API (use your endpoint in this case)
const response = await fetch('http://localhost:3000/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user: {
email,
password,
name
}
})
})
//if the response is ok, set the cookies and redirect to the homepage
if (response.ok) {
const data = await response.json()
//setting the cookies of user and jwt
cookies.set('user', JSON.stringify(data.user))
cookies.set('jwt', response.headers.get('Authorization'))
let obj = {
...data,
jwt: response.headers.get('Authorization')
}
user.set(obj)
//redirect user to the protected page
throw redirect(302, '/')
} else {
//if the response is not ok, return the errors
const { errors } = await response.json()
return {
status: 400,
body: {
success: false,
errors
}
}
}
}
}
For the signin route, create +page.server.js
:
// signin/+page.server.js
import { user } from '$lib/stores/user.js'
import { redirect } from '@sveltejs/kit'
export const actions = {
default: async ({ cookies, request, fetch}) => {
const data = await request.formData()
const email = data.get('email')
const password = data.get('password')
if (
typeof email !== 'string' ||
typeof password !== 'string' ||
!email ||
!password
) {
return {
status: 400,
body: {
success: false,
}
}
}
const response = await fetch('http://localhost:3000/users/sign_in', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user: {
email,
password
}
})
})
if (response.ok) {
const data = await response.json()
cookies.set('user', JSON.stringify(data.user))
cookies.set('jwt', response.headers.get('Authorization'))
let obj = {
...data,
jwt: response.headers.get('Authorization')
}
user.set(obj)
throw redirect(302, '/')
} else {
const { errors } = await response.json()
return {
status: 400,
body: {
success: false,
errors
}
}
}
}
}
Finally, for the logout route, create +page.server.js
:
// logout/+page.server.js
import { redirect } from '@sveltejs/kit'
export const actions = {
default: async ({ cookies}) => {
//set the cookies to null and redirect
cookies.set('user', null)
throw redirect(302, '/')
}
}
Now, let's implement the hook. Create a new file, hooks.server.js
, in the src folder.
// src/+hooks.server.js
import { redirect } from "@sveltejs/kit";
// define the routes of we want to be possible to access without auth
const public_paths = [
'/signup',
'/signin'
];
// function to verify if the request path is inside the public_paths array
function isPathAllowed(path) {
return public_paths.some(allowedPath =>
path === allowedPath || path.startsWith(allowedPath + '/')
);
}
export const handle = async ({ event, resolve}) => {
let user = null
// check if the cookie exist, and if exists, parse it to the user variable
if(event.cookies.get('user') != undefined && event.cookies.get('user') != null){
user = JSON.parse(event.cookies.get('user'))
}
const url = new URL(event.request.url);
// validate the user existence and if the path is acceesible
if (!user && !isPathAllowed(url.pathname)) {
throw redirect(302, '/signin');
}
if(user){
//set the user to the locals (i explain this later on the article)
event.locals.user = user
// redirect user if he is already logged if he try to access signin or signup
if(url.pathname == '/signup' || url.pathname == '/signin'){
throw redirect(302, '/')
}
}
const response = await resolve(event)
return response
}
This hook captures every request passing through the application. Using the handle function, we access the cookies set previously, determine whether the user should be redirected, and set the user store again. Note the use of event.locals to pass user data through server-side functions with the request.
On the routes
folder, create the follow files
- +layout.svelte
- +layout.server.js
on the +layout.svelte
set the code:
<!-- routes/+layout.svelte -->
<script>
import { user } from '$lib/stores/user.js'
export let data
$user = data.user
</script>
<!-- i will put that for it can be possible to see the user data consistance -->
<h1>User info :</h1>
<pre>{JSON.stringify($user, null, 2)}</pre>
<slot />
and the last one, the +layout.server.js
:
// routes/+layout.server.js
export const load = async ({ locals }) => {
return {
user: locals.user,
}
}
Now, you can test the implementation. Attempting to access the protected route (/) without signing in will redirect you to the signin page.
Conclusion
In this article, I aimed to simplify the understanding of authentication flow and the use of hooks in SvelteKit. Having encountered some difficulties myself in grasping these features, I hope this guide proves helpful for your understanding as well.
Top comments (2)
Thanks for these amazing artical. I got to learn lot of things.
Awesome article 💎