From 8dfd621c607833ca75c01fd01237637641bb659f Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Mon, 5 Jun 2023 10:58:07 +0000 Subject: [PATCH] Update guide to include PKCE auth flow --- .../guides/auth/auth-helpers/sveltekit.mdx | 759 +++++++++++++----- 1 file changed, 572 insertions(+), 187 deletions(-) diff --git a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx index 13e109beae9..e279e528a5b 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx @@ -9,7 +9,9 @@ export const meta = { This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications. -## Installation +## Configuration + +### Install SvelteKit Auth Helpers library This library supports Node.js `^16.15.0`. @@ -17,21 +19,65 @@ This library supports Node.js `^16.15.0`. npm install @supabase/auth-helpers-sveltekit ``` -## Getting Started +### Declare Environment Variables -### Configuration +Retrieve your project's URL and anon key from your [API settings](https://app.supabase.com/project/_/settings/api), and create a `.env.local` file with the following environment variables: -Set up the following env vars. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/sveltekit/.env.example). - -```bash +```bash title=".env.local" # Find these in your Supabase project settings https://app.supabase.com/project/_/settings/api PUBLIC_SUPABASE_URL=https://your-project.supabase.co PUBLIC_SUPABASE_ANON_KEY=your-anon-key ``` -### Set up the Supabase client +### Creating a Supabase Client -Create a server supabase client in a handle hook: + + + +Create a new `hooks.server.js` file in the root of your project and populate with the following: + +```js title=src/hooks.server.js +// src/hooks.server.js +import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' +import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' + +export const handle = async ({ event, resolve }) => { + event.locals.supabase = createSupabaseServerClient({ + supabaseUrl: PUBLIC_SUPABASE_URL, + supabaseKey: PUBLIC_SUPABASE_ANON_KEY, + event, + }) + + /** + * a little helper that is written for convenience so that instead + * of calling `const { data: { session } } = await supabase.auth.getSession()` + * you just call this `await getSession()` + */ + event.locals.getSession = async () => { + const { + data: { session }, + } = await event.locals.supabase.auth.getSession() + return session + } + + return resolve(event, { + filterSerializedResponseHeaders(name) { + return name === 'content-range' + }, + }) +} +``` + + + + + +Create a new `hooks.server.ts` file in the root of your project and populate with the following: ```ts title=src/hooks.server.ts // src/hooks.server.ts @@ -59,11 +105,6 @@ export const handle: Handle = async ({ event, resolve }) => { } return resolve(event, { - /** - * There“s an issue with `filterSerializedResponseHeaders` not working when using `sequence` - * - * https://github.com/sveltejs/kit/issues/8061 - */ filterSerializedResponseHeaders(name) { return name === 'content-range' }, @@ -71,90 +112,65 @@ export const handle: Handle = async ({ event, resolve }) => { } ``` -> Note that we are specifying filterSerializedResponseHeaders here. We need to tell SvelteKit that supabase needs the content-range header. + + -### Send session to client + -In order to make the session available to the UI (pages, layouts) we need to pass the session in the root layout server load function: +Note that we are specifying filterSerializedResponseHeaders here. We need to tell SvelteKit that supabase needs the content-range header. -```ts title=src/routes/+layout.server.ts -// src/routes/+layout.server.ts -import type { LayoutServerLoad } from './$types' + -export const load: LayoutServerLoad = async ({ locals: { getSession } }) => { - return { - session: await getSession(), +### Code Exchange Route + +The `Code Exchange` route is required for the [server-side auth flow](https://supabase.com/docs/guides/auth/server-side-rendering) implemented by the SvelteKit Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase. + + + + +Create a new file at `src/routes/auth/callback/+server.js` and populate with the following: + +```jsx title="src/routes/auth/callback/+server.js" +import { redirect } from '@sveltejs/kit' + +export const GET = async ({ url, locals: { supabase } }) => { + const code = url.searchParams.get('code') + + if (code) { + await supabase.auth.exchangeCodeForSession(code) } + + throw redirect(303, '/') } ``` -### Shared Load functions and pages + -To be able to use Supabase in shared load functions and inside pages you need to create a Supabase client in the root layout load: + -```ts -// src/routes/+layout.ts -import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' -import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' -import type { LayoutLoad } from './$types' -import type { Database } from '../DatabaseDefinitions' +Create a new file at `src/routes/auth/callback/+server.ts` and populate with the following: -export const load: LayoutLoad = async ({ fetch, data, depends }) => { - depends('supabase:auth') +```tsx title="src/routes/auth/callback/+server.ts" +import { redirect } from '@sveltejs/kit' - const supabase = createSupabaseLoadClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_ANON_KEY, - event: { fetch }, - serverSession: data.session, - }) +export const GET = async ({ url, locals: { supabase } }) => { + const code = url.searchParams.get('code') - const { - data: { session }, - } = await supabase.auth.getSession() + if (code) { + await supabase.auth.exchangeCodeForSession(code) + } - return { supabase, session } + throw redirect(303, '/') } ``` -Access the client inside pages by `$page.data.supabase` or `data.supabase` when using `export let data: PageData`. - -The usage of `depends` tells sveltekit that this load function should be executed whenever `invalidate` is called to keep the page store in sync. - -`createSupabaseLoadClient` caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs. - -### Setting up the event listener on the client side - -We need to create an event listener in the root `+layout.svelte` file in order catch supabase events being triggered. - -```svelte - - - - -``` - -The usage of `invalidate` tells sveltekit that the root `+layout.ts` load function should be executed whenever the session updates to keep the page store in sync. + + ### Generate types from your database @@ -181,78 +197,323 @@ declare global { } ``` -## Client-side data fetching with RLS +## Authentication -For [row level security](https://supabase.com/docs/guides/auth/row-level-security) to work properly when fetching data client-side, you need to use `supabaseClient` from `PageData` and only run your query once the session is defined client-side: +Authentication can be initiated [client](/docs/guides/auth/auth-helpers/sveltekit#client-side) or [server-side](/docs/guides/auth/auth-helpers/sveltekit#server-side). All of the [supabase-js authentication strategies](http://localhost:3001/docs/reference/javascript/auth-api) are supported with the Auth Helpers client. -```html - +### Client-side -{#if data.session} -

client-side data fetching with RLS

-
{JSON.stringify(loadedData, null, 2)}
-{/if} -``` +#### Send session to client -## Server-side data fetching with RLS +To make the session available across the UI, including pages and layouts, it is crucial to pass the session as a parameter in the root layout's server load function. -```html - - - -
Protected content for {user.email}
-
{JSON.stringify(tableData, null, 2)}
-
{JSON.stringify(user, null, 2)}
-``` - -```ts -// src/routes/profile/+page.ts -import type { PageLoad } from './$types' -import { redirect } from '@sveltejs/kit' - -export const load: PageLoad = async ({ parent }) => { - const { supabase, session } = await parent() - if (!session) { - throw redirect(303, '/') - } - const { data: tableData } = await supabase.from('test').select('*') + + +```js title=src/routes/+layout.server.js +// src/routes/+layout.server.js +export const load = async ({ locals: { getSession } }) => { return { - user: session.user, - tableData, + session: await getSession(), } } ``` -## Protecting API routes + + + +```ts title=src/routes/+layout.server.ts +// src/routes/+layout.server.ts +export const load = async ({ locals: { getSession } }) => { + return { + session: await getSession(), + } +} +``` + + + + +#### Shared Load functions and pages + +To utilize Supabase in shared load functions and within pages, it is essential to create a Supabase client in the root layout load. + + + + +```ts title="src/routes/+layout.js" +// src/routes/+layout.js +import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' +import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' + +export const load = async ({ fetch, data, depends }) => { + depends('supabase:auth') + + const supabase = createSupabaseLoadClient({ + supabaseUrl: PUBLIC_SUPABASE_URL, + supabaseKey: PUBLIC_SUPABASE_ANON_KEY, + event: { fetch }, + serverSession: data.session, + }) + + const { + data: { session }, + } = await supabase.auth.getSession() + + return { supabase, session } +} +``` + + + + + +```ts title="src/routes/+layout.ts" +// src/routes/+layout.ts +import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' +import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' +import type { Database } from '../DatabaseDefinitions' + +export const load = async ({ fetch, data, depends }) => { + depends('supabase:auth') + + const supabase = createSupabaseLoadClient({ + supabaseUrl: PUBLIC_SUPABASE_URL, + supabaseKey: PUBLIC_SUPABASE_ANON_KEY, + event: { fetch }, + serverSession: data.session, + }) + + const { + data: { session }, + } = await supabase.auth.getSession() + + return { supabase, session } +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createSupabaseLoadClient` to add type support to the Supabase client. + + + + + + +Access the client inside pages by `$page.data.supabase` or `data.supabase` when using `export let data: PageData`. + +The usage of `depends` tells sveltekit that this load function should be executed whenever `invalidate` is called to keep the page store in sync. + +`createSupabaseLoadClient` caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs. + +#### Setting up the event listener on the client side + +We need to create an event listener in the root `+layout.svelte` file in order to catch supabase events being triggered. + +```svelte title="src/routes/+layout.svelte" + + + + +``` + +The usage of `invalidate` tells SvelteKit that the root `+layout.ts` load function should be executed whenever the session updates to keep the page store in sync. + +#### Sign in / Sign up / Sign out + +We can access the supabase instance in our `+page.svelte` file through the data object. + +```svelte title="src/routes/auth/+page.svelte" + + + +
+ + + +
+ + + +``` + +### Server-side + +[Form Actions](https://kit.svelte.dev/docs/form-actions) can be used to trigger the authentication process from form submissions. + + + + +```js title="src/routes/login/+page.server.js" +// src/routes/login/+page.server.js +export const actions = { + default: async ({ request, url, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email') + const password = formData.get('password') + + const { error } = await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${url.origin}/auth/callback`, + }, + }) + + if (error) { + return fail(500, { message: 'Server error. Try again later.', success: false, email }) + } + + return { + message: 'Please check your email for a magic link to log into the website.', + success: true, + } + }, +} +``` + +```svelte title="src/routes/login/+page.svelte" + + + +
+ + + +
+``` + +
+ + + +```js title="src/routes/login/+page.server.ts" +// src/routes/login/+page.server.ts +export const actions = { + default: async ({ request, url, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email') as string + const password = formData.get('password') as string + + const { error } = await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${url.origin}/auth/callback`, + }, + }) + + if (error) { + return fail(500, { message: 'Server error. Try again later.', success: false, email }) + } + + return { + message: 'Please check your email for a magic link to log into the website.', + success: true, + } + }, +} +``` + +```svelte title="src/routes/login/+page.svelte" + + + +
+ + + +
+``` + +
+
+ +## Authorization + +### Protecting API routes Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`. ```ts // src/routes/api/protected-route/+server.ts -import type { RequestHandler } from './$types' import { json, error } from '@sveltejs/kit' -export const GET: RequestHandler = async ({ locals: { supabase, getSession } }) => { +export const GET = async ({ locals: { supabase, getSession } }) => { const session = await getSession() if (!session) { // the user is not signed in @@ -266,16 +527,15 @@ export const GET: RequestHandler = async ({ locals: { supabase, getSession } }) If you visit `/api/protected-route` without a valid session cookie, you will get a 401 response. -## Protecting Actions +### Protecting Actions Wrap an Action to check that the user has a valid session. If they're not logged in the session is `null`. ```ts // src/routes/posts/+page.server.ts -import type { Actions } from './$types' import { error, fail } from '@sveltejs/kit' -export const actions: Actions = { +export const actions = { createPost: async ({ request, locals: { supabase, getSession } }) => { const session = await getSession() @@ -305,14 +565,148 @@ export const actions: Actions = { If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 401 error response. +### Protecting multiple routes + +To avoid writing the same auth logic in every single route you can use the handle hook to +protect multiple routes at once. + + + + +```js +// src/hooks.server.js +import { redirect, error } from '@sveltejs/kit' + +export const handle = async ({ event, resolve }) => { + // protect requests to all routes that start with /protected-routes + if (event.url.pathname.startsWith('/protected-routes')) { + const session = await event.locals.getSession() + if (!session) { + // the user is not signed in + throw redirect(303, '/') + } + } + + // protect POST requests to all routes that start with /protected-posts + if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') { + const session = await event.locals.getSession() + if (!session) { + // the user is not signed in + throw error(303, '/') + } + } + + return resolve(event) +} +``` + + + + + +```ts +// src/hooks.server.ts +import { type Handle, redirect, error } from '@sveltejs/kit' + +export const handle: Handle = async ({ event, resolve }) => { + // protect requests to all routes that start with /protected-routes + if (event.url.pathname.startsWith('/protected-routes')) { + const session = await event.locals.getSession() + if (!session) { + // the user is not signed in + throw redirect(303, '/') + } + } + + // protect POST requests to all routes that start with /protected-posts + if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') { + const session = await event.locals.getSession() + if (!session) { + // the user is not signed in + throw error(303, '/') + } + } + + return resolve(event) +} +``` + + + + +## Data fetching + +### Client-side data fetching with RLS + +For [row level security](https://supabase.com/docs/guides/auth/row-level-security) to work properly when fetching data client-side, you need to use `supabaseClient` from `PageData` and only run your query once the session is defined client-side: + +```html + + +{#if data.session} +

client-side data fetching with RLS

+
{JSON.stringify(loadedData, null, 2)}
+{/if} +``` + +### Server-side data fetching with RLS + +```html + + + +
Protected content for {user.email}
+
{JSON.stringify(tableData, null, 2)}
+
{JSON.stringify(user, null, 2)}
+``` + +```ts +// src/routes/profile/+page.ts +import { redirect } from '@sveltejs/kit' + +export const load = async ({ parent }) => { + const { supabase, session } = await parent() + if (!session) { + throw redirect(303, '/') + } + const { data: tableData } = await supabase.from('test').select('*') + + return { + user: session.user, + tableData, + } +} +``` + ## Saving and deleting the session ```ts -import type { Actions } from './$types' import { fail, redirect } from '@sveltejs/kit' import { AuthApiError } from '@supabase/supabase-js' -export const actions: Actions = { +export const actions = { signin: async ({ request, locals: { supabase } }) => { const formData = await request.formData() @@ -351,42 +745,33 @@ export const actions: Actions = { } ``` -## Protecting multiple routes +## Migration Guide [#migration] -To avoid writing the same auth logic in every single route you can use the handle hook to -protect multiple routes at once. +### Migrate to 0.10 + +#### PKCE Auth Flow + +Proof Key for Code Exchange (PKCE) is the new server-side auth flow implemented by the SvelteKit Auth Helpers. It requires a server endpoint for `/auth/callback` that exchanges an auth `code` for the user's `session`. + +Check the [Code Exchange Route steps](/docs/guides/auth/auth-helpers/sveltekit#code-exchange-route) above to implement this server endpoint. + +#### Authentication + +For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange route handler - `/auth/callback`. This is an example with the `signUp` function: ```ts -// src/hooks.server.ts -import type { RequestHandler } from './$types' -import { redirect, error } from '@sveltejs/kit' - -export const handle: Handle = async ({ event, resolve }) => { - // protect requests to all routes that start with /protected-routes - if (event.url.pathname.startsWith('/protected-routes')) { - const session = await event.locals.getSession() - if (!session) { - // the user is not signed in - throw redirect(303, '/') - } - } - - // protect POST requests to all routes that start with /protected-posts - if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') { - const session = await event.locals.getSession() - if (!session) { - // the user is not signed in - throw error(303, '/') - } - } - - return resolve(event) -} +await supabase.auth.signUp({ + email: 'jon@example.com', + password: 'sup3rs3cur3', + options: { + emailRedirectTo: 'http://localhost:3000/auth/callback', + }, +}) ``` -## Migrate from 0.8.x to 0.9 [#migration] +### Migrate from 0.8.x to 0.9 [#migration-0-9] -### Set up the Supabase client [#migration-set-up-supabase-client] +#### Set up the Supabase client [#migration-set-up-supabase-client] In version 0.9 we now setup our Supabase client for the server inside of a `hooks.server.ts` file. @@ -448,7 +833,7 @@ export const handle: Handle = async ({ event, resolve }) => { -### Initialize the client [#migration-initialize-client] +#### Initialize the client [#migration-initialize-client] In order to use the Supabase library in your client code you will need to setup a shared load function inside the root `+layout.ts` and create a `+layout.svelte` to handle our event listening for Auth events. @@ -542,11 +927,11 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => { -### Set up hooks [#migration-set-up-hooks] +#### Set up hooks [#migration-set-up-hooks] Since version 0.9 relies on `hooks.server.ts` to setup our client, we no longer need the `hooks.client.ts` in our project for Supabase related code. -### Typings [#migration-typings] +#### Typings [#migration-typings] -### Protecting a page [#migration-protecting-a-page] +#### Protecting a page [#migration-protecting-a-page] { -### Protecting a API route [#migration-protecting-a-api-route] +#### Protecting a API route [#migration-protecting-a-api-route] -## Migrate from 0.7.x to 0.8 [#migration-0-8] +### Migrate from 0.7.x to 0.8 [#migration-0-8] -### Set up the Supabase client [#migration-set-up-supabase-client-0-8] +#### Set up the Supabase client [#migration-set-up-supabase-client-0-8] -### Initialize the client [#migration-initialize-client-0-8] +#### Initialize the client [#migration-initialize-client-0-8] -### Set up hooks [#migration-set-up-hooks-0-8] +#### Set up hooks [#migration-set-up-hooks-0-8] -### Typings [#migration-typings-0-8] +#### Typings [#migration-typings-0-8] -### withPageAuth [#migration-with-page-auth-0-8] +#### withPageAuth [#migration-with-page-auth-0-8] { -### withApiAuth [#migration-with-api-auth-0-8] +#### withApiAuth [#migration-with-api-auth-0-8] { -## Migrate from 0.6.11 and below to 0.7.0 [#migration-0-7] +### Migrate from 0.6.11 and below to 0.7.0 [#migration-0-7] There are numerous breaking changes in the latest 0.7.0 version of this library. -### Environment variable prefix +#### Environment variable prefix The environment variable prefix is now `PUBLIC_` instead of `VITE_` (e.g., `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`). -### Set up the Supabase client [#migration-set-up-supabase-client-0-7] +#### Set up the Supabase client [#migration-set-up-supabase-client-0-7] -### Initialize the client [#migration-initialize-client-0-7] +#### Initialize the client [#migration-initialize-client-0-7] -### Set up hooks [#migration-set-up-hooks-0-7] +#### Set up hooks [#migration-set-up-hooks-0-7] -### Typings [#migration-typings-0-7] +#### Typings [#migration-typings-0-7] -### Check the user on the client +#### Check the user on the client -### withPageAuth +#### withPageAuth -### withApiAuth +#### withApiAuth