DEV Community

Cover image for SvelteKit Changes: Cookies and Authentication
Shivam Meena
Shivam Meena

Posted on

SvelteKit Changes: Cookies and Authentication

Introduction

In this article, We going to talk about cookies in sveltekit and learn how to handle authentication with Cookies after sveltekit removed getSession handler from hooks. We are now left with new route system and new cookies method. Here, we going to look into two different possible methods to authenticate a user in sveltekit.

  1. Using parent() function in routes.
  2. Using svelte stores to handle authentication.

Cookies

Cookies is a new method introduced in sveltekit for cookie header. You can use this method to work with cookies in your webapp with least efforts. There are some more methods in cookies those are set, get and delete.

  • Set Method

This methods sets a cookie. This will add a set-cookie header to the response, but also make the cookie available via cookies.get during the current request.

The httpOnly and secure options are true by default, and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The sameSite option default to lax.

By default, the path of a cookie is the 'directory' of the current pathname. In most cases you should explicitly set path: '/' to make the cookie available throughout your app.

Set method takes three parameters name, value and options, options are optional so it's up to you what you gonna set.

cookies.set('session', user.entityId, {
            path: '/',
            httpOnly: true,
            sameSite: 'strict',
            secure: !dev,
            maxAge: 60 * 60 * 24 * 30
        });
Enter fullscreen mode Exit fullscreen mode

Here, I'm using set method to set my session cookie in Set-Cookie header and defining options as per need.

1. name : session (string)
2. value : user.entityId (string)
3. options: {
            path: '/',
            httpOnly: true,
            sameSite: 'strict',
            secure: !dev,
            maxAge: 60 * 60 * 24 * 30
        }
Enter fullscreen mode Exit fullscreen mode
  • Get Method

This method is use to get a cookie that was previously set with cookies.set, or from the request headers.

Get method takes two parameters name and options, options are optional.

const session = cookies.get('session');
Enter fullscreen mode Exit fullscreen mode

Here, I'm using get method to access my session value from cookies that we set above using set method.

  • Delete Method

This method is use to delete a cookie by setting its value to an empty string and setting the expiry date in the past.

Delete method takes two parameters name and options, options are optional.

await cookies.delete('session');
Enter fullscreen mode Exit fullscreen mode

That's all we need to remove a cookie.

  • Serialize Method

It's same as set method which serialize a cookie name-value pair into a Set-Cookie header string.

Authentication Flow in sveltekit

Here, I'll explain steps to authenticate a user using cookies and session.

  1. Every user authentication starts after you have a register method. So, first make sure to have a user registered in db.

  2. Make a login route which will gonna handle your user authentication. Here we gonna set Cookies named 'session' using set method.

  3. We will update our hooks.server.ts where we going to access cookies and get our session cookie using get method.

  4. We will get our user on the basis of session and going to add data to locals as user.

  5. In +layout.server.js, we going to access our locals data and going to return user data.

  6. We can use that user data to set a store for authentication or use it by parent method.

Local interface that defines event.locals, which can be accessed in hooks (handle, and handleError), server-only load functions, and +server.js files.

Authentication using parent() method

Now we will follow our flow and going to code our authentication flow.

  • Create a route which will going to handle your login. It should have +page.svelte and +page.server.js. In +page.svelte you going to add your login form which email and password fields. On login it's going to call a form action on +page.server.js for now it's our default action.
// +page.server.ts (inside login route)

import { redirect } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { customResponse } from '$lib/utils';
import { userRepository } from '$lib/Redis/dbRepository';
import * as bcrypt from 'bcrypt';
import { dev } from '$app/environment';

