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.
Terminalnpm init svelte@next cookie-session-authcd cookie-session-authvim . # Or code . if you use a VS Code editornpm installnpm run dev
Open up a browser and access http://locahost:3000
. You should be able to see a welcome page.
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:
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.js1export const getSession = event => {2 console.log(event)34 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: null20 },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:
- Contextual
- What this means is that it is added to the context of the root component of our app.
- 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.svelte1<script>2 import { session } from "$app/stores";3</script>45<h1>Session Object</h1>6{JSON.stringify($session)}
Make sure to spin up dev server and access http://localhost:3000
in a browser:
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.svelte1<script context="module">2 export const load = ({ session }) => {3 return {4 props: {5 session,6 },7 };8 };9</script>1011<script>12 export let session;13</script>1415<h1>Session Object</h1>16{JSON.stringify(session)}
Make sure to spin up dev server and access http://localhost:3000
in a browser:
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.js1export const handle = async ({ event, resolve }) => {2 console.log(event)34 event.locals.userName = 'Mary Doe'56 const response = await resolve(event)78 response.headers.set('x-custom-header', 'custom-header')910 return response11}1213export const getSession = event => {14 console.log(event)1516 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
:
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
.
Example: Cookie Session Authentication
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
Install Dependencies
Terminalnpm install cookie uuid
Login Page
Let's get started with creating a new page login.svelte
in ./src/routes
directory:
./src/routes/login.svelte1<script>2 let msg = "";34 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 password8 body: JSON.stringify({9 email: "johndoe@example.com",10 password: "secret",11 }),12 headers: {13 "Content-Type": "application/json",14 },15 });1617 const data = await res.json();1819 if (!res.ok) {20 msg = data.msg;21 return;22 }2324 msg = data.msg;25 };26</script>2728<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.js1import { serialize } from 'cookie'2import { v4 as uuidv4 } from 'uuid'34export const post = async ({ request }) => {5 const user = await request.json()6 console.log(user)78 // In a real life situation, the password must to be hashed/salted and compared to database9 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 }1718 const sessionId = uuidv4()19 console.log(sessionId)2021 const headers = {22 'Set-Cookie': serialize('session_id', sessionId, {23 httpOnly: true,24 sameSite: 'lax',25 maxAge: 60 * 60 * 24 * 7, // 7 days26 secure: process.env.NODE_ENV === 'production',27 path: '/',28 }),29 }30 console.log(headers)3132 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-loggeduser
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 Thesession_Id
will be something like this:49914bf9-64e6-424a-bb2e-df7348882256Then it also generates a
Set-Cookie
headers with the session id usingcookie
package. Theheaders
is set like:{'Set-Cookie': 'session_id=49914bf9-64e6-424a-bb2e-df7348882256; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax'}Set-Cookie Attribute Explanation httpOnly
By setting it true
, this prevents the client javascript from accessing the cookiesameSite
This sets the Same-Site
attribute in the cookie. The cookies are sent when a user is navigating to the origin site only.maxAge
This sets the Max-Age
attribute in the cookie. It lets the cookie be deleted in the browser when a specified period is over.secure
By setting it true
, the cookie is sent to the server only when a request is made with thehttps:
scheme.path
This 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 messageLogin successful!
.
Hooks: handle, getSession
./src/hooks.js1import { parse } from 'cookie'23export const handle = async ({ event, resolve }) => {4 // Print out cookie in the terminal5 // It will be something like: session_id=49914bf9-64e6-424a-bb2e-df73488822566 console.log(event.request.headers.get('cookie'))78 // Parse cookie9 const cookies = parse(event.request.headers.get('cookie') || '')10 console.log('cookies: ', cookies)1112 // Attach a user object with the cookies to the event.locals13 event.locals.user = cookies1415 // Determine whether or not a user is authenticated by checking out the sesssion_id16 if (!cookies.session_id) {17 event.locals.user.authenticated = false18 } else {19 event.locals.user.authenticated = true20 }2122 // Resolve the request23 const response = await resolve(event)2425 return response26}2728export const getSession = event => {29 console.log(event.locals.user)30 const user = event.locals.user3132 if (!user.session_id) {33 return {}34 }3536 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 fromevent.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 theevent.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 throughhandle
. When you console logevnet.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:
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:
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.svelte1<script context="module">2 export const load = ({ session }) => {3 return {4 props: {5 session,6 },7 };8 };9</script>1011<script>12 export let session;13</script>1415<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:
Comments