Cookie Session Authentication In SvelteKit

Overview

In this article, we will take a look at how hooks, particularly handle & getSession work in SvelteKit. Then I will show a usecase of hooks in a simple app. A user will be able to login and we will authenticate the user and set a cookie. Then whenever the user visits a page, we will check if the cookie is in the request to determine whether or not he/she is authorized to visit the page.

What is Hooks?

In programming, a hook is a place and usually an interface provided in packaged code that allows a programmer to insert customized programming. If you are familiar with a middlware in a server-side programming of node.js and express.js, that is considered as a hook because it does certain things inside the middleware by accessing to a request and a response object.

In SvelteKit, there are currently four functions provided as part of hooks at the time of this writing:

  • handle
  • handleError
  • getSession
  • externalFetch

To be able to do cookie-session authentication, we need only two of them: handle & getSession.

Let's dive into each of them and check out how they work.

Getting started

Create a skeleton Sveltekit project and spin up a dev server.

Terminal
npm init svelte@next cookie-session-auth
cd cookie-session-auth
vim . # Or code . if you use a VS Code editor
npm install
npm run dev

Open up a browser and access http://locahost:3000. You should be able to see a welcome page.

sveltekit

getSession

In this section, we will take a look at how to create a session data on a server using getSession and access it from a client side using two methods:

  1. $app/stores
  2. load

getSession() runs whenever SvelteKit server-renders a page. It takes an event object and returns a session object that is available on the client side. Note that the session object must be serializable, which means that it must not contain things like functions or classes.

To use getSession, we must create a hooks.js file in our ./src directory:

./src/hooks.js
1export const getSession = event => {
2 console.log(event)
3
4 return {
5 user: {
6 id: '1h4jk90ds',
7 name: 'John Doe',
8 role: 'admin',
9 },
10 }
11}

Go back to your terminal and check out the event object printed out:

console.log(event)
1{
2 clientAddress: [Getter],
3 locals: {},
4 params: {},
5 platform: undefined,
6 request: Request {
7 size: 0,
8 follow: 20,
9 compress: true,
10 counter: 0,
11 agent: undefined,
12 highWaterMark: 16384,
13 insecureHTTPParser: false,
14 [Symbol(Body internals)]: {
15 body: null,
16 stream: null,
17 boundary: null,
18 disturbed: false,
19 error: null
20 },
21 [Symbol(Request internals)]: {
22 method: 'GET',
23 redirect: 'follow',
24 headers: [Object],
25 parsedURL: [URL],
26 signal: null,
27 referrer: undefined,
28 referrerPolicy: ''
29 }
30 },
31 routeId: '',
32 url: URL {
33 href: 'http://localhost:3000/',
34 origin: 'http://localhost:3000',
35 protocol: 'http:',
36 username: '',
37 password: '',
38 host: 'localhost:3000',
39 hostname: 'localhost',
40 port: '3000',
41 pathname: '/',
42 search: '',
43 searchParams: URLSearchParams {},
44 hash: ''
45 }
46}

As you see, we have lots of information available in the event object. Please pay attention to the highlighted line locals:{},. In the example, we will populate this locals object through handle function that will run on the server every time it receives a request. What this means is that intead of populating the user data inside getSession as we did above, we will intercept the request first and populate the user session data via locals object using handle.

Because the getSession returns a session object to a client, whatever we pass in the session object will be accessible through two ways in the client side:

1. Using $app/stores

$app/stores is one of built-in modules in SvelteKit. To access the session data we populated on the server, we can import session from $app/stores on the client. The session object has two important characteristics:

  1. Contextual
  • What this means is that it is added to the context of the root component of our app.
  1. Unique
  • It is also unique to each request on the server. What this means that the session is not shared between multiple requests handled by the same server simultaneously.

These two properties make the session so useful to store user-specific data. To use stores, it is important to note that we must subscribe to it while component is being initialized. Using $session would do subscription for us convienently.

./src/routes/index.svelte
1<script>
2 import { session } from "$app/stores";
3</script>
4
5<h1>Session Object</h1>
6{JSON.stringify($session)}

Make sure to spin up dev server and access http://localhost:3000 in a browser:

sveltekit

2. Using load function

You can also use load to access the session object as well by modifying the index.svelte. To understand how load function works in SvelteKit, you can read this article.

