Skip to content

Commit

Permalink
Do not store the GitBook Visitor cookie in another cookie.
Browse files Browse the repository at this point in the history
  • Loading branch information
emmerich committed Dec 19, 2024
1 parent 24b18d2 commit c537b19
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 53 deletions.
8 changes: 4 additions & 4 deletions packages/gitbook/e2e/pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { DeepPartial } from 'ts-essentials';
import {
getVisitorAuthCookieName,
getVisitorAuthCookieValue,
getVisitorCustomCookieName,
VISITOR_TOKEN_COOKIE,
} from '@/lib/visitor-token';

import { getContentTestURL } from '../tests/utils';
Expand Down Expand Up @@ -1124,7 +1124,7 @@ const testCases: TestsCase[] = [
);
return [
{
name: getVisitorCustomCookieName(),
name: VISITOR_TOKEN_COOKIE,
value: token,
httpOnly: true,
},
Expand Down Expand Up @@ -1163,7 +1163,7 @@ const testCases: TestsCase[] = [
);
return [
{
name: getVisitorCustomCookieName(),
name: VISITOR_TOKEN_COOKIE,
value: token,
httpOnly: true,
},
Expand Down Expand Up @@ -1203,7 +1203,7 @@ const testCases: TestsCase[] = [
);
return [
{
name: getVisitorCustomCookieName(),
name: VISITOR_TOKEN_COOKIE,
value: token,
httpOnly: true,
},
Expand Down
67 changes: 40 additions & 27 deletions packages/gitbook/src/lib/visitor-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { NextRequest } from 'next/server';
import hash from 'object-hash';

const VISITOR_AUTH_PARAM = 'jwt_token';
const VISITOR_AUTH_COOKIE_ROOT = 'gitbook-visitor-token';
const VISITOR_AUTH_COOKIE_ROOT = 'gitbook-visitor-token~';
export const VISITOR_TOKEN_COOKIE = 'gitbook-visitor-token';

/**
* The contents of the visitor authentication cookie.
Expand All @@ -12,15 +13,28 @@ export type VisitorAuthCookieValue = {
token: string;
};

export function isVisitorAuthTokenFromCookies(
visitorAuthToken: NonNullable<ReturnType<typeof getVisitorToken>>,
) {
return (
typeof visitorAuthToken !== 'string' &&
'basePath' in visitorAuthToken &&
'token' in visitorAuthToken
);
}
/**
* The result of a visitor token lookup.
*/
export type VisitorTokenLookup =
| {
/** A visitor token was found in the URL. */
source: 'url';
token: string;
}
| {
/** A visitor auth token was found in a VA cookie */
source: 'visitor-auth-cookie';
basePath: string;
token: string;
}
| {
/** A visitor token (not auth) was found in a cookie. */
source: 'gitbook-visitor-cookie';
token: string;
}
/** Not visitor token was found */
| undefined;

/**
* Get the visitor token for the request. This token can either be in the
Expand All @@ -29,12 +43,20 @@ export function isVisitorAuthTokenFromCookies(
export function getVisitorToken(
request: NextRequest,
url: URL | NextRequest['nextUrl'],
): string | VisitorAuthCookieValue | undefined {
return (
url.searchParams.get(VISITOR_AUTH_PARAM) ??
getVisitorAuthTokenFromCookies(request, url) ??
getVisitorCustomTokenFromCookies(request)
);
): VisitorTokenLookup {
if (url.searchParams.get(VISITOR_AUTH_PARAM)) {
return { source: 'url', token: url.searchParams.get(VISITOR_AUTH_PARAM)! };
}

const visitorAuthToken = getVisitorAuthTokenFromCookies(request, url);
if (visitorAuthToken) {
return { source: 'visitor-auth-cookie', ...visitorAuthToken };
}

const visitorCustomToken = getVisitorCustomTokenFromCookies(request);
if (visitorCustomToken) {
return { source: 'gitbook-visitor-cookie', token: visitorCustomToken };
}
}

/**
Expand All @@ -44,7 +66,7 @@ export function getVisitorToken(
* different content hosted on the same subdomain.
*/
export function getVisitorAuthCookieName(basePath: string): string {
return `${VISITOR_AUTH_COOKIE_ROOT}~${hash(basePath)}`;
return `${VISITOR_AUTH_COOKIE_ROOT}${hash(basePath)}`;
}

/**
Expand All @@ -55,14 +77,6 @@ export function getVisitorAuthCookieValue(basePath: string, token: string): stri
return JSON.stringify(value);
}

/**
* Get the name of the customer cookie that can be set by third party backends
* to pass information in the form of claims about the visitor.
*/
export function getVisitorCustomCookieName(): string {
return VISITOR_AUTH_COOKIE_ROOT;
}

/**
* Normalize the URL by removing the visitor authentication token from the query parameters (if present).
*/
Expand Down Expand Up @@ -126,9 +140,8 @@ function getVisitorAuthTokenFromCookies(
* The cookie should contain as value a JWT encoded token that contains the claims of the visitor.
*/
function getVisitorCustomTokenFromCookies(request: NextRequest): string | undefined {
const cookieName = getVisitorCustomCookieName();
const visitorCustomCookie = Array.from(request.cookies).find(
([, cookie]) => cookie.name === cookieName,
([, cookie]) => cookie.name === VISITOR_TOKEN_COOKIE,
);
return visitorCustomCookie ? visitorCustomCookie[1].value : undefined;
}
Expand Down
36 changes: 14 additions & 22 deletions packages/gitbook/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ import { buildVersion } from '@/lib/build';
import { createContentSecurityPolicyNonce, getContentSecurityPolicy } from '@/lib/csp';
import { getURLLookupAlternatives, normalizeURL, setMiddlewareHeader } from '@/lib/middleware';
import {
VisitorAuthCookieValue,
VisitorTokenLookup,
getVisitorAuthCookieName,
getVisitorAuthCookieValue,
getVisitorToken,
isVisitorAuthTokenFromCookies,
normalizeVisitorAuthURL,
} from '@/lib/visitor-token';

Expand Down Expand Up @@ -677,7 +676,7 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
*/
async function lookupSiteByAPI(
lookupURL: URL,
visitorAuthToken: ReturnType<typeof getVisitorToken>,
visitorTokenLookup: VisitorTokenLookup,
): Promise<LookupResult> {
const url = stripURLSearch(lookupURL);
const lookup = getURLLookupAlternatives(url);
Expand All @@ -689,18 +688,13 @@ async function lookupSiteByAPI(
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
// For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
const redirectOnError =
typeof visitorAuthToken !== 'undefined' && isVisitorAuthTokenFromCookies(visitorAuthToken);
const redirectOnError = visitorTokenLookup?.source === 'visitor-auth-cookie';

const result = await race(lookup.urls, async (alternative, { signal }) => {
const data = await getPublishedContentByUrl(
alternative.url,
typeof visitorAuthToken === 'undefined'
? undefined
: isVisitorAuthTokenFromCookies(visitorAuthToken)
? visitorAuthToken.token
: visitorAuthToken,
typeof visitorAuthToken === 'undefined' ? undefined : redirectOnError,
visitorTokenLookup?.token,
redirectOnError,
{
signal,
},
Expand Down Expand Up @@ -797,27 +791,25 @@ async function lookupSiteByAPI(
*/
function getLookupResultForVisitorAuth(
basePath: string,
visitorAuthToken: string | VisitorAuthCookieValue,
visitorTokenLookup: VisitorTokenLookup,
): Partial<LookupResult> {
return {
// No caching for content served with visitor auth
cacheMaxAge: undefined,
cacheTags: [],
cookies: {
/**
* 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.
* If the visitor token has been retrieve from the URL, or if its a VA cookie and the basePath is the same, set it
* as a cookie on the response.
*
* Note that we do not re-store the gitbook-visitor-cookie in another cookie, to maintain a single source of truth.
*/
...(typeof visitorAuthToken === 'string' || visitorAuthToken.basePath === basePath
...(visitorTokenLookup?.source === 'url' ||
(visitorTokenLookup?.source === 'visitor-auth-cookie' &&
visitorTokenLookup.basePath === basePath)
? {
[getVisitorAuthCookieName(basePath)]: {
value: getVisitorAuthCookieValue(
basePath,
typeof visitorAuthToken === 'string'
? visitorAuthToken
: visitorAuthToken.token,
),
value: getVisitorAuthCookieValue(basePath, visitorTokenLookup.token),
options: {
httpOnly: true,
sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
Expand Down

0 comments on commit c537b19

Please sign in to comment.