Sveltekit Authentication

🎓Tutorial: What you will learn

* How to create an OAuth Application using Github

* How to redirect requests using SvelteKit

* How to handle OAuth Callbacks

* How to use the Access Token to get Github User Information

* How to store http-only secure cookies using SvelteKit

* How to use the hooks middleware in SvelteKit

* How to read session information in the SvelteKit client

SvelteKit is the new way to build svelte applications. SvelteKit gives you the ability to run your application on the server and client. With this new approach you have the option to leverage http-only (server-side) cookies to manage authentication state. In this post, we will walk through the process of setting up OAuth authentication using Github and SvelteKit.

Prerequisites

What do I need to know for this tutorial?

Getting Started

Ready, set, go! SvelteKit provides a command-line application that we can use to spin up a new project, the CLI will ask us a bunch of questions, lets step through them. In your terminal create a new folder for this project. Let’s call the project authy or any name you prefer:

mkdir authy
cd authy

Use the npm init function to create the SvelteKit project

npm init svelte@next

Let’s go through the questions:

create-svelte version 2.0.0-next.73

Welcome to SvelteKit!

This is beta software; expect bugs and missing features.

If you encounter a problem, open an issue on https://github.com/sveltejs/kit/issues if none exists already.

? Directory not empty. Continue? › (y/N) y
? Which Svelte app template? › - Use arrow-keys. Return to submit.
[Choose Skeleton project]
? Use TypeScript? › No / Yes -> No
? Add ESLint for code linting? › No / Yes -> No
? Add Prettier for code formatting? › No / Yes -> No

✨ Yay! We just setup SvelteKit

Create Github OAuth Application

Go to https://github.com/settings/applications/new in your browser and create a new application called authy with a homepage of http://localhost:3000 and a callback url of http://localhost:3000/callback

Click Register application

You will be redirected to a page that looks similar to this:

In your project directory, create a .env file and in this file take the client id from the github page and add to the .env file as VITE_CLIENT_ID and then click the Generate a new client secret then copy the secret and add it to the .env file as VITE_CLIENT_SECRET

VITE_CLIENT_ID=XXXXXXX
VITE_CLIENT_SECRET=XXXXXXXXXX

Save and close your .env file

🎉 you have created a Github OAuth application! Now we can wireup the OAuth application into our project to create a secure workflow.

Setup the login button

Setting up the login, we will need to add a button to src/routes/index.svelte and then create a Sveltekit endpoint, this endpoint will perform a redirect to Github for authentication.

src/routes/index.svelte

<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<a href="/login">
  <button>Login using Github</button>
</a>

Create the /login endpoint

SvelteKit not only leverages the file system to define page routes, SvelteKit leverages the file system to define endpoints as well. In the routes folder or any child folder in the routes folder, if a file ends with the .svelte extension it is a page if the file ends with a .js extension it is an endpoint. Using the exports feature of esm, you can map http verbs to javascript handlers. In our case, we want to create a src/routes/login.js file and map the GET http verb to the exported get function.

export async function get(req) {
  return {
    body: 'Hello'
  }
}

With the get handler on src/routes/login.js defined, it will take a Request object as input and return a Response object as output. Each of these object types are defined as part of the fetch specification:

In the SvelteKit documentation you can see them defined as typescript types:

type Headers = Record<string, string>;

type Request<Locals = Record<string, any>, Body = unknown> = {
    method: string;
    host: string;
    headers: Headers;
    path: string;
    params: Record<string, string>;
    query: URLSearchParams;
    rawBody: string | Uint8Array;
    body: ParameterizedBody<Body>;
    locals: Locals; // populated by hooks handle
};

type EndpointOutput = {
    status?: number;
    headers?: Headers;
    body?: string | Uint8Array | JSONValue;
};

type RequestHandler<Locals = Record<string, any>> = (
    request: Request<Locals>
) => void | EndpointOutput | Promise<EndpointOutput>;

So what do we want to accomplish here?

We want to redirect the request to the github authentication endpoint with our CLIENT_ID.

In order to respond from the server to the client with a redirect directive, we need to return a 3xx status code, lets use 302 and we need to provide a location in the header. This location should be github oauth authorization location. https://github.com/login/oauth/authorize

src/routes/login.js

const ghAuthURL = 'https://github.com/login/oauth/authorize'
const clientId = import.meta.env.VITE_CLIENT_ID

export async function get(req) {
  const sessionId = '1234'
  return {
    status: 302,
    headers: {
      location: `${ghAuthURL}?client_id=${clientId}&state=${sessionId}`
    }
  }
}

Handling the callback

When Github authorizes or does not authorize, Github needs a way to let our application know. This is why we gave Github the callback url. This url is the endpoint we need to create next. Create a new file src/routes/callback.js and in that file provide a get handler.

src/routes/callback.js

export async function get(req) {
  return {
    body: 'callback'
  }
}

When we redirect the user to Github, Github will ask them to login, then authorize our application. If the user chooses to authorize the application, Github will redirect the browser to our callback endpoint passing with it a code query parameter. We want to use that code query parameter to get an access_token for the authorized user. Then we will use the access_token to get the user information from Github.

We can use query.get method off of the request object to get the code value. We can use the fetch function from the node-fetch library to make our request.

yarn add node-fetch

Get access token

src/routes/callback.js

import fetch from 'node-fetch'
const tokenURL = 'https://github.com/login/oauth/access_token'

const clientId = import.meta.env.VITE_CLIENT_ID
const secret = import.meta.env.VITE_CLIENT_SECRET

export async function get(req) {
  const code = req.query.get('code')
  const accessToken = await getAccessToken(code)

  return {
    body: JSON.stringify(accessToken)
  }
}