export const actions = {
    default: async ({ request, cookies }) => {
        const form = await request.formData();
        const email = form.get('email');
        const password = form.get('password');

        if (!email || !password) return customResponse(400, false, 'Email and Password are required');

        if (typeof email !== 'string' || typeof password !== 'string')
            return customResponse(400, false, 'Enter a valid email and password.');

        const user = await userRepository.search().where('email').equals(email).return.first();
        const passwordMatch = user && (await bcrypt.compare(password, user.password));

        if (!user || !passwordMatch)
            return customResponse(400, false, 'You entered the wrong credentials.');
        cookies.set('session', user.entityId, {
            path: '/',
            httpOnly: true,
            sameSite: 'strict',
            secure: !dev,
            maxAge: 60 * 60 * 24 * 30
        });
        // return customResponse(200, true, 'User loggedIn successfully');
        throw redirect(307, '/dashboard');
    }
};

// customResponse function from lib/utils

import { invalid } from '@sveltejs/kit';

export const customResponse = (status: number, success: boolean, message: string, data?: any) => {
    if (success) {
        return {
            success: success,
            message: message,
            info: data
        };
    }
    return invalid(status, {
        success: success,
        message: message,
        info: data
    });
};
Enter fullscreen mode Exit fullscreen mode

Here, I'm validating the user and after validating successfully we are using our set method to set session cookie.

cookies.set('session', user.entityId, {
            path: '/',
            httpOnly: true,
            sameSite: 'strict',
            secure: !dev,
            maxAge: 60 * 60 * 24 * 30
        });
Enter fullscreen mode Exit fullscreen mode
  • Now we will going to use our hook.server.ts to provide user data to every route whenever we need it.
// hook.server.ts

import type { Handle } from '@sveltejs/kit';
import { userRepository } from './lib/Redis/dbRepository';

// custom redirect from joy of code `https://github.com/JoysOfCode/sveltekit-auth-cookies/blob/migration/src/hooks.ts`
function redirect(location: string, body?: string) {
    return new Response(body, {
        status: 303,
        headers: { location }
    });
}

const unProtectedRoutes: string[] = [
    '/',
    '/login',
    '/createAdmin',
    '/features',
    '/docs',
    '/deployment'
];

export const handle: Handle = async ({ event, resolve }) => {
    const session = event.cookies.get('session');
    if (!session && !unProtectedRoutes.includes(event.url.pathname))
        return redirect('/login', 'No authenticated user.');
    const currentUser = await userRepository.fetch(session as string);

    if (currentUser) {
        event.locals.user = {
            isAuthenticated: true,
            name: currentUser.name,
            email: currentUser.email,
            type: currentUser.user_type,
            active: currentUser.active,
            phone: currentUser.phone
        };
    } else {
        if (!unProtectedRoutes.includes(event.url.pathname)) return redirect('/', 'Not a valid user');
    }

    return resolve(event);
};
Enter fullscreen mode Exit fullscreen mode

Here, We going to access our session cookie using get method const session = event.cookies.get('session');.

After that we need to check if session cookie exist or not and we are adding extra condition for route check because we are going to redirect if user is not loggedIn.

Why we are adding this extra route check conditions? It's because when user is on login route and if user is not authorized we going to redirect him to login page which will create an infinite loop of redirect and our browser don’t like that.

After validation we are going to get our user from database and update our locals

event.locals.user = {
            isAuthenticated: true,
            name: currentUser.name,
            email: currentUser.email,
            type: currentUser.user_type,
            active: currentUser.active,
            phone: currentUser.phone
        };
Enter fullscreen mode Exit fullscreen mode
  • Now, we going to access our locals in +layout.server.js and going to return our user data from locals to our web app.
// +layout.server.js

export const load = async ({ request, locals, cookies }) => {

    return {
        user: locals.user
    };
};
Enter fullscreen mode Exit fullscreen mode

Here, we are accessing our locals and then returning it to all the pages because it's layout, all the data will be accessible to every page which shared the same layout.

  • Accessing data in routes is easy because of parent method. We going to use routes page.js's load method to access data from layout.
// +page.js

export const load = async ({ parent }) => {
    const { user } = await parent();
    if (user) {
        user: user
    }
};
Enter fullscreen mode Exit fullscreen mode

