Jannik Wempe Fullstack Software Engineer passionate about serverless and modern frontend. Blogging at blog.jannikwempe.com and tweeting as @JannikWempe.

Authentication in Svelte using cookies

11 min read 3109

Authentication in SvelteKit Using Cookies

Authentication is an essential part of any application that makes certain functions available only to certain users. The two main methods for authentication in web applications are cookies and tokens (mostly JSON Web Tokens (JWTs)). In this tutorial, we will create a Svelte app using SvelteKit that implements a cookie for authentication.

The app provides the basic functions you need related to authentication: sign in, sign up, sign out, and access to some user data in the frontend. This is what the app will look like:

SvelteKit Authentication Demo

And here’s our table of contents:

First of all, let’s start with some basics before getting to the fun part.

Introduction of SvelteKit

One last thing before we are getting started with actual coding. What is SvelteKit? How is it different from Svelte?

You could think of SvelteKit being for Svelte what Next.js is for React. It is a framework on top of a framework — a meta-framework.

SvelteKit is a framework for building web applications of all sizes, with a beautiful development experience and flexible file system–based routing.

SvelteKit extends Svelte with some functionality that we will use in this tutorial: file system–based routing, endpoints (server-side functions), and hooks.

Implementing authentication in Svelte with SvelteKit

Okay, now let’s go ahead and build this. All the code is also available on GitHub.



Setup

First of all, we initialize the SvelteKit project. For this tutorial, we’ll go ahead with JavaScript instead of TypeScript:

npm init [email protected] sveltekit-auth
# ✔ Which Svelte app template? › Skeleton project
# ✔ Use TypeScript? … No
# ✔ Add ESLint for code linting? … Yes
# ✔ Add Prettier for code formatting? … Yes
cd sveltekit-auth
npm install

Let’s add Tailwind for some basic styling. We also use the Tailwind forms plugin, which provides some basic styling for our forms:

npx [email protected] tailwindcss
npm i @tailwindcss/forms

In order to use the Tailwind forms plugin, we have to add it to the plugins in the tailwind.config.cjs, which was created by the svelte-add command:

// tailwind.config.cjs
const config = {
    mode: 'jit',
    purge: ['./src/**/*.{html,js,svelte,ts}'],

    theme: {
     extend: {},
    },

    plugins: [
    require('@tailwindcss/forms')
  ],
};

module.exports = config;

That’s it for the very basic setup. Let’s build the UI next before we get to the actual authentication.

Build the UI

Let us create the forms for signing up and signing in first. Create /src/lib/components/SignInForm.svelte:

// src/lib/components/SignInForm.svelte
<script>
  import Input from '$lib/components/Input.svelte';
  import Button from '$lib/components/Button.svelte';

  import { createEventDispatcher } from 'svelte';

  let email = '';
  let password = '';

  const dispatch = createEventDispatcher();

  function submit() {
    dispatch('submit', {
    email,
    password
    })
  }
</script>

<form on:submit|preventDefault={submit} class='space-y-5 {$$props.class}'>
  <Input label='Email' id='email' name='email' type='email' bind:value={email} required />
  <Input label='Password' id='password' name='password' type='password' bind:value={password} required />
  <Button type='submit'>Sign In</Button>
</form>

Here we have a form with an email and password input. The component dispatches a submit event when the user submits the form. With {$$props.class} in the form’s class attribute, we allow passing in Tailwind classes from outside. I use this mainly for positioning a component from the outside. The component itself should not have a margin or something similar on its container.

N.B., you can find the code for Button and Input in the GitHub repo.


More great articles from LogRocket:


And it is basically the same for SignUpForm.svelte:

// src/lib/components/SignUpForm.svelte
<script>
  import Input from '$lib/components/Input.svelte';
  import Button from '$lib/components/Button.svelte';

  import { createEventDispatcher } from 'svelte';

  let email = '';
  let password = '';
  let confirmPassword = '';
  let error;
  let confirmPasswordInputRef;

  const dispatch = createEventDispatcher();

  function submit() {
    error = null;
    if (password !== confirmPassword) {
    error = "Passwords do not match.";
    confirmPasswordInputRef.focus();
    return;
    }

    dispatch('submit', {
    email,
    password
    })
  }