./src/routes/index.svelte
1<script context="module">
2 export const load = ({ session }) => {
3 return {
4 props: {
5 session,
6 },
7 };
8 };
9</script>
10
11<script>
12 export let session;
13</script>
14
15<h1>Session Object</h1>
16{JSON.stringify(session)}

Make sure to spin up dev server and access http://localhost:3000 in a browser:

sveltekit

handle

handle() runs on the server every time SvelteKit receives a request. It receives two objects as the function arguments:

  • an event object that carries the request and
  • a resolve function that invokes SvelteKit's router and generates a response

Through the handle, we can modify both the request and a response.

Update the hooks.js file in the project root directory with the following changes:

./src/hooks.js
1export const handle = async ({ event, resolve }) => {
2 console.log(event)
3
4 event.locals.userName = 'Mary Doe'
5
6 const response = await resolve(event)
7
8 response.headers.set('x-custom-header', 'custom-header')
9
10 return response
11}
12
13export const getSession = event => {
14 console.log(event)
15
16 return {
17 user: {
18 id: '1h4jk90ds',
19 name: 'John Doe',
20 role: 'admin',
21 },
22 }
23}

What this code does

We've added another function handle that receives event and resolve. The console.log(event)(line 2) will spit out the same object in the terminal as we've seen from getSession example with locals: {}, in it. However, we populate locals with userName: 'Mary Doe' right before resolving the request in handle function so that getSession will receive event object with the populated locals in it.

Then we immediately resolve the event to receive a response(line 6). We attach a custom header to the original response(line 8) that is going to sent to the client. If you open up your browser's developer tool and inspect the repsonse headers after accessing http://localhost:3000, there will be x-custom-header: custom-header:

sveltekit

The getSession function is exactly the same as before. But check your terminal again, you will see that this time the console.log(event)(line 12) printed out locals: { userName: 'Mary Doe' }, in the terminal because the handle had intercepted the request and added the userName.

Now that we know how getSession, handle work in SvelteKit, we can take a step further to do a cookie session authentication in a simple app. The app itself won't be a complete form, but it would suffice to show how things are put together to make authentication work in SvelteKit.

Let's work on making authentication endpoints first.

App Structure

sveltekit

Install Dependencies

Terminal
npm install cookie uuid

Login Page

Let's get started with creating a new page login.svelte in ./src/routes directory:

./src/routes/login.svelte
1<script>
2 let msg = "";
3
4 const login = async () => {
5 const res = await fetch("/api/login", {
6 method: "POST",
7 // For demonstration purpose, we just hard-coded a user email and password
8 body: JSON.stringify({
9 email: "johndoe@example.com",
10 password: "secret",
11 }),
12 headers: {
13 "Content-Type": "application/json",
14 },
15 });
16
17 const data = await res.json();
18
19 if (!res.ok) {
20 msg = data.msg;
21 return;
22 }
23
24 msg = data.msg;
25 };
26</script>
27
28<h1>Login</h1>
29<button on:click={login}>Login</button>
30<p>{msg}</p>

Basically, what this code does is that once a user clicks the login button, it will make a HTTP POST request to the /api/login endpoint. Depending on the server response, it will display the corresponding(i.e., login success or fail) message on the login page.

Login Endpoint

To handle the HTTP POST request from the login page, we have to create a matching endpoint:

./src/routes/api/login.js
1import { serialize } from 'cookie'
2import { v4 as uuidv4 } from 'uuid'
3
4export const post = async ({ request }) => {
5 const user = await request.json()
6 console.log(user)
7
8 // In a real life situation, the password must to be hashed/salted and compared to database
9 if (!(user.email === 'johndoe@example.com' && user.password === 'secret')) {
10 return {
11 status: 401,
12 body: {
13 msg: 'Invalid email or password',
14 },
15 }
16 }
17
18 const sessionId = uuidv4()
19 console.log(sessionId)
20
21 const headers = {
22 'Set-Cookie': serialize('session_id', sessionId, {
23 httpOnly: true,
24 sameSite: 'lax',
25 maxAge: 60 * 60 * 24 * 7, // 7 days
26 secure: process.env.NODE_ENV === 'production',
27 path: '/',
28 }),
29 }
30 console.log(headers)
31
32 return {
33 status: 200,
34 headers,
35 body: {
36 msg: 'Login successful!',
37 },
38 }
39}

