DEV Community

Cover image for Streamlined Authentication with SvelteKit and Auth0
Kevin
Kevin

Posted on

Streamlined Authentication with SvelteKit and Auth0

In this post, we will implement client-side authentication for SvelteKit apps using Auth0’s auth0-spa-js library.

The Goal: A Route Guard and a Centralized Auth Store

The goal is to create a guard that prevents users from accessing protected routes and a centralized store of information so that we can react to changes in authentication status. A Svelte writable store is perfect for this, it can hold the Auth0 client instance, the user's information, authentication status, and any loading, or error states.

1. Configuration and Setup

The first step is to set up an Auth0 application. We can do this by following Auth0's official quickstart guide for vanilla JavaScript applications - it'll walk us through the process.

Once our Auth0 application is created, we need to make its domain and client ID accessible to our SvelteKit project. We do this by creating a .env file in the root of the SvelteKit project and adding the following:

PUBLIC_AUTH_DOMAIN=<OUR AUTH0 DOMAIN> 
PUBLIC_CLIENT_ID=<OUR AUTH0 CLIENT_ID>
Enter fullscreen mode Exit fullscreen mode

SvelteKit automatically exposes environment variables prefixed with PUBLIC_, so we can easily import these values into our application from $env/static/public:

import { PUBLIC_AUTH_DOMAIN, PUBLIC_CLIENT_ID } from $env/static/public
Enter fullscreen mode Exit fullscreen mode

This approach provides a modern and type-safe way to handle public environment variables in SvelteKit. With these variables in place, the next step is to create a file at src/lib/auth0.js where we define the methods for managing our centralized authentication state.

2. The Authentication State

We define a SvelteKit writable store to hold authentication related details and assign it some initial values. This store will allow our components to react to changes in the user's authentication status.

import { writable } from 'svelte/store';

const initialState = {
  auth0Client: undefined,
  isAuthenticated: false,
  isLoading: false,
  user: undefined,
  error: null
};

export const authStore = writable(initialState);
Enter fullscreen mode Exit fullscreen mode

Then we declare a module-level variable to store the Auth0 client instance. This instance will be responsible for handling all interactions with the Auth0 service.

let clientInstance;
Enter fullscreen mode Exit fullscreen mode

3. initAuth0: Our Application's Authentication Setup

initAuth0 is responsible for creating an Auth0 client and updating the initial user state within our application. It should be called only once, whenever our application starts.

import { PUBLIC_AUTH_DOMAIN, PUBLIC_CLIENT_ID } from '$env/static/public';
import { createAuth0Client } from '@auth0/auth0-spa-js';

export async function initAuth0() {
  if (clientInstance) return;
  authStore.update((store) => ({ ...store, isLoading: true, error: null }));

  try {

    // 1. Initialize Auth0 Client
    clientInstance = await createAuth0Client({
      domain: PUBLIC_AUTH_DOMAIN,
      clientId: PUBLIC_CLIENT_ID,
      authorizationParams: {
        redirect_uri: window.location.origin
      },
      cacheLocation: 'localstorage',
      useRefreshTokens: true
    });

    // 2. Handle Authentication Redirect (Post-Login)
    if (
      window.location.search.includes('code=') &&
      window.location.search.includes('state=')
    ){
      const { appState } = await clientInstance.handleRedirectCallback();
      // `goto` isn't available in hooks.client.js, so we 
      // use `window.location.replace` instead.
      window.location.replace(appState?.targetUrl || '');
    }

    // 3. Check Authentication Status and Get User Profile
    const isAuthenticated = await clientInstance.isAuthenticated();
    const user = isAuthenticated ? await clientInstance.getUser() : undefined;

    // 4. Update Application's Authentication State
    authStore.update((store) => ({
      ...store,
      auth0Client: clientInstance,
      isAuthenticated,
      user,
      isLoading: false
    }));

  } catch (e) {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

To make sure this function is only invoked once when our application loads, we can call it from src/hooks.client.js:

import { initAuth0 } from '$lib/auth0.service';

export async function init() {
  await initAuth0();
}
Enter fullscreen mode Exit fullscreen mode

Since the SvelteKit init client side hook calls initAuth0, our application will only hydrate after our Auth0 authentication service is initialized.

4. Protecting Routes with auth0Guard

auth0Guard simplifies authentication checks within our load functions. It acts as a gatekeeper, ensuring that users are properly authenticated before they can access protected routes. If an unauthenticated user tries to sneak in, auth0Guard seamlessly redirects them to our login page.

import { get } from 'svelte/store';
import { goto } from '$app/navigation';

const defaultRouteGuardOptions = {
  requiresAuth: true,
  loginPath: '/login'
};
export async function auth0Guard(options) {
  const { requiresAuth, loginPath, redirectTo } = { 
    ...defaultRouteGuardOptions,
    ...options
  };
  if (requiresAuth && !get(authStore).isAuthenticated) {
    const path = loginPath + 
      '?redirect=' + 
      encodeURIComponent(redirectTo || window.location.pathname);
    return goto(path, { replaceState: true });
  }
}
Enter fullscreen mode Exit fullscreen mode

auth0Guard can protect individual pages and groups of routes. To protect a specific page, we call auth0Guard from its +page.js load. Remember to double check that auth0Guard is only being called in a browser environment.

For example, to protect routes/user-settings, define the following routes/user-settings/+page.js:

import { browser } from '$app/environment';
import { auth0Guard } from '$lib/auth0';

export async function load() {
  if (browser) {
    await auth0Guard();
  }
}
Enter fullscreen mode Exit fullscreen mode

To protect groups of routes, leverage SvelteKit's route groups by placing auth0Guard in a top-level +layout.js.

For example, to protect routes/(protected)/user-settings and routes/(protected)/user-profile, add auth0Guard to routes/(protected)/+layout.js:

import { browser } from '$app/environment';
import { auth0Guard } from '$lib/auth0';

export async function load({ url }) {
  if (browser) {
    await auth0Guard({
      redirectTo: url.pathname
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

A URL pathname must be explicitly passed to auth0Guard when protecting route groups to ensure the authentication flow will successfully redirect the user back to the correct destination.

5. Reacting to Authentication State

We react to changes in authentication state by importing the authStore into a component or page. The snippet below displays different messages based on whether the user is authenticated:

<script>
  import { authStore } from '$lib/auth0';
  let isAuthenticated = $derived($authStore.isAuthenticated);
</script>

{#if isAuthenticated}
  <p> Hello, World! </p>
{:else}
  <p> Goodbye, World! </p>
{/if}
Enter fullscreen mode Exit fullscreen mode

Conclusion: Simple SvelteKit Authentication with Auth0

By harnessing Svelte’s writable stores, we’ve established a single source of truth for our authentication state. This simplifies managing user logins and reacting to changes in authentication state across our application. We’ve avoided unnecessary Auth0 client initialization by ensuring that initAuth0 is only called in SvelteKit’s application initialization hook, and we’ve looked at how we can protect our routes from unauthorized access using a route guard. With these tools, we can confidently build secure SvelteKit applications!

Top comments (0)