mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 23:43:16 +01:00
Scope auth cookies by instance
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2cd85c7bed
commit
2d56abcbe7
3 files changed
+65
-1
No files matched your search
@@ -222,6 +222,8 @@ That repo-local env also sets:
|
||||
- `PAPERCLIP_WORKTREE_COLOR=<hex-color>`
|
||||
|
||||
The server/UI use those values for worktree-specific branding such as the top banner and dynamically colored favicon.
|
||||
Authenticated worktree servers also use the `PAPERCLIP_INSTANCE_ID` value to scope Better Auth cookie names.
|
||||
Browser cookies are shared by host rather than port, so this prevents logging into one `127.0.0.1:<port>` worktree from replacing another worktree server's session cookie.
|
||||
|
||||
Print shell exports explicitly when needed:
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { BetterAuthOptions } from "better-auth";
|
||||
import { getCookies } from "better-auth/cookies";
|
||||
import {
|
||||
buildBetterAuthAdvancedOptions,
|
||||
deriveAuthCookiePrefix,
|
||||
} from "../auth/better-auth.js";
|
||||
|
||||
const ORIGINAL_INSTANCE_ID = process.env.PAPERCLIP_INSTANCE_ID;
|
||||
|
||||
afterEach(() => {
|
||||
if (ORIGINAL_INSTANCE_ID === undefined) delete process.env.PAPERCLIP_INSTANCE_ID;
|
||||
else process.env.PAPERCLIP_INSTANCE_ID = ORIGINAL_INSTANCE_ID;
|
||||
});
|
||||
|
||||
describe("Better Auth cookie scoping", () => {
|
||||
it("derives an instance-scoped cookie prefix", () => {
|
||||
expect(deriveAuthCookiePrefix("default")).toBe("paperclip-default");
|
||||
expect(deriveAuthCookiePrefix("PAP-1601-worktree")).toBe("paperclip-PAP-1601-worktree");
|
||||
});
|
||||
|
||||
it("uses PAPERCLIP_INSTANCE_ID for the Better Auth cookie prefix", () => {
|
||||
process.env.PAPERCLIP_INSTANCE_ID = "sat-worktree";
|
||||
|
||||
const advanced = buildBetterAuthAdvancedOptions({ disableSecureCookies: false });
|
||||
|
||||
expect(advanced).toEqual({
|
||||
cookiePrefix: "paperclip-sat-worktree",
|
||||
});
|
||||
expect(getCookies({ advanced } as BetterAuthOptions).sessionToken.name).toBe(
|
||||
"paperclip-sat-worktree.session_token",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps local http auth cookies non-secure while preserving the scoped prefix", () => {
|
||||
process.env.PAPERCLIP_INSTANCE_ID = "pap-worktree";
|
||||
|
||||
expect(buildBetterAuthAdvancedOptions({ disableSecureCookies: true })).toEqual({
|
||||
cookiePrefix: "paperclip-pap-worktree",
|
||||
useSecureCookies: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
authVerifications,
|
||||
} from "@paperclipai/db";
|
||||
import type { Config } from "../config.js";
|
||||
import { resolvePaperclipInstanceId } from "../home-paths.js";
|
||||
|
||||
export type BetterAuthSessionUser = {
|
||||
id: string;
|
||||
@@ -25,6 +26,24 @@ export type BetterAuthSessionResult = {
|
||||
|
||||
type BetterAuthInstance = ReturnType<typeof betterAuth>;
|
||||
|
||||
const AUTH_COOKIE_PREFIX_FALLBACK = "default";
|
||||
const AUTH_COOKIE_PREFIX_INVALID_SEGMENTS_RE = /[^a-zA-Z0-9_-]+/g;
|
||||
|
||||
export function deriveAuthCookiePrefix(instanceId = resolvePaperclipInstanceId()): string {
|
||||
const scopedInstanceId = instanceId
|
||||
.trim()
|
||||
.replace(AUTH_COOKIE_PREFIX_INVALID_SEGMENTS_RE, "-")
|
||||
.replace(/^-+|-+$/g, "") || AUTH_COOKIE_PREFIX_FALLBACK;
|
||||
return `paperclip-${scopedInstanceId}`;
|
||||
}
|
||||
|
||||
export function buildBetterAuthAdvancedOptions(input: { disableSecureCookies: boolean }) {
|
||||
return {
|
||||
cookiePrefix: deriveAuthCookiePrefix(),
|
||||
...(input.disableSecureCookies ? { useSecureCookies: false } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function headersFromNodeHeaders(rawHeaders: IncomingHttpHeaders): Headers {
|
||||
const headers = new Headers();
|
||||
for (const [key, raw] of Object.entries(rawHeaders)) {
|
||||
@@ -97,7 +116,7 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins?
|
||||
requireEmailVerification: false,
|
||||
disableSignUp: config.authDisableSignUp,
|
||||
},
|
||||
...(isHttpOnly ? { advanced: { useSecureCookies: false } } : {}),
|
||||
advanced: buildBetterAuthAdvancedOptions({ disableSecureCookies: isHttpOnly }),
|
||||
};
|
||||
|
||||
if (!baseUrl) {
|
||||
|
||||
Reference in new issue
Block a user