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
492 changes: 480 additions & 12 deletions src/__snapshots__/createServerClient.spec.ts.snap

Large diffs are not rendered by default.

84 changes: 83 additions & 1 deletion src/cookies.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { describe, expect, it, beforeEach, afterEach } from "vitest";
import { isBrowser, DEFAULT_COOKIE_OPTIONS, MAX_CHUNK_SIZE } from "./utils";
import { CookieOptions } from "./types";

import { createStorageFromOptions, applyServerStorage } from "./cookies";
import {
createStorageFromOptions,
applyServerStorage,
decodeCookie,
} from "./cookies";

describe("createStorageFromOptions in browser without cookie methods", () => {
beforeEach(() => {
Expand Down Expand Up @@ -1070,3 +1074,81 @@ describe("applyServerStorage", () => {
]);
});
});

describe("decodeCookie", () => {
let warnings: any[][] = [];
let originalWarn: any;

beforeEach(() => {
warnings = [];

originalWarn = console.warn;
console.warn = (...args: any[]) => {
warnings.push(structuredClone(args));
};
});

afterEach(() => {
console.warn = originalWarn;
});

it("should decode base64url+length encoded value", () => {
const value = JSON.stringify({ a: "b" });
const valueB64 = Buffer.from(value).toString("base64url");

expect(
decodeCookie(`base64l-${valueB64.length.toString(36)}-${valueB64}`),
).toEqual(value);
expect(
decodeCookie(
`base64l-${valueB64.length.toString(36)}-${valueB64}padding_that_is_ignored`,
),
).toEqual(value);
expect(
decodeCookie(
`base64l-${valueB64.length.toString(36)}-${valueB64.substring(0, valueB64.length - 1)}`,
),
).toBeNull();
expect(decodeCookie(`base64l-0-${valueB64}`)).toBeNull();
expect(decodeCookie(`base64l-${valueB64}`)).toBeNull();
expect(warnings).toMatchInlineSnapshot(`
[
[
"@supabase/ssr: Detected stale cookie data. Please check your integration with Supabase for bugs. This can cause your users to loose the session.",
],
[
"@supabase/ssr: Detected stale cookie data. Please check your integration with Supabase for bugs. This can cause your users to loose the session.",
],
]
`);
});

it("should decode base64url encoded value", () => {
const value = JSON.stringify({ a: "b" });
const valueB64 = Buffer.from(value).toString("base64url");

expect(decodeCookie(`base64-${valueB64}`)).toEqual(value);
expect(warnings).toMatchInlineSnapshot(`[]`);
});

it("should not decode base64url encoded value with invalid UTF-8", () => {
const valueB64 = Buffer.from([0xff, 0xff, 0xff, 0xff]).toString(
"base64url",
);

expect(decodeCookie(`base64-${valueB64}`)).toBeNull();
expect(
decodeCookie(`base64l-${valueB64.length.toString(36)}-${valueB64}`),
).toBeNull();
expect(warnings).toMatchInlineSnapshot(`
[
[
"@supabase/ssr: Detected stale cookie data that does not decode to a UTF-8 string. Please check your integration with Supabase for bugs. This can cause your users to loose session access.",
],
[
"@supabase/ssr: Detected stale cookie data that does not decode to a UTF-8 string. Please check your integration with Supabase for bugs. This can cause your users to loose session access.",
],
]
`);
});
});
90 changes: 67 additions & 23 deletions src/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,55 @@ import type {
} from "./types";

const BASE64_PREFIX = "base64-";
const BASE64_LENGTH_PREFIX = "base64l-";
const BASE64_LENGTH_PATTERN = /^base64l-([0-9a-z]+)-(.+)$/;

export function decodeBase64Cookie(value: string) {
try {
return stringFromBase64URL(value);
} catch (e: any) {
// if an invalid UTF-8 sequence is encountered, it means that reconstructing the chunkedCookie failed and the cookies don't contain useful information
console.warn(
"@supabase/ssr: Detected stale cookie data that does not decode to a UTF-8 string. Please check your integration with Supabase for bugs. This can cause your users to loose session access.",
);
return null;
}
}

export function decodeCookie(chunkedCookie: string) {
let decoded = chunkedCookie;

if (chunkedCookie.startsWith(BASE64_PREFIX)) {
return decodeBase64Cookie(decoded.substring(BASE64_PREFIX.length));
} else if (chunkedCookie.startsWith(BASE64_LENGTH_PREFIX)) {
const match = chunkedCookie.match(BASE64_LENGTH_PATTERN);

if (!match) {
return null;
}

const expectedLength = parseInt(match[1], 36);

if (expectedLength === 0) {
return null;
}

if (match[2].length !== expectedLength) {
console.warn(
"@supabase/ssr: Detected stale cookie data. Please check your integration with Supabase for bugs. This can cause your users to loose the session.",
);
}

if (expectedLength <= match[2].length) {
return decodeBase64Cookie(match[2].substring(0, expectedLength));
} else {
// data is missing, cannot decode cookie
return null;
}
}

return decoded;
}

