From 254cbdb1a44f9cb40f9057d03ef93bd73020009d Mon Sep 17 00:00:00 2001
From: Jordan Ellis <18269476+Jordan-Ellis@users.noreply.github.com>
Date: Fri, 28 Feb 2025 07:31:09 -0700
Subject: [PATCH] feat(auth): improve client flexibility & allow overriding
cookie options (#463)
---
docs/content/2.get-started.md | 56 +++++++++++++++++--
docs/content/3.authentication.md | 26 ++++-----
.../composables/useSupabaseCookieRedirect.md | 54 ++++++++++++++++++
src/module.ts | 53 ++++++++++++++++--
.../composables/useSupabaseCookieRedirect.ts | 40 +++++++++++++
src/runtime/plugins/auth-redirect.ts | 13 +++--
src/runtime/plugins/supabase.client.ts | 38 +++++++++----
src/runtime/plugins/supabase.server.ts | 29 ++++++----
.../server/services/serverSupabaseClient.ts | 14 ++---
src/types/index.ts | 11 ++++
10 files changed, 272 insertions(+), 62 deletions(-)
create mode 100644 docs/content/4.usage/composables/useSupabaseCookieRedirect.md
create mode 100644 src/runtime/composables/useSupabaseCookieRedirect.ts
diff --git a/docs/content/2.get-started.md b/docs/content/2.get-started.md
index 37d28d371..d19769bdc 100644
--- a/docs/content/2.get-started.md
+++ b/docs/content/2.get-started.md
@@ -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`
@@ -86,7 +102,7 @@ Default:
callback: '/confirm',
include: undefined,
exclude: [],
- cookieRedirect: false,
+ saveRedirectToCookie: false,
}
```
@@ -94,13 +110,19 @@ Default:
- `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`
@@ -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: {
@@ -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).
diff --git a/docs/content/3.authentication.md b/docs/content/3.authentication.md
index 9167d53ae..8ce5b46d2 100644
--- a/docs/content/3.authentication.md
+++ b/docs/content/3.authentication.md
@@ -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`.
@@ -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]
+
Waiting for login...
```
+
+::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.
+::
diff --git a/docs/content/4.usage/composables/useSupabaseCookieRedirect.md b/docs/content/4.usage/composables/useSupabaseCookieRedirect.md
new file mode 100644
index 000000000..3188f0ea1
--- /dev/null
+++ b/docs/content/4.usage/composables/useSupabaseCookieRedirect.md
@@ -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
+
+```
+
+## 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).
\ No newline at end of file
diff --git a/src/module.ts b/src/module.ts
index 4bf9d342f..d2f66c3d0 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -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'
@@ -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 {
@@ -109,8 +127,11 @@ export default defineNuxtModule({
callback: '/confirm',
exclude: [],
cookieRedirect: false,
+ saveRedirectToCookie: false,
},
cookieName: 'sb',
+ cookiePrefix: undefined,
+ useSsrCookies: true,
cookieOptions: {
maxAge: 60 * 60 * 8,
sameSite: 'lax',
@@ -120,6 +141,7 @@ export default defineNuxtModule({
clientOptions: {} as SupabaseClientOptions,
},
setup(options, nuxt) {
+ const logger = useLogger('@nuxt/supabase')
const { resolve, resolvePath } = createResolver(import.meta.url)
// Public runtimeConfig
@@ -129,6 +151,8 @@ export default defineNuxtModule({
redirect: options.redirect,
redirectOptions: options.redirectOptions,
cookieName: options.cookieName,
+ cookiePrefix: options.cookiePrefix,
+ useSsrCookies: options.useSsrCookies,
cookieOptions: options.cookieOptions,
clientOptions: options.clientOptions,
})
@@ -138,12 +162,31 @@ export default defineNuxtModule({
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
diff --git a/src/runtime/composables/useSupabaseCookieRedirect.ts b/src/runtime/composables/useSupabaseCookieRedirect.ts
new file mode 100644
index 000000000..32cfc56db
--- /dev/null
+++ b/src/runtime/composables/useSupabaseCookieRedirect.ts
@@ -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
+ /**
+ * 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 = useCookie(
+ `${prefix}-redirect-path`,
+ {
+ ...config.cookieOptions,
+ readonly: false,
+ },
+ )
+
+ return {
+ path: cookie,
+ pluck: () => {
+ const value = cookie.value
+ cookie.value = null
+ return value
+ },
+ }
+}
diff --git a/src/runtime/plugins/auth-redirect.ts b/src/runtime/plugins/auth-redirect.ts
index 3ef7965ef..d13daa921 100644
--- a/src/runtime/plugins/auth-redirect.ts
+++ b/src/runtime/plugins/auth-redirect.ts
@@ -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({
@@ -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) {
@@ -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)
diff --git a/src/runtime/plugins/supabase.client.ts b/src/runtime/plugins/supabase.client.ts
index 35b8036f3..65fbc1bd7 100644
--- a/src/runtime/plugins/supabase.client.ts
+++ b/src/runtime/plugins/supabase.client.ts
@@ -1,5 +1,5 @@
import { createBrowserClient } from '@supabase/ssr'
-import type { Session, SupabaseClient } from '@supabase/supabase-js'
+import { type Session, type SupabaseClient, createClient } from '@supabase/supabase-js'
import { fetchWithRetry } from '../utils/fetch-retry'
import type { Plugin } from '#app'
import { defineNuxtPlugin, useRuntimeConfig, useSupabaseSession, useSupabaseUser } from '#imports'
@@ -8,17 +8,33 @@ export default defineNuxtPlugin({
name: 'supabase',
enforce: 'pre',
async setup({ provide }) {
- const { url, key, cookieOptions, clientOptions } = useRuntimeConfig().public.supabase
+ const { url, key, cookieOptions, cookiePrefix, useSsrCookies, clientOptions } = useRuntimeConfig().public.supabase
- const client = createBrowserClient(url, key, {
- ...clientOptions,
- cookieOptions,
- isSingleton: true,
- global: {
- fetch: fetchWithRetry,
- ...clientOptions.global,
- },
- })
+ let client
+
+ if (useSsrCookies) {
+ client = createBrowserClient(url, key, {
+ ...clientOptions,
+ cookieOptions: {
+ ...cookieOptions,
+ name: cookiePrefix,
+ },
+ isSingleton: true,
+ global: {
+ fetch: fetchWithRetry,
+ ...clientOptions.global,
+ },
+ })
+ }
+ else {
+ client = createClient(url, key, {
+ ...clientOptions,
+ global: {
+ fetch: fetchWithRetry,
+ ...clientOptions.global,
+ },
+ })
+ }
provide('supabase', { client })
diff --git a/src/runtime/plugins/supabase.server.ts b/src/runtime/plugins/supabase.server.ts
index 030d85f0c..4977cff2a 100644
--- a/src/runtime/plugins/supabase.server.ts
+++ b/src/runtime/plugins/supabase.server.ts
@@ -10,7 +10,7 @@ export default defineNuxtPlugin({
name: 'supabase',
enforce: 'pre',
async setup({ provide }) {
- const { url, key, cookieOptions, clientOptions } = useRuntimeConfig().public.supabase
+ const { url, key, cookiePrefix, useSsrCookies, cookieOptions, clientOptions } = useRuntimeConfig().public.supabase
const event = useRequestEvent()!
@@ -26,7 +26,10 @@ export default defineNuxtPlugin({
}[],
) => cookies.forEach(({ name, value, options }) => setCookie(event, name, value, options)),
},
- cookieOptions,
+ cookieOptions: {
+ ...cookieOptions,
+ name: cookiePrefix,
+ },
global: {
fetch: fetchWithRetry,
...clientOptions.global,
@@ -35,16 +38,18 @@ export default defineNuxtPlugin({
provide('supabase', { client })
- // Initialize user and session states
- const [
- session,
- user,
- ] = await Promise.all([
- serverSupabaseSession(event).catch(() => null),
- serverSupabaseUser(event).catch(() => null),
- ])
+ // Initialize user and session states if available.
+ if (useSsrCookies) {
+ const [
+ session,
+ user,
+ ] = await Promise.all([
+ serverSupabaseSession(event).catch(() => null),
+ serverSupabaseUser(event).catch(() => null),
+ ])
- useSupabaseSession().value = session
- useSupabaseUser().value = user
+ useSupabaseSession().value = session
+ useSupabaseUser().value = user
+ }
},
}) as Plugin<{ client: SupabaseClient }>
diff --git a/src/runtime/server/services/serverSupabaseClient.ts b/src/runtime/server/services/serverSupabaseClient.ts
index 8863d5876..8163cb2eb 100644
--- a/src/runtime/server/services/serverSupabaseClient.ts
+++ b/src/runtime/server/services/serverSupabaseClient.ts
@@ -10,14 +10,7 @@ export const serverSupabaseClient: (event: H3Event) => Promise(event: H3Event) => Promise cookies.forEach(({ name, value, options }) => setCookie(event, name, value, options)),
},
- cookieOptions,
+ cookieOptions: {
+ ...cookieOptions,
+ name: cookiePrefix,
+ },
global: {
fetch: fetchWithRetry,
...global,
diff --git a/src/types/index.ts b/src/types/index.ts
index 05b9a3167..cd9c5a891 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -9,6 +9,8 @@ declare module '@nuxt/schema' {
redirect: boolean
redirectOptions: RedirectOptions
cookieName: string
+ cookiePrefix: string
+ useSsrCookies: boolean
cookieOptions: CookieOptions
types: string | false
clientOptions: SupabaseClientOptions
@@ -21,5 +23,14 @@ export interface RedirectOptions {
callback: string
include?: string[]
exclude?: string[]
+ /**
+ * @deprecated Use `saveRedirectToCookie` instead.
+ */
cookieRedirect?: boolean
+
+ /**
+ * If true, when automatically redirected the redirect path will be saved to a cookie, allowing retrieval later with the `useSupabaseRedirect` composable.
+ * @default false
+ */
+ saveRedirectToCookie?: boolean
}