Repository
@supabase/ssr v0.6.1 (also affects @supabase/auth-js v2.70.0 error handling path)
Describe the bug
createBrowserClient silently overrides the user-supplied auth.detectSessionInUrl and auth.flowType options. The user's options are spread before the library's hardcoded values, so the library always wins:
// @supabase/ssr/dist/module/createBrowserClient.js (lines 29-39)
auth: {
...options?.auth, // user's settings (spread first)
flowType: "pkce", // overrides user's flowType
autoRefreshToken: isBrowser(), // overrides user's autoRefreshToken
detectSessionInUrl: isBrowser(), // overrides user's detectSessionInUrl
persistSession: true,
storage,
},
This means:
detectSessionInUrl: false is ignored — it is always true in the browser.
flowType is always forced to "pkce", regardless of the user's setting.
Why this is a problem
The combination of these two overrides creates a destructive code path when a page loads with Supabase auth-related hash fragments in the URL (e.g. #access_token=... or #error=access_denied&error_code=otp_expired&...):
_initialize() in @supabase/auth-js parses the URL hash.
_isImplicitGrantCallback() returns true (because access_token or error_description is present in the hash).
- Since
detectSessionInUrl was forced to true, the client enters the URL-processing code path.
_getSessionFromURL() is called.
- If the hash contains error params (
error, error_description, error_code): throws AuthImplicitGrantRedirectError.
- If the hash contains tokens (
access_token, refresh_token): the flow-type check detects a mismatch (implicit URL vs forced pkce config) and throws AuthPKCEGrantCodeExchangeError.
- Both error types fall through to:
// failed login attempt via url
await this._removeSession();
- The existing valid session is destroyed — the cookie is set to
Max-Age=0 and deleted from the browser.
How auth hash fragments end up on unrelated pages
This happens naturally through HTTP redirect fragment inheritance (RFC 7231 Section 7.1.2):
If the Location value provided in a 3xx response does not have a fragment component, a user agent MUST process the redirection as if the value inherits the fragment component of the URI reference used to generate the request target.
Example scenario with Next.js middleware:
- Supabase magic link (or error redirect) lands on
/register/abc#access_token=... (or #error=access_denied&...).
- Next.js middleware detects the user is already logged in and redirects to
/dashboard.
- The middleware redirect does not include a fragment.
- The browser inherits the hash:
/dashboard#access_token=....
- On
/dashboard, createBrowserClient initializes, detects the hash, hits a flow-type mismatch, and deletes the session cookie.
The user is now silently logged out on a page that has nothing to do with authentication. From the user's perspective, the session randomly disappeared.
To Reproduce
- Set up a Next.js app with
@supabase/ssr v0.6.1 and @supabase/supabase-js v2.50.2.
- Create a browser client with
detectSessionInUrl: false:
const supabase = createBrowserClient(url, key, {
auth: { detectSessionInUrl: false },
});
- Establish a valid session (e.g. via magic link).
- Navigate to any page whose URL contains a Supabase auth hash fragment, e.g.:
http://localhost:3000/some-page#access_token=expired_or_stale_token&refresh_token=x&token_type=bearer&type=signup
This can happen naturally when middleware redirects from a magic-link landing page to another page — the browser inherits the fragment.
- Observe that the session cookie (
sb-<ref>-auth-token) is deleted.
Expected behavior
-
detectSessionInUrl: false should be respected. If the user explicitly passes detectSessionInUrl: false, the library should not override it. The current spread order makes the user's setting impossible to apply.
-
flowType should be configurable. If there's a strong reason to default to "pkce", it should still be overridable by the user. The current code makes it impossible.
-
_removeSession() should not be called on flow-type mismatches. Even when detectSessionInUrl is true, a flow-type mismatch between the URL params and the client config should not destroy an existing valid session. The mismatch indicates the URL hash is stale/irrelevant — not that the user's current session is invalid.
Suggested fix
Change the spread order in createBrowserClient so that user-provided options take precedence:
// BEFORE (user's options are overridden):
auth: {
...options?.auth,
flowType: "pkce",
detectSessionInUrl: isBrowser(),
// ...
},
// AFTER (library provides defaults, user can override):
auth: {
flowType: "pkce",
autoRefreshToken: isBrowser(),
detectSessionInUrl: isBrowser(),
persistSession: true,
...options?.auth, // user's options last = user wins
storage, // storage must still be forced
},
Additionally, consider not calling _removeSession() in @supabase/auth-js's _initialize() when the error is a flow-type mismatch (AuthPKCEGrantCodeExchangeError). A stale URL hash should not nuke a valid session.
Workaround
In Next.js middleware (or any server-side redirect), include a non-empty fragment in the redirect URL to prevent the browser from inheriting the original hash:
const redirectUrl = new URL("/dashboard", request.url);
redirectUrl.hash = "_"; // prevents RFC 7231 fragment inheritance
return NextResponse.redirect(redirectUrl);
Note: an empty fragment (#) gets stripped by the URL parser, so a non-empty placeholder like _ is required.
Environment
@supabase/ssr: 0.6.1
@supabase/supabase-js: 2.50.2
@supabase/auth-js: 2.70.0
- Next.js 15 (App Router)
- Browser: Chrome (also reproducible in other browsers that follow RFC 7231)
Repository
@supabase/ssrv0.6.1 (also affects@supabase/auth-jsv2.70.0 error handling path)Describe the bug
createBrowserClientsilently overrides the user-suppliedauth.detectSessionInUrlandauth.flowTypeoptions. The user's options are spread before the library's hardcoded values, so the library always wins:This means:
detectSessionInUrl: falseis ignored — it is alwaystruein the browser.flowTypeis always forced to"pkce", regardless of the user's setting.Why this is a problem
The combination of these two overrides creates a destructive code path when a page loads with Supabase auth-related hash fragments in the URL (e.g.
#access_token=...or#error=access_denied&error_code=otp_expired&...):_initialize()in@supabase/auth-jsparses the URL hash._isImplicitGrantCallback()returnstrue(becauseaccess_tokenorerror_descriptionis present in the hash).detectSessionInUrlwas forced totrue, the client enters the URL-processing code path._getSessionFromURL()is called.error,error_description,error_code): throwsAuthImplicitGrantRedirectError.access_token,refresh_token): the flow-type check detects a mismatch (implicit URL vs forcedpkceconfig) and throwsAuthPKCEGrantCodeExchangeError.Max-Age=0and deleted from the browser.How auth hash fragments end up on unrelated pages
This happens naturally through HTTP redirect fragment inheritance (RFC 7231 Section 7.1.2):
Example scenario with Next.js middleware:
/register/abc#access_token=...(or#error=access_denied&...)./dashboard./dashboard#access_token=..../dashboard,createBrowserClientinitializes, detects the hash, hits a flow-type mismatch, and deletes the session cookie.The user is now silently logged out on a page that has nothing to do with authentication. From the user's perspective, the session randomly disappeared.
To Reproduce
@supabase/ssrv0.6.1 and@supabase/supabase-jsv2.50.2.detectSessionInUrl: false:sb-<ref>-auth-token) is deleted.Expected behavior
detectSessionInUrl: falseshould be respected. If the user explicitly passesdetectSessionInUrl: false, the library should not override it. The current spread order makes the user's setting impossible to apply.flowTypeshould be configurable. If there's a strong reason to default to"pkce", it should still be overridable by the user. The current code makes it impossible._removeSession()should not be called on flow-type mismatches. Even whendetectSessionInUrlistrue, a flow-type mismatch between the URL params and the client config should not destroy an existing valid session. The mismatch indicates the URL hash is stale/irrelevant — not that the user's current session is invalid.Suggested fix
Change the spread order in
createBrowserClientso that user-provided options take precedence:Additionally, consider not calling
_removeSession()in@supabase/auth-js's_initialize()when the error is a flow-type mismatch (AuthPKCEGrantCodeExchangeError). A stale URL hash should not nuke a valid session.Workaround
In Next.js middleware (or any server-side redirect), include a non-empty fragment in the redirect URL to prevent the browser from inheriting the original hash:
Note: an empty fragment (
#) gets stripped by the URL parser, so a non-empty placeholder like_is required.Environment
@supabase/ssr: 0.6.1@supabase/supabase-js: 2.50.2@supabase/auth-js: 2.70.0