diff --git a/src/lib/visitor-auth.test.ts b/src/lib/visitor-auth.test.ts index d62cc40a6d..80e25e08bc 100644 --- a/src/lib/visitor-auth.test.ts +++ b/src/lib/visitor-auth.test.ts @@ -2,6 +2,7 @@ import { it, describe, expect } from 'bun:test'; import { NextRequest } from 'next/server'; import { + VisitorAuthCookieValue, getVisitorAuthCookieName, getVisitorAuthCookieValue, getVisitorAuthToken, @@ -17,14 +18,18 @@ describe('getVisitorAuthToken', () => { const request = nextRequest('https://example.com', { [getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', '123') }, }); - expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123'); + const visitorAuth = getVisitorAuthToken(request, request.nextUrl); + assertVisitorAuthCookieValue(visitorAuth); + expect(visitorAuth.token).toEqual('123'); }); it('should return the token from the cookie root basepath for a sub-path', () => { const request = nextRequest('https://example.com/hello/world', { [getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', '123') }, }); - expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123'); + const visitorAuth = getVisitorAuthToken(request, request.nextUrl); + assertVisitorAuthCookieValue(visitorAuth); + expect(visitorAuth.token).toEqual('123'); }); it('should return the closest token from the path', () => { @@ -34,7 +39,9 @@ describe('getVisitorAuthToken', () => { value: getVisitorAuthCookieValue('/hello/', '123'), }, }); - expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123'); + const visitorAuth = getVisitorAuthToken(request, request.nextUrl); + assertVisitorAuthCookieValue(visitorAuth); + expect(visitorAuth.token).toEqual('123'); }); it('should return the token from the cookie in a collection type url', () => { @@ -43,7 +50,9 @@ describe('getVisitorAuthToken', () => { value: getVisitorAuthCookieValue('/hello/v/space1/', '123'), }, }); - expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123'); + const visitorAuth = getVisitorAuthToken(request, request.nextUrl); + assertVisitorAuthCookieValue(visitorAuth); + expect(visitorAuth.token).toEqual('123'); }); it('should return undefined if no cookie and no query param', () => { @@ -52,6 +61,14 @@ describe('getVisitorAuthToken', () => { }); }); +function assertVisitorAuthCookieValue(value: unknown): asserts value is VisitorAuthCookieValue { + if (value && typeof value === 'object' && 'token' in value) { + return; + } + + throw new Error('Expected a VisitorAuthCookieValue'); +} + function nextRequest(url: string, cookies: Record = {}) { const nextUrl = new URL(url); // @ts-ignore diff --git a/src/lib/visitor-auth.ts b/src/lib/visitor-auth.ts index 659cfd5cfe..5adcd0178e 100644 --- a/src/lib/visitor-auth.ts +++ b/src/lib/visitor-auth.ts @@ -16,7 +16,10 @@ export type VisitorAuthCookieValue = { * Get the visitor authentication token for the request. This token can either be in the * query parameters or stored as a cookie. */ -export function getVisitorAuthToken(request: NextRequest, url: URL): string | undefined { +export function getVisitorAuthToken( + request: NextRequest, + url: URL, +): string | VisitorAuthCookieValue | undefined { return url.searchParams.get(VISITOR_AUTH_PARAM) ?? getVisitorAuthTokenFromCookies(request, url); } @@ -68,7 +71,10 @@ function getUrlBasePathCombinations(url: URL): string[] { * checking all cookies for a matching "visitor authentication cookie" and returning the * best possible match for the current URL. */ -function getVisitorAuthTokenFromCookies(request: NextRequest, url: URL): string | undefined { +function getVisitorAuthTokenFromCookies( + request: NextRequest, + url: URL, +): VisitorAuthCookieValue | undefined { const urlBasePaths = getUrlBasePathCombinations(url); // Try to find a visitor authentication token for the current URL. The request // for the content could be hosted on a base path like `/foo/v/bar` or `/foo` or just `/` @@ -90,14 +96,17 @@ function getVisitorAuthTokenFromCookies(request: NextRequest, url: URL): string function findVisitorAuthCookieForBasePath( request: NextRequest, basePath: string, -): string | undefined { - return Array.from(request.cookies).reduce((acc, [name, cookie]) => { - if (name === getVisitorAuthCookieName(basePath)) { - const value = JSON.parse(cookie.value) as VisitorAuthCookieValue; - if (value.basePath === basePath) { - acc = value.token; +): VisitorAuthCookieValue | undefined { + return Array.from(request.cookies).reduce( + (acc, [name, cookie]) => { + if (name === getVisitorAuthCookieName(basePath)) { + const value = JSON.parse(cookie.value) as VisitorAuthCookieValue; + if (value.basePath === basePath) { + acc = value; + } } - } - return acc; - }, undefined); + return acc; + }, + undefined, + ); } diff --git a/src/middleware.ts b/src/middleware.ts index 9de61c27b0..b2e926fe9d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -22,6 +22,7 @@ import { buildVersion } from '@/lib/build'; import { createContentSecurityPolicyNonce, getContentSecurityPolicy } from '@/lib/csp'; import { getURLLookupAlternatives, normalizeURL } from '@/lib/middleware'; import { + VisitorAuthCookieValue, getVisitorAuthCookieName, getVisitorAuthCookieValue, getVisitorAuthToken, @@ -588,7 +589,7 @@ async function lookupSpaceInMultiPathMode(request: NextRequest, url: URL): Promi */ async function lookupSpaceByAPI( lookupURL: URL, - visitorAuthToken: string | undefined, + visitorAuthToken: ReturnType, ): Promise { const url = stripURLSearch(lookupURL); const lookup = getURLLookupAlternatives(url); @@ -598,9 +599,17 @@ async function lookupSpaceByAPI( ); const result = await race(lookup.urls, async (alternative, { signal }) => { - const data = await getPublishedContentByUrl(alternative.url, visitorAuthToken, { - signal, - }); + const data = await getPublishedContentByUrl( + alternative.url, + typeof visitorAuthToken === 'undefined' + ? undefined + : typeof visitorAuthToken === 'string' + ? visitorAuthToken + : visitorAuthToken.token, + { + signal, + }, + ); if ('error' in data) { if (alternative.primary) { @@ -672,22 +681,36 @@ async function lookupSpaceByAPI( */ function getLookupResultForVisitorAuth( basePath: string, - visitorAuthToken: string, + visitorAuthToken: string | VisitorAuthCookieValue, ): Partial { return { // No caching for content served with visitor auth cacheMaxAge: undefined, cacheTags: [], cookies: { - [getVisitorAuthCookieName(basePath)]: { - value: getVisitorAuthCookieValue(basePath, visitorAuthToken), - options: { - httpOnly: true, - sameSite: 'none', - secure: process.env.NODE_ENV === 'production', - maxAge: 7 * 24 * 60 * 60, - }, - }, + /** + * If the visitorAuthToken has been retrieved from a cookie, we set it back only + * if the basePath matches the current one. This is to avoid setting cookie for + * different base paths. + */ + ...(typeof visitorAuthToken === 'string' || visitorAuthToken.basePath === basePath + ? { + [getVisitorAuthCookieName(basePath)]: { + value: getVisitorAuthCookieValue( + basePath, + typeof visitorAuthToken === 'string' + ? visitorAuthToken + : visitorAuthToken.token, + ), + options: { + httpOnly: true, + sameSite: 'none', + secure: process.env.NODE_ENV === 'production', + maxAge: 7 * 24 * 60 * 60, + }, + }, + } + : {}), }, }; }