DEV Community

Cover image for Protecting sveltekit routes from unauthenticated users
Thiago David
Thiago David

Posted on

Protecting sveltekit routes from unauthenticated users

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
Enter fullscreen mode Exit fullscreen mode

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:

file structure of the routes

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.

file structure of the store folder

Inside user.js, add the following code:

// src/stores/user.js
import { writable } from "svelte/store";

export const user = writable({
  email: "",
  name: "",
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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, '/')
  }

}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 />

Enter fullscreen mode Exit fullscreen mode

and the last one, the +layout.server.js:

// routes/+layout.server.js
export const load = async ({ locals }) => {
    return {
        user: locals.user,
    }
}
Enter fullscreen mode Exit fullscreen mode

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 (1)

Collapse
 
ernanej profile image
Ernane Ferreira

Awesome article 💎