fix(auth): namespace Goto cookies #17

Merged
eleith merged 1 commits from feat/auth-cookie-prefix into main 2026-11-10 18:41:29 +00:00
3 changed files with 80 additions and 0 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
'@goto/web': patch
'@goto/worker': patch
'@goto/cli': patch
---
Namespace authentication cookies with the native Better Auth `goto` prefix so Goto does not overwrite other localhost apps' default authentication cookies. Existing Goto sessions require a fresh sign-in; unrelated cookies are not removed.
@@ -0,0 +1,72 @@
import { validateConfig } from '@goto/config';
import { parseSetCookieHeader } from 'better-auth/cookies';
import { expect, test, vi } from 'vitest';
import { createAuthentication } from './index.js';
vi.mock('$app/env', () => ({ building: false }));
const credentials = { username: 'cookie-test-admin', password: 'cookie-test-password' };
const foreignCookies =
'better-auth.session_token=other-app-token; better-auth.session_data=other-app-data';
function authentication(origin: string) {
const { configuration } = validateConfig(
{
site: { url: origin },
auth: { secret: 'cookie-test-secret-with-at-least-thirty-two-characters', credentials },
domains: { 'go.example.test': { links: {} } }
},
{ env: {} }
);
return createAuthentication(configuration);
}
function requestCookies(response: Response): string {
const cookies = parseSetCookieHeader(response.headers.get('set-cookie') ?? '');
return [...cookies]
.filter(([, attributes]) => attributes.value)
.map(([name, attributes]) => `${name}=${attributes.value}`)
.join('; ');
}
test.each([
{ origin: 'http://localhost:5173', prefix: 'goto.' },
{ origin: 'https://goto.example.test', prefix: '__Secure-goto.' }
])('isolates sign-in and sign-out cookies at $origin', async ({ origin, prefix }) => {
const auth = authentication(origin);
const headers = {
origin,
'content-type': 'application/json',
'x-real-ip': '192.0.2.44',
cookie: foreignCookies
};
const login = await auth.handler(
new Request(`${origin}/auth/sign-in/credentials`, {
method: 'POST',
headers,
body: JSON.stringify(credentials)
})
);
expect(login.status).toBe(200);
const issued = parseSetCookieHeader(login.headers.get('set-cookie') ?? '');
expect([...issued.keys()]).toContain(`${prefix}session_token`);
expect([...issued.keys()]).toContain(`${prefix}session_data`);
expect([...issued.keys()].every((name) => name.startsWith(prefix))).toBe(true);
const cookie = `${requestCookies(login)}; ${foreignCookies}`;
const session = await auth.api.getSession({ headers: new Headers({ cookie }) });
expect(session?.user.name).toBe(credentials.username);
const logout = await auth.handler(
new Request(`${origin}/auth/sign-out`, {
method: 'POST',
headers: { ...headers, cookie },
body: '{}'
})
);
expect(logout.status).toBe(200);
const cleared = parseSetCookieHeader(logout.headers.get('set-cookie') ?? '');
expect([...cleared.keys()]).toContain(`${prefix}session_token`);
expect([...cleared.keys()]).toContain(`${prefix}session_data`);
expect([...cleared.keys()].every((name) => name.startsWith(prefix))).toBe(true);
});
+1
View File
@@ -34,6 +34,7 @@ export function createAuthentication(cfg: Immutable<GotoConfiguration>) {
accountLinking: { enabled: false }
},
advanced: {
cookiePrefix: 'goto',
disableOriginCheck: false,
ipAddress: { ipAddressHeaders: ['x-real-ip'] }
},