</script>

<form on:submit|preventDefault={submit} class='space-y-5 {$$props.class}'>
  <Input label='Email' id='email' name='email' type='email' bind:value={email} />
  <Input label='Password' id='password' name='password' type='password' bind:value={password} />
  <Input label='Confirm Password' id='confirm-password' name='confirm-password' type='password' bind:value={confirmPassword} bind:inputRef={confirmPasswordInputRef} />
  {#if error}
    <p class='text-red-600 text-sm font-semibold'>{error}</p>
  {/if}
  <Button type='submit'>Sign Up</Button>
</form>

In this case, we have an additional input in order to verify that the user entered the intended password. If the user entered different passwords, we show an error and set the focus back to the input.

Okay, let’s now use these forms on actual pages. You can create a route using SvelteKit by creating a .svelte file in the src/routes folder. The name of the file will match the route that will be created. We create our /sign-up route by creating src/routes/sign-up.svelte:

// src/routes/sign-up.svelte
<script>
  import SignUpForm from '$lib/components/SignUpForm.svelte';

  let error;

  async function handleSubmit({detail: {email, password}}) {
    const response = await fetch('/api/sign-up', {
    method: 'POST',
    body: JSON.stringify({email, password}),
    headers: {
        'Content-Type': 'application/json'
    }
    });

    if (!response.ok) {
    error = (await response.json()).message;
    return;
    }

    window.location = '/protected';
  }
</script>

<h1 class='text-2xl font-semibold text-center'>Sign Up</h1>
{#if error}
  <p class='mt-3 text-red-500 text-center font-semibold'>{error}</p>
{/if}
<SignUpForm class='max-w-xl mx-auto mt-8' on:submit={handleSubmit}/>

Here, we use our SignUpForm and handle the dispatched submit event. If the user submits the form, we send a POST request containing the email and the password in the body to /api/sign-up, which we’ll create in a bit. If the server responds with a success status (2xx) we’ll navigate the user to the /protected route, which we’ll also create later. Otherwise, we render an error.

N.B., SvelteKit provides its own function for client-side navigation: goto. But in this case, it didn’t work for me. The user would have to refresh the page in order to be logged in. A simple window.location = '/protected' does its job here.

The /sign-in looks exactly the same with the only differences being the used form and the endpoint where we send the request:

// src/routes/sign-in.svelte
<script>
  import SignInForm from '$lib/components/SignInForm.svelte';

  let error;

  async function handleSubmit({detail: {email, password}}) {
    const response = await fetch('/api/sign-in', {
    method: 'POST',
    body: JSON.stringify({ email, password }),
    headers: {
        'Content-Type': 'application/json'
    }
    });

    if (!response.ok) {
    error = (await response.json()).message;
    return;
    }

    window.location = '/protected';
  }
</script>

<h1 class='text-2xl font-semibold text-center'>Sign In</h1>
{#if error}
  <p class='mt-3 text-red-500 text-center font-semibold'>{error}</p>
{/if}
<SignInForm class='max-w-xl mx-auto mt-8' on:submit={handleSubmit} />

There are also a layout component, an index page, and a navigation component, which I won’t go into detail about here. They are not required to understand how to implement authentication in Svelte. You can look them up in the GitHub repo.

Let’s create the endpoints for the authentication.

Authentication endpoints

A user has to sign up first. Therefore, we’ll go ahead and create the /sign-up endpoint where we send a request when a user submits the sign-up form. You have to create a .js (or .ts) file in src/routes in order to create an endpoint in SvelteKit. We create our JavaScript files in the api subfolder, which creates routes beginning with /api/. First of all, we need two additional libraries for our endpoints:

npm i cookie uuid

Let me introduce you to our in-memory database for storing the user data and the sessions:

// src/routes/api/_db.js
import { v4 as uuidv4 } from 'uuid';

const users = [
    {
     email: '[email protected]',
     // ⚠️ CAUTION: Do not store a plain password like this. Use proper hashing and salting.
     password: 'thisisnotsecret',
    },
];

let sessions = [];

export const getUserByEmail = async (email) => {
    const existingUser = users.find((user) => user.email === email);
    if (!existingUser) return Promise.resolve(null);
    return Promise.resolve(existingUser);
};

export const registerUser = (user) => {
    const existingUser = users.find((u) => u.email === user.email);
    if (!!existingUser) return Promise.reject(new Error('User already exists'));
    users.push(user);
    return Promise.resolve(user);
};

export const createSession = (email) => {
    const session = {
     id: uuidv4(),
     email,
    };
    sessions.push(session);
    return Promise.resolve(session);
};

export const getSession = (id) => {
    const session = sessions.find((session) => session.id === id);
    if (!session) return Promise.resolve(null);
    return Promise.resolve(session);
};

export const removeSession = (id) => {
    const session = sessions.find((session) => session.id === id);
    if (!session) return Promise.reject(new Error('Session not found'));
    sessions = sessions.filter((session) => session.id !== id);
    return Promise.resolve(session);
};

Notice the filename which is prefixed with _, which indicates that this is not an endpoint but a normal JS file.

The users and sessions arrays act as our in-memory databases. The data will get lost if the application is restarted. This is fine for this tutorial, but in the real world, you would connect to a real database or use a SaaS like Supabase.

All the other functions in this file just act as an API for our data in users and sessions. We store the user data in users and the session data consisting of the session_id and email in sessions.

Now let’s get to our first actual endpoint:

// src/routes/api/sign-up.js
import { createSession, getUserByEmail, registerUser } from './_db';
import { serialize } from 'cookie';

/** @type {import('@sveltejs/kit').RequestHandler} */
export async function post({ body: { email, password } }) {
    const user = await getUserByEmail(email);

    if (user) {
     return {
         status: 409,
         body: {
             message: 'User already exists',
         },
     };
    }

    // ⚠️ CAUTION: Do not store a plain password like this. Use proper hashing and salting.
    await registerUser({
     email,
     password,
    });

    const { id } = await createSession(email);
    return {
     status: 201,
     headers: {
         'Set-Cookie': serialize('session_id', id, {
             path: '/',
             httpOnly: true,
                sameSite: 'strict',
             secure: process.env.NODE_ENV === 'production',
             maxAge: 60 * 60 * 24 * 7, // one week
         }),
     },
     body: {
         message: 'Successfully signed up',
     },
    };
}

By exporting a function named post, SvelteKit will use this function for post requests (you could also export get, etc.). We check if a user with the given email already exists. If that’s not the case, we do register the new user and create a session.

The interesting and important part happens in the headers section of the response. We set the actual cookie containing the session_id. That ID will be sent automatically by the client with the subsequent requests. We can look up the session ID in our sessions “database.”

This is a quick overview of the cookie options we pass to the serialize function, which returns the string to describe the cookie. You can read about details on MDN’s Using HTTP cookies:

  • path: defines for which paths the cookie will be sent alongside the request. By setting the path to /, the cookie will be sent alongside each request (also our requests to /api/**)
  • httpOnly: prevents JS in the client to access that cookie. document.cookie will not contain that cookie. This is a security setting and should be your default. Without setting it to true, malicious JS (also from browser extensions) could read that session_id and send it somewhere and be logged in with your session
  • sameSite = strict sets the Same-Site attribute in the cookie. By setting it to strict, the cookie is only sent to the site where it originated. It prevents CSRF.
  • secure is another security feature. By setting secure = true, the client will only send the cookie alongside the request if https:// is used. It prevents an attacker from using a man-in-the-middle attack reading the cookie while being sent
  • max-age sets the Max-Age attribute in the cookie. If the specified duration is over (one week in our example), the client (= the browser) will not send the cookie alongside the request anymore and deletes it. This makes the cookie a “permanent” cookie rather than a “session” cookie. Remember the common Keep me logged in checkbox? By enabling that checkbox, the server will set the Max-Age (or Expires) attribute in the cookie. The user will keep the session and therefore stay logged in for one week

Sidenote ℹ️: It is questionable whether sending the customer the information that an email is already in use is a good idea. A potential attacker could use the information about existing email addresses. Nevertheless, companies like Twitter and Facebook are also sending the information to the client (not saying that these should be the gold standard for data privacy and security).

Okay, with understanding how the sign-up works, the sign-in and sign-out are easy to understand. This is how the /api/sign-in handler looks:

// src/routes/api/sign-in.js
import { createSession, getUserByEmail } from './_db';
import { serialize } from 'cookie';

/** @type {import('@sveltejs/kit').RequestHandler} */
export async function post({ body: { email, password } }) {
    const user = await getUserByEmail(email);

    // ⚠️ CAUTION: Do not store a plain passwords. Use proper hashing and salting.
    if (!user || user.password !== password) {
     return {
         status: 401,
         body: {
             message: 'Incorrect user or password',
         },
     };
    }

    const { id } = await createSession(email);
    return {
     status: 200,
     headers: {
         'Set-Cookie': serialize('session_id', id, {
             path: '/',
             httpOnly: true,
             sameSite: 'strict',
             secure: process.env.NODE_ENV === 'production',
             maxAge: 60 * 60 * 24 * 7, // one week
         }),
     },
     body: {
         message: 'Successfully signed in',
     },
    };
}

It is essentially the same, but this time we only look up an existing user rather than also creating a new one. We again create a session and send the cookie containing the session_id to the client.

In the /api/sign-out, handler we use a GET request because the client doesn’t have to actively send any data (remember, the cookie will be sent automatically). We remove the session from our in-memory database and remove the cookie by unsettling the value and setting an immediate expiration date:

// src/routes/api/sign-out.js
import { removeSession } from './_db';
import { parse, serialize } from 'cookie';

/** @type {import('@sveltejs/kit').RequestHandler} */
export async function get({ headers: { cookie } }) {
    const cookies = parse(cookie || '');

    if (cookies.session_id) {
     await removeSession(cookies.session_id);
    }

    return {
     status: 200,
     headers: {
         'Set-Cookie': serialize('session_id', '', {
             path: '/',
             expires: new Date(0),
         }),
     },
    };
}

That is it for our endpoints. But how is the client able to access any session data? We did not send anything other than the session_id to the client. That is what we will be looking at next.

Svelte hooks

Hooks are a special thing in SvelteKit. Hooks run on the server and allow us to extend the behavior of SvelteKit.

The handle hook runs on every request (and during prerendering). It gives us access to the request and allows us to modify the response. We can add custom data to request.locals, which will be available in all endpoints. We will use it to parse the session_id cookie, retrieve the session, and attach the session data to request.locals.

But that doesn’t make the session accessible by the client. This is where another hook comes into play: getSession. Whatever we return from getSession will be available in a session Svelte store in the frontend. Make sure to not return sensitive data (like the password) here.

You could add authorization functionality by adding something like a permissions array to the user object returned from getSession. You could check these permissions in the frontend and allow the user only to do certain things based on the permissions.

This is how we implement the hooks:

// src/hooks.js
import { parse } from 'cookie';
import { getSession as getSessionFromApi } from './routes/api/_db';

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, resolve }) {
    const cookies = parse(request.headers.cookie || '');

    if (cookies.session_id) {
     const session = await getSessionFromApi(cookies.session_id);
     if (session) {
         request.locals.user = { email: session.email };
         return resolve(request);
     }
    }

    request.locals.user = null;
    return resolve(request);
}

/** @type {import('@sveltejs/kit').GetSession} */
export function getSession(request) {
    return request?.locals?.user
     ? {
             user: {
                 email: request.locals.user.email,
             },
       }
     : {};
}

This way, the user object containing the users’ email will be accessible in the frontend. That is what we will be looking at next.

Securing routes and accessing the session on the client

Back to the frontend. Let us now use the user object in the session. We now create another route that will only be accessible by authenticated users.

Pages and layouts have access to a special method called load. The method has to be written in the <script context="module"> block since it runs before the component is rendered. It runs on the client and on the server during server-side rendering. The load function gives us access to the session (and several other things we don’t need here).

We can check if the session contains the user. If that’s not the case, the user isn’t signed in. We can redirect the user by returning the combination of HTTP status code 302 (Found) and a redirect pointing to the route where the user should be redirected. Because the load function is running before the actual rendering of the page, an unauthenticated user will never see the page. (You can try it by navigating to /protected in the finished demo.)

By returning the user in the props object from the load function, we can access user as a prop in the component instance. There is an alternative way to access user in the session since the load function is only available on pages and layouts. You can access the session via the session store provided by SvelteKit. (This is used in the Navigation component).

This is how a protected route looks:

// src/routes/protected.svelte
<script context="module">
  export async function load({ session }) {
    if (!session?.user) {
    return {
        status: 302,
        redirect: "/sign-in"
    }
    }
    return {
    props: {
        user: session.user
    }
    };
  }
</script>

<script>
  export let user;


  // import { session } from '$app/stores';
  // $session.user;
</script>

<h1 class='text-2xl font-semibold text-center'>Hi! You are registered with email {user.email}.</h1>

That is it. We checked all the boxes and have an app with working authentication.

Conclusion

SvelteKit helps a lot by providing us the tools we need to create a nice user flow for authentication. We can easily create endpoints for the logic; hooks for parsing, checking, and providing the session; and in the frontend, we can access the session data either in the load function or through the provided session store. You have full control and can easily extend and change the functionality.

Make sure to play around with the demo (GitHub repo). Or — even better — try to recreate it yourself.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Jannik Wempe Fullstack Software Engineer passionate about serverless and modern frontend. Blogging at blog.jannikwempe.com and tweeting as @JannikWempe.

11 Replies to “Authentication in Svelte using cookies”

  1. Any luck getting any of the salting/hashing libraries ie bcrypt or argon2 with Sveltekit and vercel? works in dev but can’t get any hashing library working in prod with the adapter.

    1. Are they ESM modules? I have problem using CommonJS modules like ‘pg’ with Vercel or Netlify but the adapter for Node.js works for me. Another option might be to let the database handle the encryption?

  2. I’m not that good in JS and such yet but why are you using Promise.. in your unctions? You’re using a feww awaits but not promsies for checking if something is resolved, pending or such. Coudln’t you jsut replace them with true/false instead?

    1. Can you point me to an exact place in the code that isn’t clear to you?

      In general await is just syntactic sugar in JS. It allows you to write your code like it would be synchronous. With await you are literally awaiting the promise. It can be used instead of .then(). And it allows you to surround it with try-catch instead of using .catch()

      In src/routes/api/_db.js I just add promise to mimic asynchronous calls to a DB or other services.

  3. goto() won’t work either for me. Another solution would be to set the session via ‘$app/stores’ right after successfully logged in, then you can call goto function. User object should matched the same one loaded from getSession btw. What do you think about this approach?

    1. That might work.

      I plan on updating the repo to the latest SvelteKit version in a bit and I’ll have a look at that redirect part again. I’ll give an update here if it’s done.

      1. In my app, I found that goto works but does not cause the session to be reevaluated. I ended up doing that manually in my app

        const rs = await fetch(“api/auth”, { method: “POST”, body, headers });
        if (rs.ok) {
        const user = await rs.json();
        // Load user info into session.
        session.set({ authenticated: true, user });
        goto(“/”);
        }

        1. For anyone having issues with this try “await goto” to have a matching session!

Leave a Reply