Skip to content

Commit 98245e5

Browse files
Adapt code to pull token from customer backend generated custom cookies (#2650)
Co-authored-by: Steven Hall <[email protected]>
1 parent 0b6ddca commit 98245e5

File tree

6 files changed

+250
-56
lines changed

6 files changed

+250
-56
lines changed

.changeset/blue-baboons-exercise.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': minor
3+
---
4+
5+
Adapt code to pull token from customer backend generated custom cookies

packages/gitbook/e2e/pages.spec.ts

+148-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import jwt from 'jsonwebtoken';
1717
import rison from 'rison';
1818
import { DeepPartial } from 'ts-essentials';
1919

20-
import { getVisitorAuthCookieName, getVisitorAuthCookieValue } from '@/lib/visitor-auth';
20+
import {
21+
getVisitorAuthCookieName,
22+
getVisitorAuthCookieValue,
23+
VISITOR_TOKEN_COOKIE,
24+
} from '@/lib/visitor-token';
2125

2226
import { getContentTestURL } from '../tests/utils';
2327

@@ -1081,6 +1085,149 @@ const testCases: TestsCase[] = [
10811085
},
10821086
],
10831087
},
1088+
{
1089+
name: 'Adaptive Content - Public',
1090+
baseUrl: `https://gitbook-open-e2e-sites.gitbook.io/adaptive-content-public/`,
1091+
tests: [
1092+
{
1093+
name: 'No custom cookie',
1094+
url: '',
1095+
run: async (page) => {
1096+
const welcomePage = page
1097+
.locator('a[class*="group\\/toclink"]')
1098+
.filter({ hasText: 'Welcome Page' });
1099+
const alphaUserPage = page
1100+
.locator('a[class*="group\\/toclink"]')
1101+
.filter({ hasText: 'Alpha User' });
1102+
const betaUserPage = page
1103+
.locator('a[class*="group\\/toclink"]')
1104+
.filter({ hasText: 'Beta User' });
1105+
1106+
await expect(welcomePage).toBeVisible();
1107+
await expect(alphaUserPage).toHaveCount(0);
1108+
await expect(betaUserPage).toHaveCount(0);
1109+
},
1110+
},
1111+
{
1112+
name: 'Custom cookie with isAlphaUser claim',
1113+
cookies: (() => {
1114+
const privateKey = '4ddd3c2f-e4b7-4e73-840b-526c3be19746';
1115+
const token = jwt.sign(
1116+
{
1117+
name: 'gitbook-open-tests',
1118+
isAlphaUser: true,
1119+
},
1120+
privateKey,
1121+
{
1122+
expiresIn: '24h',
1123+
},
1124+
);
1125+
return [
1126+
{
1127+
name: VISITOR_TOKEN_COOKIE,
1128+
value: token,
1129+
httpOnly: true,
1130+
},
1131+
];
1132+
})(),
1133+
url: '',
1134+
run: async (page) => {
1135+
const welcomePage = page
1136+
.locator('a[class*="group\\/toclink"]')
1137+
.filter({ hasText: 'Welcome Page' });
1138+
const alphaUserPage = page
1139+
.locator('a[class*="group\\/toclink"]')
1140+
.filter({ hasText: 'Alpha User' });
1141+
const betaUserPage = page
1142+
.locator('a[class*="group\\/toclink"]')
1143+
.filter({ hasText: 'Beta User' });
1144+
1145+
await expect(welcomePage).toBeVisible();
1146+
await expect(alphaUserPage).toBeVisible();
1147+
await expect(betaUserPage).toHaveCount(0);
1148+
},
1149+
},
1150+
{
1151+
name: 'Custom cookie with isBetaUser claim',
1152+
cookies: (() => {
1153+
const privateKey = '4ddd3c2f-e4b7-4e73-840b-526c3be19746';
1154+
const token = jwt.sign(
1155+
{
1156+
name: 'gitbook-open-tests',
1157+
isBetaUser: true,
1158+
},
1159+
privateKey,
1160+
{
1161+
expiresIn: '24h',
1162+
},
1163+
);
1164+
return [
1165+
{
1166+
name: VISITOR_TOKEN_COOKIE,
1167+
value: token,
1168+
httpOnly: true,
1169+
},
1170+
];
1171+
})(),
1172+
url: '',
1173+
run: async (page) => {
1174+
const welcomePage = page
1175+
.locator('a[class*="group\\/toclink"]')
1176+
.filter({ hasText: 'Welcome Page' });
1177+
const alphaUserPage = page
1178+
.locator('a[class*="group\\/toclink"]')
1179+
.filter({ hasText: 'Alpha User' });
1180+
const betaUserPage = page
1181+
.locator('a[class*="group\\/toclink"]')
1182+
.filter({ hasText: 'Beta User' });
1183+
1184+
await expect(welcomePage).toBeVisible();
1185+
await expect(betaUserPage).toBeVisible();
1186+
await expect(alphaUserPage).toHaveCount(0);
1187+
},
1188+
},
1189+
{
1190+
name: 'Custom cookie with isAlphaUser & isBetaUser claims',
1191+
cookies: (() => {
1192+
const privateKey = '4ddd3c2f-e4b7-4e73-840b-526c3be19746';
1193+
const token = jwt.sign(
1194+
{
1195+
name: 'gitbook-open-tests',
1196+
isAlphaUser: true,
1197+
isBetaUser: true,
1198+
},
1199+
privateKey,
1200+
{
1201+
expiresIn: '24h',
1202+
},
1203+
);
1204+
return [
1205+
{
1206+
name: VISITOR_TOKEN_COOKIE,
1207+
value: token,
1208+
httpOnly: true,
1209+
},
1210+
];
1211+
})(),
1212+
url: '',
1213+
run: async (page) => {
1214+
const welcomePage = page
1215+
.locator('a[class*="group\\/toclink"]')
1216+
.filter({ hasText: 'Welcome Page' });
1217+
const alphaUserPage = page
1218+
.locator('a[class*="group\\/toclink"]')
1219+
.filter({ hasText: 'Alpha User' });
1220+
const betaUserPage = page
1221+
.locator('a[class*="group\\/toclink"]')
1222+
.filter({ hasText: 'Beta User' });
1223+
1224+
await expect(welcomePage).toBeVisible();
1225+
await expect(betaUserPage).toBeVisible();
1226+
await expect(alphaUserPage).toBeVisible();
1227+
},
1228+
},
1229+
],
1230+
},
10841231
{
10851232
name: 'Tables',
10861233
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',

packages/gitbook/src/lib/api.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ export const getPublishedContentByUrl = cache({
214214
get: async (
215215
url: string,
216216
visitorAuthToken: string | undefined,
217-
redirectOnError: boolean | undefined,
217+
// Prefer undefined for a better cache key.
218+
redirectOnError: true | undefined,
218219
options: CacheFunctionOptions,
219220
) => {
220221
try {

packages/gitbook/src/lib/visitor-auth.test.ts renamed to packages/gitbook/src/lib/visitor-token.test.ts

+18-12
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,22 @@ import { it, describe, expect } from 'bun:test';
22
import { NextRequest } from 'next/server';
33

44
import {
5-
VisitorAuthCookieValue,
65
getVisitorAuthCookieName,
76
getVisitorAuthCookieValue,
8-
getVisitorAuthToken,
9-
} from './visitor-auth';
7+
getVisitorToken,
8+
} from './visitor-token';
109

1110
describe('getVisitorAuthToken', () => {
1211
it('should return the token from the query parameters', () => {
1312
const request = nextRequest('https://example.com?jwt_token=123');
14-
expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123');
13+
expect(getVisitorToken(request, request.nextUrl)).toEqual({ source: 'url', token: '123' });
1514
});
1615

1716
it('should return the token from the cookie root basepath', () => {
1817
const request = nextRequest('https://example.com', {
1918
[getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', '123') },
2019
});
21-
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
20+
const visitorAuth = getVisitorToken(request, request.nextUrl);
2221
assertVisitorAuthCookieValue(visitorAuth);
2322
expect(visitorAuth.token).toEqual('123');
2423
});
@@ -27,7 +26,7 @@ describe('getVisitorAuthToken', () => {
2726
const request = nextRequest('https://example.com/hello/world', {
2827
[getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', '123') },
2928
});
30-
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
29+
const visitorAuth = getVisitorToken(request, request.nextUrl);
3130
assertVisitorAuthCookieValue(visitorAuth);
3231
expect(visitorAuth.token).toEqual('123');
3332
});
@@ -39,7 +38,7 @@ describe('getVisitorAuthToken', () => {
3938
value: getVisitorAuthCookieValue('/hello/', '123'),
4039
},
4140
});
42-
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
41+
const visitorAuth = getVisitorToken(request, request.nextUrl);
4342
assertVisitorAuthCookieValue(visitorAuth);
4443
expect(visitorAuth.token).toEqual('123');
4544
});
@@ -50,14 +49,14 @@ describe('getVisitorAuthToken', () => {
5049
value: getVisitorAuthCookieValue('/hello/v/space1/', '123'),
5150
},
5251
});
53-
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
52+
const visitorAuth = getVisitorToken(request, request.nextUrl);
5453
assertVisitorAuthCookieValue(visitorAuth);
5554
expect(visitorAuth.token).toEqual('123');
5655
});
5756

5857
it('should return undefined if no cookie and no query param', () => {
5958
const request = nextRequest('https://example.com');
60-
expect(getVisitorAuthToken(request, request.nextUrl)).toBeUndefined();
59+
expect(getVisitorToken(request, request.nextUrl)).toBeUndefined();
6160
});
6261

6362
// For backwards compatibility
@@ -69,14 +68,21 @@ describe('getVisitorAuthToken', () => {
6968
},
7069
});
7170

72-
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
71+
const visitorAuth = getVisitorToken(request, request.nextUrl);
7372
assertVisitorAuthCookieValue(visitorAuth);
7473
expect(visitorAuth.token).toEqual('gotcha');
7574
});
7675
});
7776

