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>
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’
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);
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;
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) {
...
}
}
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();
}
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 });
}
}
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();
}
}
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
});
}
}
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}
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)