/**
* Creates a storage client that handles cookies correctly for browser and
Expand All @@ -33,7 +82,7 @@ const BASE64_PREFIX = "base64-";
*/
export function createStorageFromOptions(
options: {
cookieEncoding: "raw" | "base64url";
cookieEncoding: "raw" | "base64url" | "base64url+length";
cookies?:
| CookieMethodsBrowser
| CookieMethodsBrowserDeprecated
Expand Down Expand Up @@ -203,15 +252,7 @@ export function createStorageFromOptions(
return null;
}

let decoded = chunkedCookie;

if (chunkedCookie.startsWith(BASE64_PREFIX)) {
decoded = stringFromBase64URL(
chunkedCookie.substring(BASE64_PREFIX.length),
);
}

return decoded;
return decodeCookie(chunkedCookie);
},
setItem: async (key: string, value: string) => {
const allCookies = await getAll([key]);
Expand All @@ -225,6 +266,13 @@ export function createStorageFromOptions(

if (cookieEncoding === "base64url") {
encoded = BASE64_PREFIX + stringToBase64URL(value);
} else if (cookieEncoding === "base64url+length") {
encoded = [
BASE64_LENGTH_PREFIX,
value.length.toString(36),
"-",
value,
].join("");
}

const setCookies = createChunks(key, encoded);
Expand Down Expand Up @@ -342,18 +390,7 @@ export function createStorageFromOptions(
return null;
}

let decoded = chunkedCookie;

if (
typeof chunkedCookie === "string" &&
chunkedCookie.startsWith(BASE64_PREFIX)
) {
decoded = stringFromBase64URL(
chunkedCookie.substring(BASE64_PREFIX.length),
);
}

return decoded;
return decodeCookie(chunkedCookie);
},
setItem: async (key: string, value: string) => {
// We don't have an `onAuthStateChange` event that can let us know that
Expand Down Expand Up @@ -411,7 +448,7 @@ export async function applyServerStorage(
removedItems: { [name: string]: boolean };
},
options: {
cookieEncoding: "raw" | "base64url";
cookieEncoding: "raw" | "base64url" | "base64url+length";
cookieOptions?: CookieOptions | null;
},
) {
Expand Down Expand Up @@ -439,6 +476,13 @@ export async function applyServerStorage(

if (cookieEncoding === "base64url") {
encoded = BASE64_PREFIX + stringToBase64URL(encoded);
} else if (cookieEncoding === "base64url+length") {
encoded = [
BASE64_LENGTH_PREFIX,
encoded.length.toString(36),
"-",
stringToBase64URL(encoded),
].join("");
}

const chunks = createChunks(itemName, encoded);
Expand Down
2 changes: 1 addition & 1 deletion src/createBrowserClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MAX_CHUNK_SIZE, stringToBase64URL } from "./utils";
import { CookieOptions } from "./types";
import { createBrowserClient } from "./createBrowserClient";

describe("createServerClient", () => {
describe("createBrowserClient", () => {
describe("validation", () => {
it("should throw an error on empty URL and anon key", async () => {
expect(() => {
Expand Down
8 changes: 4 additions & 4 deletions src/createBrowserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function createBrowserClient<
options?: SupabaseClientOptions<SchemaName> & {
cookies?: CookieMethodsBrowser;
cookieOptions?: CookieOptionsWithName;
cookieEncoding?: "raw" | "base64url";
cookieEncoding?: "raw" | "base64url" | "base64url+length";
isSingleton?: boolean;
},
): SupabaseClient<Database, SchemaName, Schema>;
Expand All @@ -72,7 +72,7 @@ export function createBrowserClient<
options?: SupabaseClientOptions<SchemaName> & {
cookies: CookieMethodsBrowserDeprecated;
cookieOptions?: CookieOptionsWithName;
cookieEncoding?: "raw" | "base64url";
cookieEncoding?: "raw" | "base64url" | "base64url+length";
isSingleton?: boolean;
},
): SupabaseClient<Database, SchemaName, Schema>;
Expand All @@ -91,7 +91,7 @@ export function createBrowserClient<
options?: SupabaseClientOptions<SchemaName> & {
cookies?: CookieMethodsBrowser | CookieMethodsBrowserDeprecated;
cookieOptions?: CookieOptionsWithName;
cookieEncoding?: "raw" | "base64url";
cookieEncoding?: "raw" | "base64url" | "base64url+length";
isSingleton?: boolean;
},
): SupabaseClient<Database, SchemaName, Schema> {
Expand All @@ -113,7 +113,7 @@ export function createBrowserClient<
const { storage } = createStorageFromOptions(
{
...options,
cookieEncoding: options?.cookieEncoding ?? "base64url",
cookieEncoding: options?.cookieEncoding ?? "base64url+length",
},
false,
);
Expand Down
Loading