@auth0/nextjs-auth0
Version:
Auth0 Next.js SDK
339 lines (338 loc) • 15.9 kB
JavaScript
import { RequestCookies, ResponseCookies } from "@edge-runtime/cookies";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Auth0Client } from "./client.js";
import { deleteChunkedCookie, deleteCookie } from "./cookies.js";
import { StatelessSessionStore } from "./session/stateless-session-store.js";
import { TransactionStore } from "./transaction-store.js";
describe("Base path and cookie configuration tests", () => {
let originalEnv;
beforeEach(() => {
originalEnv = { ...process.env };
vi.clearAllMocks();
vi.resetModules();
// Clear any existing environment variables
delete process.env.NEXT_PUBLIC_BASE_PATH;
delete process.env.AUTH0_COOKIE_PATH;
});
afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
delete process.env.NEXT_PUBLIC_BASE_PATH;
delete process.env.AUTH0_COOKIE_PATH;
});
describe("Logout integration with base paths", () => {
it("should reproduce the bug scenario: cookies set with base path should be deleted with same path", async () => {
// Set up environment with base path
process.env.NEXT_PUBLIC_BASE_PATH = "/dashboard";
// Create Auth0Client which should auto-detect the base path
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com/dashboard",
secret: "test-secret-that-is-long-enough-for-jwt"
});
// Get the session store
const sessionStore = client
.sessionStore;
// Verify that the session store has the correct path configuration
expect(sessionStore.cookieConfig.path).toBe("/dashboard");
// Simulate cookie deletion during logout
const mockResCookies = new ResponseCookies(new Headers());
const mockReqCookies = new RequestCookies(new Headers());
// Mock the get method to simulate an existing session cookie
mockReqCookies.get = () => ({ value: "mock-session-value" });
mockReqCookies.getAll = () => [];
// Call delete method (this would be called during logout)
await sessionStore.delete(mockReqCookies, mockResCookies);
// Verify that the cookie deletion header includes the correct path
const cookieHeader = mockResCookies.toString();
// The cookie should be deleted with the same path it was set with
expect(cookieHeader).toContain("Path=/dashboard");
expect(cookieHeader).toContain("Max-Age=0");
});
it("should work correctly without base path (backward compatibility)", async () => {
// No base path set
delete process.env.NEXT_PUBLIC_BASE_PATH;
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com",
secret: "test-secret-that-is-long-enough-for-jwt"
});
const sessionStore = client
.sessionStore;
// Should default to root path
expect(sessionStore.cookieConfig.path).toBe("/");
// Test deletion
const mockResCookies = new ResponseCookies(new Headers());
const mockReqCookies = new RequestCookies(new Headers());
mockReqCookies.get = () => ({ value: "mock-session-value" });
mockReqCookies.getAll = () => [];
await sessionStore.delete(mockReqCookies, mockResCookies);
const cookieHeader = mockResCookies.toString();
// Should use root path or no path specified
expect(cookieHeader).toContain("Max-Age=0");
});
it("should prioritize explicit cookie path over base path", async () => {
process.env.NEXT_PUBLIC_BASE_PATH = "/dashboard";
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com",
secret: "test-secret-that-is-long-enough-for-jwt",
session: {
cookie: {
path: "/custom-path"
}
}
});
const sessionStore = client
.sessionStore;
// Should use the explicit path, not the base path
expect(sessionStore.cookieConfig.path).toBe("/custom-path");
});
});
describe("Cookie deletion functions", () => {
let mockResCookies;
beforeEach(() => {
// Create a mock ResponseCookies object
const mockHeaders = new Headers();
mockResCookies = new ResponseCookies(mockHeaders);
});
it("should delete cookie with default path when no path is specified", () => {
deleteCookie(mockResCookies, "test-cookie");
const cookieHeader = mockResCookies.toString();
expect(cookieHeader).toContain("test-cookie=");
expect(cookieHeader).toContain("Max-Age=0");
expect(cookieHeader).toContain("Path=/");
});
it("should delete cookie with specified path", () => {
deleteCookie(mockResCookies, "test-cookie", { path: "/dashboard" });
const cookieHeader = mockResCookies.toString();
expect(cookieHeader).toContain("test-cookie=");
expect(cookieHeader).toContain("Max-Age=0");
expect(cookieHeader).toContain("Path=/dashboard");
});
it("should delete cookie with root path explicitly", () => {
deleteCookie(mockResCookies, "test-cookie", { path: "/" });
const cookieHeader = mockResCookies.toString();
expect(cookieHeader).toContain("test-cookie=");
expect(cookieHeader).toContain("Max-Age=0");
expect(cookieHeader).toContain("Path=/");
});
it("should handle chunked cookie deletion with path", () => {
const mockReqCookies = {
getAll: () => [
{ name: "test-cookie__0", value: "chunk1" },
{ name: "test-cookie__1", value: "chunk2" }
]
};
deleteChunkedCookie("test-cookie", mockReqCookies, mockResCookies, false, { path: "/dashboard" });
const cookieHeader = mockResCookies.toString();
expect(cookieHeader).toContain("test-cookie=");
expect(cookieHeader).toContain("Path=/dashboard");
expect(cookieHeader).toContain("test-cookie__0=");
expect(cookieHeader).toContain("test-cookie__1=");
});
});
describe("Auth0Client constructor base path auto-detection", () => {
it("should use NEXT_PUBLIC_BASE_PATH for cookie paths when configured", () => {
process.env.NEXT_PUBLIC_BASE_PATH = "/dashboard";
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com"
});
// Access private properties through any casting for testing
const sessionStore = client
.sessionStore;
const transactionStore = client
.transactionStore;
expect(sessionStore.cookieConfig.path).toBe("/dashboard");
expect(transactionStore.cookieOptions.path).toBe("/dashboard");
});
it("should use explicit AUTH0_COOKIE_PATH over base path", () => {
process.env.NEXT_PUBLIC_BASE_PATH = "/dashboard";
process.env.AUTH0_COOKIE_PATH = "/custom";
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com"
});
const sessionStore = client
.sessionStore;
expect(sessionStore.cookieConfig.path).toBe("/custom");
});
it("should use client options over environment variables", () => {
process.env.NEXT_PUBLIC_BASE_PATH = "/dashboard";
process.env.AUTH0_COOKIE_PATH = "/custom";
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com",
session: {
cookie: {
path: "/explicit"
}
},
transactionCookie: {
path: "/txn-explicit"
}
});
const sessionStore = client
.sessionStore;
const transactionStore = client
.transactionStore;
expect(sessionStore.cookieConfig.path).toBe("/explicit");
expect(transactionStore.cookieOptions.path).toBe("/txn-explicit");
});
it("should default to root path when no base path is configured", () => {
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com"
});
const sessionStore = client
.sessionStore;
const transactionStore = client
.transactionStore;
expect(sessionStore.cookieConfig.path).toBe("/");
expect(transactionStore.cookieOptions.path).toBe("/");
});
});
describe("Session store cookie deletion with paths", () => {
it("should delete session cookies with configured path during logout", async () => {
const mockReqCookies = {
get: vi.fn().mockReturnValue({ value: "encrypted-session" }),
getAll: vi.fn().mockReturnValue([])
};
const mockResCookies = new ResponseCookies(new Headers());
const sessionStore = new StatelessSessionStore({
secret: "test-secret",
cookieOptions: {
name: "__session",
path: "/dashboard"
}
});
await sessionStore.delete(mockReqCookies, mockResCookies);
const cookieHeader = mockResCookies.toString();
expect(cookieHeader).toContain("__session=");
expect(cookieHeader).toContain("Max-Age=0");
expect(cookieHeader).toContain("Path=/dashboard");
});
});
describe("Transaction store cookie deletion with paths", () => {
it("should delete transaction cookies with configured path", async () => {
const mockResCookies = new ResponseCookies(new Headers());
const transactionStore = new TransactionStore({
secret: "test-secret",
cookieOptions: {
path: "/dashboard"
}
});
await transactionStore.delete(mockResCookies, "test-state");
const cookieHeader = mockResCookies.toString();
expect(cookieHeader).toContain("__txn_test-state=");
expect(cookieHeader).toContain("Max-Age=0");
expect(cookieHeader).toContain("Path=/dashboard");
});
it("should delete all transaction cookies with configured path", async () => {
const mockReqCookies = {
getAll: () => [
{ name: "__txn_state1", value: "value1" },
{ name: "__txn_state2", value: "value2" },
{ name: "other-cookie", value: "other" }
]
};
const mockResCookies = new ResponseCookies(new Headers());
const transactionStore = new TransactionStore({
secret: "test-secret",
cookieOptions: {
path: "/dashboard"
}
});
await transactionStore.deleteAll(mockReqCookies, mockResCookies);
const cookieHeader = mockResCookies.toString();
expect(cookieHeader).toContain("__txn_state1=");
expect(cookieHeader).toContain("__txn_state2=");
expect(cookieHeader).toContain("Max-Age=0");
expect(cookieHeader).toContain("Path=/dashboard");
expect(cookieHeader).not.toContain("other-cookie=; Max-Age=0");
});
});
describe("Edge Cases", () => {
it("should handle nested base paths correctly", () => {
process.env.NEXT_PUBLIC_BASE_PATH = "/app/dashboard";
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com"
});
const sessionStore = client
.sessionStore;
expect(sessionStore.cookieConfig.path).toBe("/app/dashboard");
});
it("should handle base path with trailing slash", () => {
process.env.NEXT_PUBLIC_BASE_PATH = "/dashboard/";
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com"
});
const sessionStore = client
.sessionStore;
expect(sessionStore.cookieConfig.path).toBe("/dashboard/");
});
it("should handle empty path correctly", () => {
deleteCookie(new ResponseCookies(new Headers()), "test-cookie", {
path: ""
});
// Should not throw and should create valid cookie deletion
expect(true).toBe(true);
});
});
describe("Backward Compatibility", () => {
it("should maintain existing behavior when no base path is configured", () => {
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com"
});
const sessionStore = client
.sessionStore;
const transactionStore = client
.transactionStore;
// Should default to root path as before
expect(sessionStore.cookieConfig.path).toBe("/");
expect(transactionStore.cookieOptions.path).toBe("/");
});
it("should not break existing explicit cookie configurations", () => {
const client = new Auth0Client({
domain: "test.auth0.com",
clientId: "test-client-id",
clientSecret: "test-client-secret",
appBaseUrl: "https://app.example.com",
session: {
cookie: {
path: "/legacy-path",
name: "legacy-session"
}
}
});
const sessionStore = client
.sessionStore;
expect(sessionStore.cookieConfig.path).toBe("/legacy-path");
expect(sessionStore.sessionCookieName).toBe("legacy-session");
});
});
});