Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ Please refer to the [official server-side rendering guides](https://supabase.com
For guidance on choosing between `getSession()`, `getUser()`, and `getClaims()`,
see the [official server-side rendering guides](https://supabase.com/docs/guides/auth/server-side).

### The `auth.storage` option is ignored

`createBrowserClient` and `createServerClient` always store the session in
cookies — this is the entire point of the package, since it lets a
server-rendered request read the same session the browser wrote. Passing
`auth.storage` has no effect; a one-time console warning is logged if you do. (`auth.userStorage` is different and is still respected when `cookies.encode` is set to `"tokens-only"`.) If you
don't need server-side access to the session, use `@supabase/supabase-js`'s
`createClient` directly with your own `storage` (e.g. `localStorage`) —
there's no reason to use `@supabase/ssr` in that case.

### Concurrent requests with the same expired session

Supabase refresh tokens are single-use. If two requests arrive simultaneously
Expand Down
64 changes: 64 additions & 0 deletions src/createBrowserClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { MAX_CHUNK_SIZE, stringToBase64URL } from "./utils";
import { CookieOptions } from "./types";
import { createBrowserClient } from "./createBrowserClient";
import { resetWarnOnceForTesting } from "./warnOnce";

// Spy on createClient to capture auth options passed through
const createClientSpy = vi.fn().mockReturnValue({
Expand Down Expand Up @@ -157,4 +158,67 @@ describe("createBrowserClient", () => {
).not.toThrow();
});
});

describe("storage option", () => {
let warnings: any[][];
let warnSpy: any;

beforeEach(() => {
resetWarnOnceForTesting();
warnings = [];
warnSpy = vi
.spyOn(console, "warn")
.mockImplementation((...args: any[]) => {
warnings.push(args);
});
});

afterEach(() => {
warnSpy.mockRestore();
});

it("warns when `auth.storage` is passed, since it is always ignored", () => {
createBrowserClient("http://localhost", "anon-key", {
isSingleton: false,
auth: { storage: {} as any },
});

expect(warnings.some((args) => /auth\.storage/.test(args[0]))).toBe(true);
});

it("warns only once across multiple calls", () => {
createBrowserClient("http://localhost", "anon-key", {
isSingleton: false,
auth: { storage: {} as any },
});
createBrowserClient("http://localhost", "anon-key", {
isSingleton: false,
auth: { storage: {} as any },
});

expect(
warnings.filter((args) => /auth\.storage/.test(args[0])).length,
).toBe(1);
});

it("does not warn when `auth.storage` is not passed", () => {
createBrowserClient("http://localhost", "anon-key", {
isSingleton: false,
});

expect(warnings.some((args) => /auth\.storage/.test(args[0]))).toBe(
false,
);
});

it("still uses the cookie-backed storage even when `auth.storage` is passed", () => {
createBrowserClient("http://localhost", "anon-key", {
isSingleton: false,
auth: { storage: { fake: true } as any },
});

const passedOptions = createClientSpy.mock.calls[0][2];
expect(passedOptions.auth.storage).not.toEqual({ fake: true });
});
});
});
15 changes: 15 additions & 0 deletions src/createBrowserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from "./types";
import { isBrowser } from "./utils";
import { VERSION } from "./version";
import { warnOnce } from "./warnOnce";
import { warnIfUsingDeprecatedAuthHelpersPackage } from "./warnDeprecatedPackage";

let cachedBrowserClient: SupabaseClient<any, any, any> | undefined;
Expand All @@ -29,6 +30,14 @@ let cachedBrowserClient: SupabaseClient<any, any, any> | undefined;
* in difficult to debug authentication issues such as random logouts, early
* session termination or problems with inconsistent state.
*
* **The `auth.storage` option is ignored.** The session is always persisted via
* cookies so that a server-rendered request can read it. Passing
* `options.auth.storage` has no effect — a one-time console warning is logged
* if you do. (`options.auth.userStorage` is still respected when `cookies.encode` is `"tokens-only"`.)
* If you don't need the session to be readable server-side, use
* `@supabase/supabase-js`'s `createClient` directly with your own `storage`
* instead; `@supabase/ssr` isn't needed in that case.
*
* @param supabaseUrl The URL of the Supabase project.
* @param supabaseKey The `anon` API key of the Supabase project.
* @param options Various configuration options.
Expand Down Expand Up @@ -107,6 +116,12 @@ export function createBrowserClient<
);
}

if (options?.auth?.storage) {
warnOnce(
"@supabase/ssr: createBrowserClient always manages the session via cookies, so the `auth.storage` option you passed is ignored. If you don't need the session to be readable on the server, use @supabase/supabase-js's createClient directly with your own `storage` instead.",
);
}

