Skip to content

SvelteKit SPA Auth0

At Synatic, we recently started using Svelte and SvelteKit for our frontend applications. These technologies have been fun to work with and have offered something different compared to React and Angular. With new libraries and frameworks, there are always a few differences and pain points and one we experienced firsthand was authentication with Auth0 for a SvelteKit single page app (SPA).

We found several guides on integrating Auth0 with SvelteKit but nearly all of them were mostly focused on apps using SSR with barely a mention of SPA apps. This blog article serves to fill this void and will hopefully help other developers looking to use Auth0 in their SPAs built with SvelteKit.

Auth0 setup

To set up Auth0 with your SvelteKit SPA, first, create an Auth0 account and set up a new application. Make sure to configure the application as a SPA in the Auth0 dashboard.

Next, install the @auth0/auth0-js package and import it into your SvelteKit app. In the top-level layout file, initialize the webAuth and authentication clients, which will make these clients accessible throughout your application. You will also need to provide your Auth0 client ID, domain and redirect URI as configuration options when creating the instance.

// src/routes/+layout.js

export const ssr = false;
export const prerender = true;

import auth0 from "auth0-js";
import {
  PUBLIC_AUTH0_CLIENT_ID,
  PUBLIC_AUTH0_ISSUER_BASE_URL,
  PUBLIC_AUTH0_AUDIENCE,
  PUBLIC_AUTH0_CALLBACK_URL,
} from "$env/static/public";

export async function load({ params, parent, url }) {
  const options = {
    clientID: PUBLIC_AUTH0_CLIENT_ID,
    domain: PUBLIC_AUTH0_ISSUER_BASE_URL,
    responseType: "token",
    audience: PUBLIC_AUTH0_AUDIENCE,
    redirectUri: PUBLIC_AUTH0_CALLBACK_URL,
    scope: "openid profile email",
  };
  const webAuthClient = new auth0.WebAuth(options);
  const authenticationClient = new auth0.Authentication(options);

  return { webAuthClient, authenticationClient };
}

Login

Now for the login page, we’re going to use the webAuth client to log in users and redirect them to the configured callback URL.

// src/routes/login/+page.svelte
<script>
    import { onMount } from 'svelte';
    import { page } from '$app/stores';
    import {
        PUBLIC_AUTH0_CLIENT_ID,
        PUBLIC_AUTH0_ISSUER_BASE_URL,
        PUBLIC_AUTH0_AUDIENCE,
        PUBLIC_AUTH0_CALLBACK_URL
    } from '$env/static/public';

    export let data;

    function login() {
        const options = {
            clientID: PUBLIC_AUTH0_CLIENT_ID,
            redirectUri: PUBLIC_AUTH0_CALLBACK_URL,
            responseType: 'code'
        };
        data.webAuthClient.authorize(options);
    }
</script>

<div>
    <button on:click={() => login()} > Login </button>
</div>

Handle callback

With the login page now completed, move on to the callback page. Assuming you have set up Auth0 correctly, you should have configured the callback URL to be something like http://localhost:3000/auth/callback. Auth0 will redirect to http://localhost:3000/auth/callback after a successful login. So, now you need to create a callback page to handle this redirect:

<script>
  import { onMount } from "svelte";
  import { page } from "$app/stores";
  import { goto } from "$app/navigation";
  import {
    createSession,
    getSession,
    isAuthenticated,
  } from "$lib/services/auth";
  import { redirect } from "@sveltejs/kit";

  onMount(() => {
    (async () => {
      if (
        $page.url.searchParams.get("code") &&
        $page.url.searchParams.get("state")
      ) {
        const code = $page.url.searchParams.get("code");
        const session = getSession();
        if (session && !code) {
          if (isAuthenticated()) {
            return goto("/dashboard");
          }
          throw redirect(303, "/");
        }

        const options = {
          grantType: "authorization_code",
          code: code,
          redirectUri: "http://localhost:3000/auth/callback",
        };

        return $page.data.authenticationClient.oauthToken(
          options,
          (err, response) => {
            if (err) {
              return goto("/");
            }
            if (response && response.accessToken && response.idToken) {
              return $page.data.webAuthClient.validateToken(
                response.idToken,
                null,
                (err, payload) => {
                  if (err) {
                    return goto("/");
                  }
                  createSession(response);
                  if (isAuthenticated()) {
                    return goto("/dashboard");
                  }
                  return goto("/");
                }
              );
            }
            return goto("/");
          }
        );
      } else {
        if (isAuthenticated()) {
          return goto("/dashboard");
        }
        return goto("/");
      }
    })();
  });
