Skip to content

Commit

Permalink
feat(auth): improve client flexibility & allow overriding cookie opti…
Browse files Browse the repository at this point in the history
…ons (#463)
  • Loading branch information
Jordan-Ellis authored Feb 28, 2025
1 parent a4b244b commit 254cbdb
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 62 deletions.
56 changes: 50 additions & 6 deletions docs/content/2.get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ Default: `process.env.SUPABASE_SERVICE_KEY`
Supabase 'service role key', has super admin rights and can bypass your Row Level Security.
### `useSsrCookies`
Default: `true`
Controls whether the module uses cookies to share session info between server and client. You *must* enable this option if you need to access session or user info from the server. It will use the SSR client from the [@supabase/ssr](https://github.com/supabase/ssr) library.
When disabled, the module will use the default Supabase client from the [@supabase/supabase-js](https://github.com/supabase/supabase-js) library which stores session info in local storage. This is useful in certain cases, such as statically generated sites or mobile apps where cookies might not be available.
::callout{icon="i-heroicons-exclamation-triangle-20-solid"}
When `useSsrCookies` is `true` the following options cannot be customized with `clientOptions`:
- `flowType`, `autoRefreshToken`, `detectSessionInUrl`, `persistSession`, `storage`
If you need to customize one of the above options, you must set `useSsrCookies` to `false`. Bear in mind that this will disable SSR support, you'll need to take care of it yourself if needed.
::
### `redirect`
Default: `true`
Expand All @@ -86,21 +102,27 @@ Default:
callback: '/confirm',
include: undefined,
exclude: [],
cookieRedirect: false,
saveRedirectToCookie: false,
}
```
- `login`: User will be redirected to this path if not authenticated or after logout.
- `callback`: This is the path the user will be redirect to after supabase login redirection. Should match configured `redirectTo` option of your [signIn method](https://supabase.com/docs/reference/javascript/auth-signinwithoauth). Should also be configured in your Supabase dashboard under `Authentication -> URL Configuration -> Redirect URLs`.
- `include`: Routes to include in the redirect. `['/admin(/*)?']` will enable the redirect only for the `admin` page and all sub-pages.
- `exclude`: Routes to exclude from the redirect. `['/foo', '/bar/**']` will exclude the `foo` page and all pages in your `bar` folder.
- `cookieRedirect`: Sets a cookie containing the path an unauthenticated user tried to access. The cookie can then be used on the [`/confirm`](https://supabase.nuxtjs.org/authentication#confirm-page-confirm) page to redirect the user to the page they previously tried to visit.
- `saveRedirectToCookie`: Automatically sets a cookie containing the path an unauthenticated user tried to access. The value can be accessed using the [`useSupabaseCookieRedirect`](/usage/composables/useSupabaseCookieRedirect) composable to redirect the user to the page they previously tried to visit.
### `cookieName`
### `cookiePrefix`
Default: `sb`
Default: `sb-{your-project-id}-auth-token`
The prefix used for all supabase cookies, and the redirect cookie.
Cookie name used for storing the redirect path when using the `redirect` option, added in front of `-redirect-path` to form the full cookie name e.g. `sb-redirect-path`
By default, the cookie prefix is the same as the default storage key from the supabase-js client. This includes your project id, and will look something like this: `sb-ttc1xwfwkoleowdqayb19-auth-token`
::callout{icon="i-heroicons-light-bulb"}
If the `useSsrCookies` option is set to `false`, this option will not affect supabase, and only apply to the `useSupabaseCookieRedirect` cookie.
::
### `cookieOptions`
Expand Down Expand Up @@ -140,7 +162,9 @@ Default:
clientOptions: { }
```
Supabase client options [available here](https://supabase.com/docs/reference/javascript/initializing#parameters) merged with default values from `@supabase/ssr`:
Customize the Supabase client options [available here](https://supabase.com/docs/reference/javascript/initializing#parameters).
If `useSsrCookies` is enabled, these options will be merged with values from `@supabase/ssr`, and some of the [options cannot be customized](/get-started#usessrcookies).
```ts
clientOptions: {
Expand All @@ -153,6 +177,26 @@ Supabase client options [available here](https://supabase.com/docs/reference/jav
}
```
### `cookieName`
::callout{color="amber" icon="i-heroicons-exclamation-triangle-20-solid"}
This option is deprecated, use [`cookiePrefix`](/get-started#cookieprefix) instead.
::
Default: `sb`
Cookie name used for storing the redirect path when using the `redirectOptions.cookieRedirect` option, added in front of `-redirect-path` to form the full cookie name e.g. `sb-redirect-path`
### `redirectOptions.cookieRedirect`
::callout{color="amber" icon="i-heroicons-exclamation-triangle-20-solid"}
This option is deprecated, use [`redirectOptions.saveRedirectToCookie`](/get-started#redirectoptions) instead.
::
Default: `false`
Use the `cookieRedirect` option to store the redirect path in a cookie.
## Demo
A live demo is made for see this module in action on [n3-supabase.netlify.app](https://n3-supabase.netlify.app), read more in the [demo section](/demo).
Expand Down
26 changes: 13 additions & 13 deletions docs/content/3.authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Once the authorization flow is triggered using the `auth` wrapper of the [useSup

## Confirm page - `/confirm`

The confirmation page receives the supabase callback. From there you can check the user value and redirect to the appropriate page.
The confirmation page receives the supabase callback which contains session information. The supabase client automatically detects and handles this, and once the session is confirmed the user value will automatically be updated. From there you can redirect to the appropriate page.

::callout{icon="i-heroicons-light-bulb"}
The redirect URL must be configured in your Supabase dashboard under `Authentication -> URL Configuration -> Redirect URLs`.
Expand All @@ -79,30 +79,30 @@ watch(user, () => {

### Redirect path

You can easily handle redirection to the initial requested route after login.
You can easily handle redirection to the initial requested route after login using the [`useSupabaseCookieRedirect`](/usage/composables/usesupabasecookieredirect) composable and the [`saveRedirectToCookie`](/get-started#redirectoptions) option.

::callout{icon="i-heroicons-light-bulb"}
You must enable the `cookieRedirect` option of the [redirectOptions](/get-started#redirectoptions) to allow cookie storage and take benefit of this feature.
::
By setting the `saveRedirectToCookie` option to `true`, the module will automatically save the current path to a cookie when the user is redirected to the login page. When the user logs in, you can then retrieve the saved path from the cookie and redirect the user to it on the [`/confirm`](/authentication#confirm-page-confirm) page:

```vue [pages/confirm.vue]
<script setup lang="ts">
const user = useSupabaseUser()
// Get redirect path from cookies
const cookieName = useRuntimeConfig().public.supabase.cookieName
const redirectPath = useCookie(`${cookieName}-redirect-path`).value
const redirectInfo = useSupabaseCookieRedirect()
watch(user, () => {
if (user.value) {
// Clear cookie
useCookie(`${cookieName}-redirect-path`).value = null
// Redirect to path
return navigateTo(redirectPath || '/');
// Get redirect path, and clear it from the cookie
const path = redirectInfo.pluck()
// Redirect to the saved path, or fallback to home
return navigateTo(path || '/')
}
}, { immediate: true })
</script>
<template>
<div>Waiting for login...</div>
</template>
```

::callout{icon="i-heroicons-light-bulb"}
If you want to manually set the redirect path, you can do so by disabling [`saveRedirectToCookie`](/get-started#redirectoptions), and then set the value using the [`useSupabaseCookieRedirect`](/usage/composables/usesupabasecookieredirect) composable directly.
::
54 changes: 54 additions & 0 deletions docs/content/4.usage/composables/useSupabaseCookieRedirect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: useSupabaseCookieRedirect
description: Handle redirecting users to the page they previously tried to visit after login
---

The `useSupabaseCookieRedirect` composable provides a simple way to handle storing and retrieving a redirect path with a cookie.

## Usage

This composable can be [manually used](#manual-usage) to save and retrieve a redirect path. However, the redirect path can automatically be set via the `saveRedirectToCookie` option in the [redirectOptions](/get-started#redirectoptions).

The redirect path is not automatically used. Instead you must implement the logic to redirect the user to the saved path, for example on the [`/confirm`](/authentication#confirm-page-confirm) page.

```vue
<script setup>
const user = useSupabaseUser()
const redirectInfo = useSupabaseCookieRedirect()
watch(user, () => {
if (user.value) {
// Get the saved path and clear it from the cookie
const path = redirectInfo.pluck()
// Redirect to the saved path, or fallback to home
return navigateTo(path || '/')
}
}, { immediate: true })
</script>
```

## Return Values

The composable returns an object with the following properties:

- `path`: A reactive cookie reference containing the redirect path. Can be both read and written to.
- `pluck()`: A convenience method that returns the current redirect path and clears it from the cookie.

## Manual Usage

You can also manually set and read the redirect path:

```ts
const redirectInfo = useSupabaseCookieRedirect()

// Save a specific path
redirectInfo.path.value = '/dashboard'

// Read the current path without clearing it
const currentPath = redirectInfo.path.value

// Get the path and clear it
const path = redirectInfo.pluck()
```

The cookie is saved with the name `{cookiePrefix}-redirect-path` where `cookiePrefix` is defined in the [runtime config](/get-started#runtime-config).
53 changes: 48 additions & 5 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fileURLToPath } from 'node:url'
import fs from 'node:fs'
import { defu } from 'defu'
import { defineNuxtModule, addPlugin, createResolver, addTemplate, extendViteConfig } from '@nuxt/kit'
import { defineNuxtModule, addPlugin, createResolver, addTemplate, extendViteConfig, useLogger } from '@nuxt/kit'
import type { CookieOptions } from 'nuxt/app'
import type { SupabaseClientOptions } from '@supabase/supabase-js'
import type { NitroConfig } from 'nitropack'
Expand Down Expand Up @@ -60,9 +60,27 @@ export interface ModuleOptions {
* Cookie name used for storing the redirect path when using the `redirect` option, added in front of `-redirect-path` to form the full cookie name e.g. `sb-redirect-path`
* @default 'sb'
* @type string
* @deprecated Use `cookiePrefix` instead.
*/
cookieName?: string

/**
* The prefix used for all supabase cookies, and the redirect cookie.
* @default The default storage key from the supabase-js client.
* @type string
*/
cookiePrefix?: string

/**
* If true, the supabase client will use cookies to store the session, allowing the session to be used from the server in ssr mode.
* Some `clientOptions` are not configurable when this is enabled. See the docs for more details.
*
* If false, the server will not be able to access the session.
* @default true
* @type boolean
*/
useSsrCookies?: boolean

/**
* Cookie options
* @default {
Expand Down Expand Up @@ -109,8 +127,11 @@ export default defineNuxtModule<ModuleOptions>({
callback: '/confirm',
exclude: [],
cookieRedirect: false,
saveRedirectToCookie: false,
},
cookieName: 'sb',
cookiePrefix: undefined,
useSsrCookies: true,
cookieOptions: {
maxAge: 60 * 60 * 8,
sameSite: 'lax',
Expand All @@ -120,6 +141,7 @@ export default defineNuxtModule<ModuleOptions>({
clientOptions: {} as SupabaseClientOptions<string>,
},
setup(options, nuxt) {
const logger = useLogger('@nuxt/supabase')
const { resolve, resolvePath } = createResolver(import.meta.url)

// Public runtimeConfig
Expand All @@ -129,6 +151,8 @@ export default defineNuxtModule<ModuleOptions>({
redirect: options.redirect,
redirectOptions: options.redirectOptions,
cookieName: options.cookieName,
cookiePrefix: options.cookiePrefix,
useSsrCookies: options.useSsrCookies,
cookieOptions: options.cookieOptions,
clientOptions: options.clientOptions,
})
Expand All @@ -138,12 +162,31 @@ export default defineNuxtModule<ModuleOptions>({
serviceKey: options.serviceKey,
})

// Make sure url and key are set
if (!nuxt.options.runtimeConfig.public.supabase.url) {
console.warn('Missing supabase url, set it either in `nuxt.config.js` or via env variable')
const finalUrl = nuxt.options.runtimeConfig.public.supabase.url

// Warn if the url isn't set.
if (!finalUrl) {
logger.warn('Missing supabase url, set it either in `nuxt.config.js` or via env variable')
}
else {
// Use the default storage key as defined by the supabase-js client if no cookiePrefix is set.
// Source: https://github.com/supabase/supabase-js/blob/3316f2426d7c2e5babaab7ddc17c30bfa189f500/src/SupabaseClient.ts#L86
const defaultStorageKey = `sb-${new URL(finalUrl).hostname.split('.')[0]}-auth-token`
const currentPrefix = nuxt.options.runtimeConfig.public.supabase.cookiePrefix
nuxt.options.runtimeConfig.public.supabase.cookiePrefix = currentPrefix || defaultStorageKey
}

// Warn if the key isn't set.
if (!nuxt.options.runtimeConfig.public.supabase.key) {
console.warn('Missing supabase anon key, set it either in `nuxt.config.js` or via env variable')
logger.warn('Missing supabase anon key, set it either in `nuxt.config.js` or via env variable')
}

// Warn for deprecated features.
if (nuxt.options.runtimeConfig.public.supabase.redirectOptions.cookieRedirect) {
logger.warn('The `cookieRedirect` option is deprecated, use `saveRedirectToCookie` instead.')
}
if (nuxt.options.runtimeConfig.public.supabase.cookieName != 'sb') {
logger.warn('The `cookieName` option is deprecated, use `cookiePrefix` instead.')
}

// ensure callback URL is not using SSR
Expand Down
40 changes: 40 additions & 0 deletions src/runtime/composables/useSupabaseCookieRedirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { CookieRef } from 'nuxt/app'
import { useRuntimeConfig, useCookie } from '#imports'

export interface UseSupabaseCookieRedirectReturn {
/**
* The reactive value of the redirect path cookie.
* Can be both read and written to.
*/
path: CookieRef<string | null>
/**
* Get the current redirect path cookie value, then clear it
*/
pluck: () => string | null
}

export const useSupabaseCookieRedirect = (): UseSupabaseCookieRedirectReturn => {
const config = useRuntimeConfig().public.supabase

// Use cookiePrefix if saveRedirectToCookie is true, otherwise fallback to the deprecated cookieName
const prefix = config.redirectOptions.saveRedirectToCookie
? config.cookiePrefix
: config.cookieName

const cookie: CookieRef<string | null> = useCookie(
`${prefix}-redirect-path`,
{
...config.cookieOptions,
readonly: false,
},
)

return {
path: cookie,
pluck: () => {
const value = cookie.value
cookie.value = null
return value
},
}
}
13 changes: 7 additions & 6 deletions src/runtime/plugins/auth-redirect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useSupabaseCookieRedirect } from '../composables/useSupabaseCookieRedirect'
import type { Plugin } from '#app'
import type { Ref } from '#imports'
import { defineNuxtPlugin, addRouteMiddleware, defineNuxtRouteMiddleware, useCookie, useRuntimeConfig, navigateTo, useSupabaseSession } from '#imports'
import { defineNuxtPlugin, addRouteMiddleware, defineNuxtRouteMiddleware, useRuntimeConfig, navigateTo, useSupabaseSession } from '#imports'
import type { RouteLocationNormalized } from '#vue-router'

export default defineNuxtPlugin({
Expand All @@ -10,8 +10,7 @@ export default defineNuxtPlugin({
'global-auth',
defineNuxtRouteMiddleware((to: RouteLocationNormalized) => {
const config = useRuntimeConfig().public.supabase
const { login, callback, include, exclude, cookieRedirect } = config.redirectOptions
const { cookieName, cookieOptions } = config
const { login, callback, include, exclude, cookieRedirect, saveRedirectToCookie } = config.redirectOptions

// Redirect only on included routes (if defined)
if (include && include.length > 0) {
Expand All @@ -33,8 +32,10 @@ export default defineNuxtPlugin({

const session = useSupabaseSession()
if (!session.value) {
if (cookieRedirect) {
(useCookie(`${cookieName}-redirect-path`, { ...cookieOptions, readonly: false }) as Ref).value = to.fullPath
// Save current path to the redirect cookie if enabled
if (cookieRedirect || saveRedirectToCookie) {
const redirectInfo = useSupabaseCookieRedirect()
redirectInfo.path.value = to.fullPath
}

return navigateTo(login)
Expand Down
Loading

0 comments on commit 254cbdb

Please sign in to comment.