const { storage } = createStorageFromOptions(
{
...options,
Expand Down
153 changes: 152 additions & 1 deletion src/createServerClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

import { MAX_CHUNK_SIZE, stringToBase64URL } from "./utils";
import { CookieOptions } from "./types";
import { createServerClient } from "./createServerClient";
import { resetWarnOnceForTesting } from "./warnOnce";

describe("createServerClient", () => {
describe("validation", () => {
Expand Down Expand Up @@ -626,4 +627,154 @@ describe("createServerClient", () => {
expect(fetchCallCount).toBe(0);
});
});

describe("storage option", () => {
let warnings: any[][];
let warnSpy: any;

beforeEach(() => {
resetWarnOnceForTesting();
warnings = [];
warnSpy = vi
.spyOn(console, "warn")
.mockImplementation((...args: any[]) => {
warnings.push(args);
});
});

afterEach(() => {
warnSpy.mockRestore();
});

it("warns when `auth.storage` is passed, since it is always ignored", () => {
createServerClient("https://project-ref.supabase.co", "anon-key", {
cookies: {
getAll() {
return [];
},
setAll() {
// no-op
},
},
auth: { storage: {} as any },
});

expect(warnings.some((args) => /auth\.storage/.test(args[0]))).toBe(true);
});

it("warns only once across multiple calls", () => {
const options = {
cookies: {
getAll() {
return [];
},
setAll() {
// no-op
},
},
auth: { storage: {} as any },
};

createServerClient(
"https://project-ref.supabase.co",
"anon-key",
options,
);
createServerClient(
"https://project-ref.supabase.co",
"anon-key",
options,
);

expect(
warnings.filter((args) => /auth\.storage/.test(args[0])).length,
).toBe(1);
});

it("does not warn when `auth.storage` is not passed", () => {
createServerClient("https://project-ref.supabase.co", "anon-key", {
cookies: {
getAll() {
return [];
},
setAll() {
// no-op
},
},
});

expect(warnings.some((args) => /auth\.storage/.test(args[0]))).toBe(
false,
);
});

it("still uses the cookie-backed storage even when `auth.storage` is passed", async () => {
const customStorageCalls: string[] = [];

const supabase = createServerClient(
"https://project-ref.supabase.co",
"anon-key",
{
cookies: {
getAll() {
return [
{
name: "sb-project-ref-auth-token",
value:
"base64-" +
stringToBase64URL(
JSON.stringify({
token_type: "bearer",
access_token: "<valid-access-token>",
refresh_token: "<valid-refresh-token>",
expires_at: Math.floor(Date.now() / 1000) + 5 * 60, // expires in 5 mins
expires_in: 5 * 60,
user: {
id: "<valid-user-id>",
},
}),
),
},
];
},

setAll() {
// no-op
},
},

auth: {
storage: {
getItem: async (key: string) => {
customStorageCalls.push(key);
return null;
},
setItem: async (key: string) => {
customStorageCalls.push(key);
},
removeItem: async (key: string) => {
customStorageCalls.push(key);
},
},
},

global: {
fetch: async () => {
throw new Error("Should not be called");
},
},
},
);

const {
data: { session },
error,
} = await supabase.auth.getSession();

expect(error).toBeNull();
expect(session).not.toBeNull();
expect(session!.user.id).toEqual("<valid-user-id>");
expect(customStorageCalls).toEqual([]);
});
});
});
14 changes: 14 additions & 0 deletions src/createServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
} from "./types";
import { memoryLocalStorageAdapter } from "./utils/helpers";
import { VERSION } from "./version";
import { warnOnce } from "./warnOnce";
import { warnIfUsingDeprecatedAuthHelpersPackage } from "./warnDeprecatedPackage";

/**
Expand Down Expand Up @@ -84,6 +85,13 @@ export function createServerClient<
* no explicit JWT is passed). Token refreshes write the updated session back
* to cookies via the `setAll` handler.
*
* **The `auth.storage` option is ignored.** The session is always persisted via
* cookies. Passing `options.auth.storage` has no effect — a one-time console
* warning is logged if you do. (`options.auth.userStorage` is still respected when `cookies.encode` is `"tokens-only"`.)
* If you want to source the session from somewhere other than the request cookies,
* use `@supabase/supabase-js`'s `createClient` directly with your own `storage`
* instead; `@supabase/ssr` isn't needed in that case.
*
* @param supabaseUrl The URL of the Supabase project.
* @param supabaseKey The `anon` API key of the Supabase project.
* @param options Various configuration options.
Expand Down Expand Up @@ -129,6 +137,12 @@ export function createServerClient<
);
}

if (options?.auth?.storage) {
warnOnce(
"@supabase/ssr: createServerClient always manages the session via cookies, so the `auth.storage` option you passed is ignored. If you want to source the session from somewhere other than the request cookies, use @supabase/supabase-js's createClient directly with your own `storage` instead.",
);
}

const { storage, getAll, setAll, setItems, removedItems } =
createStorageFromOptions(
{
Expand Down
22 changes: 22 additions & 0 deletions src/warnOnce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const warnedMessages = new Set<string>();

/**
* Logs a warning to the console only once per process for each distinct
* message. Used for configuration warnings that would otherwise fire on
* every client creation (e.g. once per server request).
*/
export function warnOnce(message: string): void {
if (warnedMessages.has(message)) {
return;
}

warnedMessages.add(message);
console.warn(message);
}

/**
* Clears the set of already-logged messages. Only for use in tests.
*/
export function resetWarnOnceForTesting(): void {
warnedMessages.clear();
}