</script>

<div class="callback">
  <div class="loading-ellipsis">Loading...</div>
</div>

<style>
  .callback {
    top: 45%;
    position: relative;
    margin-left: auto;
    margin-right: auto;
    width: 400px;
    height: 100%;
  }
</style>

The callback page is a bit more complicated than the login page. You first check if the URL has the code and state parameters. If it does, use the authenticationClient to get the access token and ID token. Then, use the webAuthClient to validate the ID token. Once the ID token has been validated, create a session for the user by storing the access token and the ID token along with the token's expiry time inside the application's local storage and then redirect the user to the dashboard page. If the URL does not have the code and state parameters, check if the user is already authenticated.

The getSession function fetches any auth information stored in localStorage:

export function getSession() {
  const session = {
    accessToken: localStorage.getItem("access_token"),
    idToken: localStorage.getItem("id_token"),
    expiresAt: localStorage.getItem("expires_at"),
  };

  if (!session.accessToken || !session.idToken || !session.expiresAt) {
    return null;
  }
  return session;
}

createSession merely takes in an object with the access token, the ID token and expiry and stores it in localStorage.

export function createSession(s) {
  const expiresAt = JSON.stringify(s.expiresIn * 1000 + new Date().getTime());
  localStorage.setItem("access_token", s.accessToken);
  localStorage.setItem("id_token", s.idToken);
  localStorage.setItem("expires_at", expiresAt);
}

Finally, isAuthenticated checks whether session information is stored in localStorage and whether the expiry date has passed:

export function isAuthenticated() {
  const session = getSession();
  if (!session) {
    return false;
  }
  // Check whether the current time is past the access token's expiry time
  const expiresAt = JSON.parse(session.expiresAt || "");
  return new Date().getTime() < expiresAt;
}

Protected routes

Now with the login process complete, we need to find a way to limit access to certain pages to only authenticated users. You can do this by creating a (protected) directory in the src/routes directory. This directory will contain all the pages that need protecting. Create a +layout.js file in the (protected) directory. This layout file will host the load function, which will be called before each page is loaded. Use this function to check if the user is authenticated. If the user is not authenticated, redirect them to the login page.

import { getSession, isAuthenticated } from "$lib/services/auth";
import { redirect } from "@sveltejs/kit";

export async function load({ params, parent, url, data }) {
  if (url.pathname.startsWith("/")) {
    const session = getSession();
    if (session) {
      if (isAuthenticated()) {
        return {};
      }

      throw redirect(303, "/login");
    }

    throw redirect(303, "/login");
  }
}

Now with our routes protected, fetching data from an API using our access token stored in localStorage should be possible. The API will make use of this token to verify the user and respond with the requested data. Here is an example of a page fetching data from a secured API:

<script>
  import { onMount } from "svelte";

  let data = null;
  let error = null;

  async function fetchData() {
    const token = localStorage.getItem("access_token");
    try {
      const response = await fetch("https://api.example.com/your-endpoint", {
        method: "GET",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
      });

      if (response.ok) {
        data = await response.json();
      } else {
        error = "Failed to fetch data";
      }
    } catch (e) {
      error = e.message;
    }
  }

  onMount(() => {
    fetchData();
  });
</script>

<main>
  {#if error}
  <p>Error: {error}</p>
  {:else if data}
  <h1>Data from API:</h1>
  <pre>{JSON.stringify(data, null, 4)}</pre>
  {/if}
</main>

Conclusion

In this article, we discussed how to integrate Auth0 with a SvelteKit SPA. We covered setting up Auth0, implementing the login process, handling the callback, protecting routes and fetching data from a secured API using the access token. Remember to refer to the official documentation of Auth0 and SvelteKit for more detailed instructions and best practices. With this information, you should be well-equipped to integrate Auth0 authentication into your SvelteKit applications.