78-
function assertVisitorAuthCookieValue(value: unknown): asserts value is VisitorAuthCookieValue {
79-
if (value && typeof value === 'object' && 'token' in value) {
77+
function assertVisitorAuthCookieValue(
78+
value: unknown,
79+
): asserts value is { source: 'visitor-auth-cookie'; basePath: string; token: string } {
80+
if (
81+
value &&
82+
typeof value === 'object' &&
83+
'source' in value &&
84+
value.source === 'visitor-auth-cookie'
85+
) {
8086
return;
8187
}
8288

packages/gitbook/src/lib/visitor-auth.ts renamed to packages/gitbook/src/lib/visitor-token.ts

+59-16
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,64 @@ import type { NextRequest } from 'next/server';
22
import hash from 'object-hash';
33

44
const VISITOR_AUTH_PARAM = 'jwt_token';
5-
const VISITOR_AUTH_COOKIE_ROOT = 'gitbook-visitor-token';
5+
const VISITOR_AUTH_COOKIE_ROOT = 'gitbook-visitor-token~';
6+
export const VISITOR_TOKEN_COOKIE = 'gitbook-visitor-token';
67

78
/**
89
* The contents of the visitor authentication cookie.
910
*/
10-
export type VisitorAuthCookieValue = {
11+
type VisitorAuthCookieValue = {
1112
basePath: string;
1213
token: string;
1314
};
1415

15-
export function isVisitorAuthTokenFromCookies(
16-
visitorAuthToken: NonNullable<ReturnType<typeof getVisitorAuthToken>>,
17-
) {
18-
return (
19-
typeof visitorAuthToken !== 'string' &&
20-
'basePath' in visitorAuthToken &&
21-
'token' in visitorAuthToken
22-
);
23-
}
16+
/**
17+
* The result of a visitor token lookup.
18+
*/
19+
export type VisitorTokenLookup =
20+
| {
21+
/** A visitor token was found in the URL. */
22+
source: 'url';
23+
token: string;
24+
}
25+
| {
26+
/** A visitor auth token was found in a VA cookie */
27+
source: 'visitor-auth-cookie';
28+
basePath: string;
29+
token: string;
30+
}
31+
| {
32+
/** A visitor token (not auth) was found in a cookie. */
33+
source: 'gitbook-visitor-cookie';
34+
token: string;
35+
}
36+
/** Not visitor token was found */
37+
| undefined;
2438

2539
/**
26-
* Get the visitor authentication token for the request. This token can either be in the
40+
* Get the visitor token for the request. This token can either be in the
2741
* query parameters or stored as a cookie.
2842
*/
29-
export function getVisitorAuthToken(
43+
export function getVisitorToken(
3044
request: NextRequest,
3145
url: URL | NextRequest['nextUrl'],
32-
): string | VisitorAuthCookieValue | undefined {
33-
return url.searchParams.get(VISITOR_AUTH_PARAM) ?? getVisitorAuthTokenFromCookies(request, url);
46+
): VisitorTokenLookup {
47+
const fromUrl = url.searchParams.get(VISITOR_AUTH_PARAM);
48+
49+
// Allow the empty string to come through
50+
if (fromUrl !== null && fromUrl !== undefined) {
51+
return { source: 'url', token: fromUrl };
52+
}
53+
54+
const visitorAuthToken = getVisitorAuthTokenFromCookies(request, url);
55+
if (visitorAuthToken) {
56+
return { source: 'visitor-auth-cookie', ...visitorAuthToken };
57+
}
58+
59+
const visitorCustomToken = getVisitorCustomTokenFromCookies(request);
60+
if (visitorCustomToken) {
61+
return { source: 'gitbook-visitor-cookie', token: visitorCustomToken };
62+
}
3463
}
3564

3665
/**
@@ -40,7 +69,7 @@ export function getVisitorAuthToken(
4069
* different content hosted on the same subdomain.
4170
*/
4271
export function getVisitorAuthCookieName(basePath: string): string {
43-
return `${VISITOR_AUTH_COOKIE_ROOT}~${hash(basePath)}`;
72+
return `${VISITOR_AUTH_COOKIE_ROOT}${hash(basePath)}`;
4473
}
4574

4675
/**
@@ -106,6 +135,20 @@ function getVisitorAuthTokenFromCookies(
106135
return undefined;
107136
}
108137

138+
/**
139+
* Return the value of a custom visitor cookie that can be set by third party backends
140+
* when they authenticate their users off flow to relay information in the form of claims
141+
* about the visitor.
142+
*
143+
* The cookie should contain as value a JWT encoded token that contains the claims of the visitor.
144+
*/
145+
function getVisitorCustomTokenFromCookies(request: NextRequest): string | undefined {
146+
const visitorCustomCookie = Array.from(request.cookies).find(
147+
([, cookie]) => cookie.name === VISITOR_TOKEN_COOKIE,
148+
);
149+
return visitorCustomCookie ? visitorCustomCookie[1].value : undefined;
150+
}
151+
109152
/**
110153
* Loop through all cookies and find the visitor authentication token for a given base path.
111154
*/

0 commit comments

Comments
 (0)