Let's break down what this code does:

  • The /api/login.js endpoint will receive a HTTP POST request from a client and check if a user email and a password are valid. The console-logged user in line 6 is:

    { email: 'johndoe@example.com', password: 'secret' }
  • If the email or the password are not valid, it returns a status 401 with a message, Invalid email or password.

  • Otherwise, it creates a session id with the help of uuid package and The session_Id will be something like this:

    49914bf9-64e6-424a-bb2e-df7348882256
  • Then it also generates a Set-Cookie headers with the session id using cookie package. The headers is set like:

    {
    'Set-Cookie': 'session_id=49914bf9-64e6-424a-bb2e-df7348882256; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax'
    }
    Set-Cookie AttributeExplanation
    httpOnlyBy setting it true, this prevents the client javascript from accessing the cookie
    sameSiteThis sets the Same-Site attribute in the cookie. The cookies are sent when a user is navigating to the origin site only.
    maxAgeThis sets the Max-Age attribute in the cookie. It lets the cookie be deleted in the browser when a specified period is over.
    secureBy setting it true, the cookie is sent to the server only when a request is made with the https: scheme.
    pathThis indicates the path that must exist in the requested URL for the browser to send the Cookie header. We set it to / so that the cookie will be sent to each request.
  • Once the header is created, we finally return an object with a status 200, the headers, and a body message Login successful!.

Hooks: handle, getSession

./src/hooks.js
1import { parse } from 'cookie'
2
3export const handle = async ({ event, resolve }) => {
4 // Print out cookie in the terminal
5 // It will be something like: session_id=49914bf9-64e6-424a-bb2e-df7348882256
6 console.log(event.request.headers.get('cookie'))
7
8 // Parse cookie
9 const cookies = parse(event.request.headers.get('cookie') || '')
10 console.log('cookies: ', cookies)
11
12 // Attach a user object with the cookies to the event.locals
13 event.locals.user = cookies
14
15 // Determine whether or not a user is authenticated by checking out the sesssion_id
16 if (!cookies.session_id) {
17 event.locals.user.authenticated = false
18 } else {
19 event.locals.user.authenticated = true
20 }
21
22 // Resolve the request
23 const response = await resolve(event)
24
25 return response
26}
27
28export const getSession = event => {
29 console.log(event.locals.user)
30 const user = event.locals.user
31
32 if (!user.session_id) {
33 return {}
34 }
35
36 return {
37 user,
38 }
39}
handle

Let's break down what this code does:

  • handle will take an event object as an argument and get a cookie object from event.request.headers.
  • Then it parses the cookie using parse() from cookie package The parsed cookie will be like this:
    { session_id: '49914bf9-64e6-424a-bb2e-df7348882256' }
  • Then we set event.locals.user to the parse cookie.
  • If the cookie has a session id, we attche authenticated key with a valuetrue, otherwise 'false' to the event.locals.user.
  • Then we resolve the request and return a response.
getSession

Because the handle has intercepted the request and already determinied whether or not a user is authenticated, we can set a session object using getSession so that it can be available in the client.

  • getSession takes an event object that has passed through handle. When you console log evnet.locals.user, you will be able to see:
    {
    session_id: '49914bf9-64e6-424a-bb2e-df7348882256',
    authenticated: true
    }
  • If the user does not have a session id, then we return nothiing.
  • Otherwise, we return the user object.

Back to Login Page

In your browser, please access http://locahost:3000/login. Once you click Login button in the login page, you will be able to see the Login successful message:

sveltekit

Because the login is successful, the /api/login endpoint returns the headers with Set-Cookie header as a part of response. When the client receives the status 200, the session_id will be available in your local browser. You can confirm this from your chrome developer tool:

sveltekit

The session_id will be effective for 7 days as we've set maxAge to 7 days, so every time you send another request to the server, the server will run hooks.js and receive a cookie headers as part of request and attempt to check if the user is authenticated or not through handle.

Main Page

Since we would like to confirm that the client gets the session data, let's use load function to access session and display it:

./src/routes/index.svelte
1<script context="module">
2 export const load = ({ session }) => {
3 return {
4 props: {
5 session,
6 },
7 };
8 };
9</script>
10
11<script>
12 export let session;
13</script>
14
15<h1>Session Object</h1>
16{JSON.stringify(session)}

Assuming that you've successfully logged in and hit the main page http://locahost:3000, you will be able to see the user's session data displayed like this:

sveltekit

Comments

© 2022 Youngjae Jay Lim. All Rights Reserved, Built with Gatsby