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
5 changes: 3 additions & 2 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ DASHBOARD_ORIGIN=http://dashboard.local.knowhereto.ai:3000
# local: http://notebook.local.knowhereto.ai:3001
NOTEBOOK_PUBLIC_URL=http://notebook.local.knowhereto.ai:3001

# Optional override of the session cookie names Dashboard sets. Defaults
# to Better Auth's standard cookie names.
# Optional additional session cookie names Dashboard sets.
# Without an override, Notebook recognizes Better Auth session-token cookies
# with standard or environment-prefixed names.
# SESSION_COOKIE_NAMES=better-auth.session_token,__Secure-better-auth.session_token

# --- Product analytics (client-side) ---
Expand Down
23 changes: 22 additions & 1 deletion src/infrastructure/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ vi.mock("next/cache", () => nextCacheMocks)
* Playwright flow added in a later PR.
*/

import { extractUser, sessionCookieNames } from "."
import { extractUser, isSessionCookieName, sessionCookieNames } from "."

const SESSION_PATH = "/api/orpc/users/getCurrentUser"

Expand Down Expand Up @@ -147,6 +147,27 @@ describe("sessionCookieNames", () => {
"__Secure-better-auth.session_token",
])
})

it("recognizes environment-prefixed Better Auth session tokens", () => {
delete process.env.SESSION_COOKIE_NAMES

expect(
isSessionCookieName("__Secure-better-auth-staging-session_token"),
).toBe(true)
expect(isSessionCookieName("__Secure-better-auth-session_token")).toBe(true)
expect(
isSessionCookieName("__Secure-better-auth-staging-session_data"),
).toBe(false)
})

it("adds an explicit cookie-name override without hiding Better Auth tokens", () => {
process.env.SESSION_COOKIE_NAMES = "custom-session"

expect(isSessionCookieName("custom-session")).toBe(true)
expect(
isSessionCookieName("__Secure-better-auth-staging-session_token"),
).toBe(true)
})
})

describe("getCurrentUser", () => {
Expand Down
12 changes: 6 additions & 6 deletions src/infrastructure/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import {
HttpClientRequest,
} from "@effect/platform"
import { authURLs } from "./urls"
import { sessionCookieNames } from "./session-cookie-names"
import {
isSessionCookieName,
sessionCookieNames,
} from "./session-cookie-names"
import { logger } from "@/lib/logger"
import { knowhereApiKeyOverride } from "@/integrations/knowhere-api-key"
import { setEmptyJsonBody } from "@/integrations/dashboard/orpc-request"
import { formatUnknownForLog } from "@/lib/format-log-value"

export { sessionCookieNames }
export { isSessionCookieName, sessionCookieNames }

/**
* Auth helpers for Knowhere Notebook.
Expand Down Expand Up @@ -222,10 +225,7 @@ export async function hasSessionCookie(): Promise<boolean> {
if (knowhereApiKeyOverride.hasApiKey()) return true

const jar = await cookies()
for (const name of sessionCookieNames()) {
if (jar.get(name) !== undefined) return true
}
return false
return jar.getAll().some((cookie) => isSessionCookieName(cookie.name))
}

/**
Expand Down
27 changes: 20 additions & 7 deletions src/infrastructure/auth/session-cookie-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@ const DEFAULT_SESSION_COOKIE_NAMES = [
"__Secure-better-auth.session_token",
] as const

const BETTER_AUTH_SESSION_COOKIE_NAME =
/^(?:__Secure-)?better-auth(?:[-.][A-Za-z0-9_]+)*[-.]session_token$/u

export function sessionCookieNames(): readonly string[] {
return configuredSessionCookieNames() ?? DEFAULT_SESSION_COOKIE_NAMES
}

export function isSessionCookieName(name: string): boolean {
return (
sessionCookieNames().includes(name) ||
BETTER_AUTH_SESSION_COOKIE_NAME.test(name)
)
}

function configuredSessionCookieNames(): readonly string[] | null {
const override = process.env.SESSION_COOKIE_NAMES
if (override !== undefined && override.trim().length > 0) {
return override
.split(",")
.map((s) => s.trim())
.filter(Boolean)
}
return DEFAULT_SESSION_COOKIE_NAMES
if (override === undefined || override.trim().length === 0) return null

return override
.split(",")
.map((name) => name.trim())
.filter(Boolean)
}
33 changes: 33 additions & 0 deletions src/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { proxy } from "./proxy";
describe("proxy", () => {
const originalDashboardOrigin = process.env.DASHBOARD_ORIGIN;
const originalKnowhereApiKey = process.env.KNOWHERE_API_KEY;
const originalSessionCookieNames = process.env.SESSION_COOKIE_NAMES;

beforeEach(() => {
delete process.env.DASHBOARD_ORIGIN;
delete process.env.KNOWHERE_API_KEY;
delete process.env.SESSION_COOKIE_NAMES;
});

afterEach(() => {
Expand All @@ -23,6 +25,11 @@ describe("proxy", () => {
} else {
process.env.KNOWHERE_API_KEY = originalKnowhereApiKey;
}
if (originalSessionCookieNames === undefined) {
delete process.env.SESSION_COOKIE_NAMES;
} else {
process.env.SESSION_COOKIE_NAMES = originalSessionCookieNames;
}
});

it("allows anonymous guest source reads", () => {
Expand Down Expand Up @@ -84,4 +91,30 @@ describe("proxy", () => {

expect(response.headers.get("x-middleware-next")).toBe("1");
});

it("recognizes Dashboard session tokens with an environment prefix", () => {
const response = proxy(
new NextRequest("http://localhost:3001/inspect/doc-1/chunks", {
headers: {
cookie: "__Secure-better-auth-staging-session_token=token",
},
}),
);

expect(response.headers.get("x-middleware-next")).toBe("1");
});

it("does not mistake Better Auth session data for a session token", () => {
const response = proxy(
new NextRequest("http://localhost:3001/inspect/doc-1/chunks", {
headers: {
cookie: "__Secure-better-auth-staging-session_data=data",
},
}),
);

expect(response.headers.get("location")).toBe(
"http://localhost:3001/login",
);
});
});
6 changes: 3 additions & 3 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse, type NextRequest } from "next/server"
import { authURLs } from "@/infrastructure/auth/urls"
import { sessionCookieNames } from "@/infrastructure/auth/session-cookie-names"
import { isSessionCookieName } from "@/infrastructure/auth/session-cookie-names"
import { knowhereApiKeyOverride } from "@/integrations/knowhere-api-key"

/**
Expand Down Expand Up @@ -59,8 +59,8 @@ export function proxy(req: NextRequest): NextResponse {

if (isPublicPath(req)) return NextResponse.next()

for (const name of sessionCookieNames()) {
if (req.cookies.get(name)) return NextResponse.next()
if (req.cookies.getAll().some((cookie) => isSessionCookieName(cookie.name))) {
return NextResponse.next()
}

const origin = process.env.DASHBOARD_ORIGIN
Expand Down
Loading