Authentication with Supabase + Sveltekit

Published: July 9, 2022 Updated: July 15, 2022

Authentication with Supabase + Sveltekit

Table of Contents

Out of Date

The information in this article is out of date. It is recommended to use @supabase/auth-helpers-sveltekit going forward. This method will still work, however.

Setup

In Supabase, go to your Authentication Settings and make sure you have set your Site URL and Additional Redirect URLs.

Auth Settings
Click to open full screen

Then scroll down and select your preferred auth provider(s).

Auth Providers:no-aspect
Click to open full screen

Finally, go to your Supabase API Settings and get your Project URL and Anon Key. Store them in environment variables like so:

# .env.local
SUPABASE_URL=
SUPABASE_ANON_KEY=

Creating a Supabase Client and Auth Store

Create a new file in your $lib directory for your Supabase Client. In here, you’ll do four things: create a supabase client, create a store to hold the auth state, watch for auth state changes, and subscribe to expiring access tokens.

The auth subscription is a timer to watch for expiring access tokens and refresh them. Supabase provides a refresh token and a timestamp when the token will expire. You can use this to set a timer that will trigger a token refresh prior to the token expiring. If it is successful, you’ll receive a new session state, access token, refresh token, etc. Store the new session state into your $auth store.

// $lib/supabase/client.ts
import { createClient } from "@supabase/supabase-js";
import type { Session } from "@supabase/supabase-js";
import { goto } from "$app/navigation";
import { env } from "../constants";

// Create the supabase client
export const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY);

// Auth state, initialize to null
export const auth = writable<Session | null>(null);

// Watch for auth events and store session information into the auth store
supabase.auth.onAuthStateChange((event, session) => {
  if (event === "SIGNED_IN") {
    auth.set(session);
  }
  if (event === "SIGNED_OUT") {
    auth.set(null);
    goto("/");
  }
});

// Refresh token timer
let authExpiresAt = 0;
let refreshTimer: NodeJS.Timeout | null = null;
auth.subscribe((session) => {
  // Do we have a new token with a fresh expiration time?
  if (session && session.expires_at && session.expires_at !== authExpiresAt) {
    authExpiresAt = session.expires_at;
    // Calculate the timer to within 10 minutes of the expiration timestamp
    const now = new Date().getTime();
    const timer = Math.max(1, authExpiresAt - now / 1000 - 10 * 60) * 1000;
    console.log("Refresh at", new Date(now + timer));
    console.log("Expires at", new Date(authExpiresAt * 1000));
    // Create the timer
    if (refreshTimer) clearTimeout(refreshTimer);
    refreshTimer = setTimeout(async () => {
      let newSession: Session | null = null;
      if (session?.refresh_token) {
        // Refresh the access token
        newSession = (
          await supabase.auth.signIn({
            refreshToken: session.refresh_token,
            provider: "github"
          })
        )?.session;
      }
      if (newSession) auth.set(newSession);
    }, timer);
  }
});

Create a Redirect Handler Route

This step is only necessary if authenticating with a third-party OAuth provider. When the OAuth provider returns to the app, I’ve noticed it causes weird layout shifts if you redirect directly to a protected route. To resolve this issue, I’ve created a redirect handler route that redirects once the auth session has been detected and stored.

<!-- src/routes/redirect.svelte -->
<script lang="ts">
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { auth } from "$lib/supabase/connection";
import PageMessage from "$lib/components/page/message.svelte";

let redirect = $page.url.searchParams.get("to") || "/";
let isAuthRedirect = $page.url.searchParams.get("auth");

$: {
  // If this is an auth redirect, wait until the $auth state is detected.
  if (isAuthRedirect) {
    if ($auth) goto(redirect, {
      replaceState: true
    });
  }
  // Otherwise, redirect immediately.
  else {
    goto(redirect, {
      replaceState: true
    })
  }
}
</script>
<PageMessage>
  <p>You are being redirected to:</p>
  <pre>{redirect}</pre>
</PageMessage>

Sign In Handler

I have used this implemented an automatic sign-in handler that will attempt to authenticate the user upon loading a protected route. However, you can use an event handler to do the same on a button press as well.

In my protected route layout, I use the following code to handle authentication.

<!-- src/routes/protected/__layout.svelte= -->
<script lang="ts">
import { onMount } from "svelte";
import type { User } from "@supabase/supabase-js";
import { supabase, auth } from "$lib/supabase/connection";
import { page } from "$app/stores";

let user: User | null = null;

onMount(async () => {
  if ($auth) user = $auth.user;

  if (!user) {
    return await supabase.auth.signIn(
      {
        provider: "google"
      },
      {
        redirectTo: `${$page.url.origin}/redirect?to=${encodeURIComponent($page.url.pathname)}&auth=1`
      }
    );
  }
});
</script>
{#if user}
  <slot />
{:else}
  <PageMessage>Authenticating</PageMessage>
{/if}

Sign Out Handler

Signing out is even more straightforward.

<button on:click={() => supabase.auth.signout()}>Sign Out</button>

Using the Access Token

Once you’ve logged in, and stored the session state into the $auth store, you can use the provided access token to validate requests on protected routes and APIs. You do so as you normally would be passing an Authorization header with a value like Bearer ACCESS_TOKEN. Here is an example:

// src/routes/protected/page.svelte
const response = await fetch("/protected/data", {
  method: "POST",
  headers: {
    authorization: `Bearer ${$auth?.access_token}`
  },
  body: formData
});

Then inside the endpoint, you would verify the token like this:

// src/routes/protected/data.ts
import type { RequestHandler } from "./__types/data";
import type { User } from "@supabase/supabase-js";
import { supabase } from "$lib/supabase/connection";

export const get: RequestHandler<OutputType> = async ({ request, url }) => {
  // Get access token
  const token = request.headers.get("authorization")?.replace("Bearer ", "") ?? "";
  // Validate access token and get user information
  const { user, error } = await supabase.auth.api.getUser(token);
  // Return error if the token is invalid or expired
  if (!user) return getError(`Unauthorized: ${error}`, 401);
  // Authenticate the requests
  supabase.auth.setAuth(token);

  // Fetch and return data
  const select = url.searchParams.get("select");
  return getResult(select);
};

When a 401 Unauthorized error is returned, the token has expired, and the user should be redirected to the home or login screen.

Conclusion

And that’s it! You now have authentication and protected routes.