Part 2: Unlocking User Management in SvelteKit+Supabase
We'll cover how to handle user authentication in this part 2 series.
How supabase simplifies data security for our startup
With policies, your database becomes the rules engine. Instead of repetitively filtering your queries, like this ...
const loggedInUserId = 'd0714948'
let { data, error } = await supabase
.from('users')
.select('user_id, name')
.eq('user_id', loggedInUserId)
// console.log(data)
// => { id: 'd0714948', name: 'Jane' }
... you can simply define a rule on your database table, auth.uid() = user_id
, and your request will return the rows which pass the rule.
How it works
- A user signs up. Supabase creates a new user in the
auth.users
table. - Supabase returns a new JWT, which contains the user's
UUID
. - Every request to your database also sends the JWT.
- Postgres inspects the JWT to determine the user making the request.
- The user's UID can be used in policies to restrict access to rows.
Supabase provides a special function in Postgres, auth.uid()
, which extracts the user's UID from the JWT. This is especially useful when creating policies.
User authentication in the app
In our sample app, we are making authentication optional on the marketing pages & mandatory on app pages. We'll work in that logic as we work through creating user accounts in this part 2.
First let's create our signup page. First we're going to create a folder within the routes
directory that will wrap all of our authentication pages. This allows us to setup a custom +layout.svelte
for all authentication pages to keep them looking similar. Let's create src/routes/(auth)
which is the folder wrapper that contains all of our authentication pages.
Let's create the src/routes/(auth)/+layout.svelte
page to serve as the wrapper for all of our authentication pages.
Now we'll create the signup page at src/routes/(auth)/signup/+page.svelte
:
<script lang="ts">
import { goto } from '$app/navigation';
import type { PageData } from './$types';
export let data: PageData;
$: ({ supabase } = data);
let loading = false;
let name: string;
let email: string;
let password: string;
async function handleLogin() {
try {
loading = true;
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: { name }
}
});
if (error) throw error;
goto(`/verify?email=${email}`);
} catch (error) {
if (error instanceof Error) {
alert(error.message);
}
} finally {
loading = false;
}
};
</script>
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Create an account
</h2>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form class="py-4 space-y-6" on:submit|preventDefault={handleLogin}>
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<div class="mt-1">
<input
type="text"
autocomplete="name"
placeholder="Your name"
bind:value={name}
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500 sm:text-sm"
/>
</div>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email address</label>
<div class="mt-1">
<input
type="email"
placeholder="Your email"
bind:value={email}
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500 sm:text-sm"
/>
</div>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<div class="mt-1">
<input
type="password"
placeholder="Your password"
bind:value={password}
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500 sm:text-sm"
/>
</div>
</div>
<div>
<button
disabled={loading}
type="submit"
class="flex w-full justify-center rounded-md border border-transparent bg-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
>
{#if loading}
Creating account...
{:else}
Create account
{/if}</button
>
</div>
</form>
</div>
</div>
</div>
Ok now let's test it out by going to localhost:5173/signup
and creating an account. After putting in your name, email, and a password, click sign in. Within the studio at http://localhost:54323/project/default/auth/users, you should see your user account created. You should also see an email within the monitor at http://localhost:54324/monitor containing your verification email. This monitor is a mock emailer listener that handles email notifications for authentication testing locally.
When you click the link to verify your email, it'll take you to a localhost:3000
callback link. We need to modify the supabase/config.toml
file to reflect our specific local environment. Make the following changes:
[auth]
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://localhost:5173"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://localhost:5173"]
You'll need to restart your local dev instance in order to apply these changes:
npx supabase stop && npx supabase start
Try creating a new account at http://localhost:5173/signup. Once you click on the email link you should be directed to the home page with a valid session.
For production purposes, we need to create a verify code page that the user is directed to after signing up & when they click the link. Some email providers scan links which break the "magic links"/"one time link" functionality of our email links we send out. For those users, we'll have a verify link where they can manually enter the OTP code.
Create the verify page at src/routes/(auth)/verify/+page.svelte
with the following code:
<script lang="ts">
import { goto } from '$app/navigation';
import type { PageData } from './$types';
export let data: PageData;
$: ({ supabase } = data);
let loading = false;
let token: string;
const verify = async () => {
try {
loading = true;
const { error } = await supabase.auth.verifyOtp({
email: data.email!,
token,
type: 'signup'
});
if (error) throw error;
await goto('/');
} catch (error) {
return alert('Verification code is invalid. Try signing in again.');
} finally {
loading = false;
}
};
if (data.token) {
token = data.token;
}
</script>
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Verify your email address
</h2>
<p class="mt-4 text-center text-md text-gray-500">
Check your email for a magic link. If the link does not log you in automatically, enter the
verification code below.
</p>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form class="py-4 space-y-6" on:submit|preventDefault={verify}>
<div>
<label for="email" class="block text-sm font-medium text-gray-700"
>Verification code</label
>
<div class="mt-1">
<input
type="text"
autocomplete="off"
placeholder="Code provided in email"
bind:value={token}
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500 sm:text-sm"
/>
</div>
<p class="mt-1 text-sm text-left text-gray-500">
You should receive an email containing your sign in code at {' '}
<span class="font-semibold">{data.email}</span>
</p>
</div>
<div>
<button
disabled={loading}
type="submit"
class="flex w-full justify-center rounded-md border border-transparent bg-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
>
{#if loading}
Signing in...
{:else}
Verify
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
<p class="mt-10 text-center text-sm text-gray-500">
Didn't receive an email?
<a href="/login" class="font-semibold leading-6 text-green-600 hover:text-green-500"
>Try signing in again.</a
>
</p>
In order to populate the PageData
with the required email
and token
we need to perform a OTP login, we'll retrieve the parameters from the URL. Create a server load page at src/routes/(auth)/verify/+page.server.ts
:
// src/routes/+layout.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const session = await event.locals.getSession();
if (session) {
throw redirect(303, '/');
}
return {
email: event.url.searchParams.get('email'),
token: event.url.searchParams.get('token'),
error: event.url.searchParams.get('error')
};
};
Ok now create a new account via the signup flow. When checking the monitor for an email, instead of clicking the link, copy the code. Paste it into the verify box. You should now have a working verify page. We'll reuse this similar logic for a password reset as well. But first let's create a login page.
Next, we'll create the login page at src/routes/(auth)/login/+page.svelte
with the following code:
Try logging in with your created account credentials and you should be routed to the home page. Now let's add a check on both the /login
and /signup
pages to see if the user is logged in. We'll direct them to /
if so.
Create a src/routes/(auth)/login/+page.server.ts
and src/routes/(auth)/signup/+page.server.ts
with the same following typescript code:
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const session = await event.locals.getSession();
if (session) {
throw redirect(303, '/');
}
return {};
};
If you now try to go to http://localhost:5173/login you'll notice you get redirected to http://localhost:5173/. We can add a logout button to src/routes/+page.svelte
by modifying it to:
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
$: ({ supabase, session } = data);
async function logout() {
await supabase.auth.signOut();
}
</script>
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<button on:click={logout}> logout</button>
<pre>{JSON.stringify(session)}</pre>
Clicking logout now clears the session. You can go to http://localhost:5173/login and re-login again.
Forgot password functionality
Now let's build out the reset password functionality. First let's build out a link in the /login
page to allow users to go to the /forgot-password
page.
Create the following files:
Add a redirect for logged in users so they aren't triggering password resets:
And lastly, lets create the page to allow users to change their password upon redirect from the password reset email
Social authentication w/ Google sign in
Let's add in google sign in to our application. To do that, first let's get an oauth credential. Create a new project via google's developer console and then create an oauth 2.0 key.
Be sure to add the following urls to the authorized redirect urls (replacing xxx with your supabase project id):
https://xxxxxxxxx.supabase.co/auth/v1/callback
http://localhost:54321/auth/v1/callback
Next, add the following configuration to to the config.toml
# supabase/config.toml
[auth.external.google]
enabled = true
client_id = "1058874065202-9dtif4drc6bkd35lobtoktvo5xxxxxx.apps.googleusercontent.com"
secret = "GOCSPX-YU8MZTCqnIK9y_xxxxxxxxx_O"
# Overrides the default auth redirectUrl.
# redirect_uri = ""
After changing your configuration file, be sure to restart your supabase instance to take effect: npx supabase stop && npx supabase start
Go ahead and click sign in with google and you should be able to redirect to the logged in page.
UPDATE: Leveraging server side PKCE
First let's upgrade the supabase sveltekit auth helper:
Next we'll create the authentication callback to exchange a code for a session by creating the callback endpoint
Lastly let's update each callback url for login/signup to redirect to the /auth/callback url.