function getAccessToken(code) {
  return fetch(tokenURL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify({
      client_id: clientId,
      client_secret: secret,
      code
    })
  }).then(r => r.json())
    .then(r => r.access_token)
}

Get user info

const userURL = 'https://api.github.com/user'

function getUser(accessToken) {
  return fetch(userURL, {
    headers: {
      Accept: 'application/json',
      Authorization: `Bearer ${accessToken}`
    }
  })
    .then(r => r.json())

}

modify get function

export async function get(req) {
  const code = req.query.get('code')
  const accessToken = await getAccessToken(code)
  const user = await getUser(accessToken)

  return {
    body: JSON.stringify(user)
  }
}

In our callback handler we should now be seeing the user object! Great Job you have the happy path of Github OAuth working in SvelteKit. But we are not done.

Setting a cookie for user session

We need to instruct SvelteKit to write a http-only cookie. This cookie will keep our user session.

hooks

We need to create a src/hooks.js file, this file will contain a handle function that will allow us to read cookies and write cookies as it wraps the incoming request for every request.

import cookie from 'cookie'

export async function handle({request, resolve}) {
  const cookies = cookie.parse(request.headers.cookie || '')

  // code here happends before the endpoint or page is called

  const response = await resolve(request)

  // code here happens after the endpoint or page is called

  return response
}

After the resolve function we want to check and see if the request’s locals object was modified with a user key. If it was, we want to set the cookie with the value.

import cookie from 'cookie'

export async function handle({ request, resolve }) {
  const cookies = cookie.parse(request.headers.cookie || '')

  // code here happends before the endpoint or page is called

  const response = await resolve(request)

  // code here happens after the endpoint or page is called

  response.headers['set-cookie'] = `user=${request.locals.user || ''}; Path=/; HttpOnly`

  return response
}

By setting the cookie with HttpOnly - this will ensure that it can only be written by the server. A cookie will be stored in the browser and remain there until we clear it. So if we want to access the cookie information in any of our page or endpoint handlers we need to parse the cookie and set the value on the request.locals object.

import cookie from 'cookie'

export async function handle({ request, resolve }) {
  const cookies = cookie.parse(request.headers.cookie || '')

  // code here happends before the endpoint or page is called
  request.locals.user = cookies.user
  console.log({ user: request.locals.user })

  const response = await resolve(request)

  // code here happens after the endpoint or page is called
  response.headers['set-cookie'] = `user=${request.locals.user || ''}; Path=/; HttpOnly`

  return response
}

set the request.locals.user value in callback.js

In src/routes/callback.js we need to set the request.locals.user value with the user.login identifier, which is guaranteed to be unique and it work nicely for this demo.

export async function get(req) {
  const code = req.query.get('code')
  const accessToken = await getAccessToken(code)
  const user = await getUser(accessToken)

  // this mutates the locals object on the request
  // and will be read by the hooks/handle function
  // after the resolve
  req.locals.user = user.login

  return {
    status: 302,
    headers: {
      location: '/'
    }
  }
}

Send session information to SvelteKit Load

In the src/hooks.js file we can setup another function called getSession this function will allow us to set a session object to be received by every load function on a SvelteKit page component.

export async function getSession(request) {
  return {
    user: request.locals.user
  }
}

Get session in the script module tag

In our src/routes/index.js page component we are going to add two script tags, the first script tag will be of context module and will run on the server, the second script tag will contain our client side logic for our Svelte Component.

<script context="module">
  export async function load({ session }) {

    return {
      props: {
        user: session.user,
      },
    };
  }
</script>
<script>
  export let user
</script>

<h1>Welcome to SvelteKit</h1>
<p>
  Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
</p>
{#if user}
<h2>Welcome {user}</h2>
<a href="/logout">
  <button>Logout</button>
</a>
{:else}
<a href="/login">
  <button>Login using Github</button>
</a>
{/if}

We use both script tags to pass the session value from the load function to the client script. This allows us to modify the view based on if the user is present in the session. We are able to show the user login name on the screen.

Sweet! ⚡️

Logout

Create a new file called src/routes/logout.js in this file we will create a get endpoint handler function. In this function, we want to set the user equal to null and redirect the request back to the home page.

export async function get(req) {
  req.locals.user = null
  console.log(req.locals.user)
  return {
    status: 302,
    headers: {
      location: '/'
    }
  }
}

Now, when you click the logout button, the user is set to an empty string versus the user.login.

Protecting pages and endpoints

Now that you have authentication working with Github OAuth, you may want to protect some pages and endpoints. You can perform a test on each page that you want to protect, or you can use the __layout.svelte component and create an accepted list of paths that you would like to protect.

src/routes/__layout.js

<script context="module">
export async function load({page, session}) {
  if (/^\/admin\/(.*)/.test(page.path) && session.user === '') {
    return { redirect: '/', status: 302 }
  }
  return { props: {} }
}
</script>

<slot />

In this example, we are protecting all pages that start with /admin/* in their path.

Summary

That is the end of this little journey my friend, it was a nice trip, hopefully you laughed more than cried, and learned something about SvelteKit. The SvelteKit routing bits are straightforward when you are able to walk through how they work, not much magic, and by setting http-only cookies, you can create simple long lived sessions for your applications. Remember, the information stored in the cookie is not encrypted so do not store any secrets, use a cache or a database if you need to put some more session/user specific data together.

Sponsored by hyper

If you are building an application and want your application to be:

  • Easy to Maintain!
  • Easy to Test!
  • Without un-intentional Technical Debt

You should check out hyper! https://hyper.io

29