This will make your user data accessible to the route. For first time it's going to look complex but after doing once you are going to love it.

Authentication using stores

In stores we going to need a auth.js file to add a store to our project.

Everything will be same as above part except here we are not going to use parent() method. We will set a store and then we are going to access our +layout.server.js data in +layout.js which will going to return our data to +layout.svelte and then we going to set our user data to store.

  • So follow all the steps till +layout.server.js, and return user data to all pages.

  • Then we are going to create a store in lib/auth.js

import { writable } from 'svelte/store';
export const user = writable();
Enter fullscreen mode Exit fullscreen mode

This will do our job for store.

  • Now in +layout.js file we will access the data came from +layout.server.js.
// `+layout.js`

export const load = async function ({ data }) {
    return {
        user: data.user
    };
};
Enter fullscreen mode Exit fullscreen mode

This will return data to our +layout.svelte in our export let data.

// +layout.svelte

<script lang="ts">
  import { user } from "$lib/auth";
  export let data;
  $: $user.set(data.user);
</script>

<slot />
Enter fullscreen mode Exit fullscreen mode

Now we have our store with user info and we can access it anywhere in our client side code.

This is all we need to handle authentication in sveltekit with cookies. We haven’t covered how to manage a protected route. So answer is already somewhere in this article(not accurate but idea to handle it). Do a bit brainstorming for that. I might have missed some part or anything you can ask me to add that in comments.

Resources

Here I'm leaving with you with some resources, I used some of them to write this article

This is me writing for you. If you wanna ask or suggest anything please put it in comment and show some love ❤️.

Top comments (14)

Collapse
 
bato3 profile image
bato3

Never, Never NEVER !!!!!11111one one one

Don't set raw userId as auth data: cookies.set('session', user.entityId, - It's an easy way to hijack your administrator account.

Important data should be signed, eg use cookie-signature or put data in JWT / unique UUID for session

Collapse
 
kolja profile image
Kolja

Thanks for this important hint.

Can you explain it a little further, or give me a link?

Collapse
 
bato3 profile image
bato3

You must remember that any value can be modified in such a way as to harm your application. In this case, after logging in, your id is provided and your permissions are retrieved based on this.
The attacker can craft the message and gain administrator privileges.

Thread Thread
 
theether0 profile image
Shivam Meena

Yeah that's true. Have a look at project.

Collapse
 
theether0 profile image
Shivam Meena

I'm not sure this will help you or not
Github - EtherCare

Collapse
 
theether0 profile image
Shivam Meena

I'll use token or jwt for this kind of thing. This project is for testing redis as primary db so i did that.

Collapse
 
bato3 profile image
bato3

Ok, you won't that, but don't show bad behavior. Some _script-kid _will copy it mindlessly, and then there will be crying and gnashing of teeth ...

Thread Thread
 
theether0 profile image
Shivam Meena • Edited

Don't worry I'll be pushing my whole project this weekend which might be helpful and i would love if you suggest something after those changes. But json tokens are easy to crack.

Collapse
 
oyenmwen profile image
Osayimwen Odia

Thank you. Couldn't figure out how to use the Cookies. I was about to lose my mind

Collapse
 
theether0 profile image
Shivam Meena

Happy to help🤩

Collapse
 
jj_squid profile image
JJ

Ayo uhhhhh definitely don’t do that with your stores. You’re setting a store value to a global variable. If that gets called/ran by the server side you’re toast lol. Extremely dangerous. Everybody now has everyone else’s info.

Collapse
 
theether0 profile image
Shivam Meena

I know what you saying, for e.g. i'm assigning locals data to users store after data is validated from sever and cleared even if it's server side rendered it won't going to cause problem but there are scenario's where it's way more dangerous.

Collapse
 
shinokada profile image
shin

Do you have a example repo for this tut?

Collapse
 
theether0 profile image
Shivam Meena