Skip to content

Commit fd5bf8c

Browse files
committed
feat(cookie): export/import chips cookies
Export/import partitionKey in Chromium. It is used to partition cookies. Not exposed in the public API yet. Reference: #35598
1 parent 0286244 commit fd5bf8c

File tree

4 files changed

+221
-28
lines changed

4 files changed

+221
-28
lines changed

packages/playwright-core/src/protocol/validator.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ scheme.SetNetworkCookie = tObject({
136136
httpOnly: tOptional(tBoolean),
137137
secure: tOptional(tBoolean),
138138
sameSite: tOptional(tEnum(['Strict', 'Lax', 'None'])),
139+
partitionKey: tOptional(tObject({
140+
topLevelSite: tString,
141+
hasCrossSiteAncestor: tBoolean,
142+
})),
143+
sameParty: tOptional(tBoolean),
144+
sourcePort: tOptional(tNumber),
139145
});
140146
scheme.NetworkCookie = tObject({
141147
name: tString,
@@ -146,6 +152,12 @@ scheme.NetworkCookie = tObject({
146152
httpOnly: tBoolean,
147153
secure: tBoolean,
148154
sameSite: tEnum(['Strict', 'Lax', 'None']),
155+
partitionKey: tOptional(tObject({
156+
topLevelSite: tString,
157+
hasCrossSiteAncestor: tBoolean,
158+
})),
159+
sameParty: tOptional(tBoolean),
160+
sourcePort: tOptional(tNumber),
149161
});
150162
scheme.NameValue = tObject({
151163
name: tString,

packages/protocol/src/channels.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ export type SetNetworkCookie = {
260260
httpOnly?: boolean,
261261
secure?: boolean,
262262
sameSite?: 'Strict' | 'Lax' | 'None',
263+
partitionKey?: {
264+
topLevelSite: string,
265+
hasCrossSiteAncestor: boolean,
266+
},
267+
sameParty?: boolean,
268+
sourcePort?: number,
263269
};
264270

265271
export type NetworkCookie = {
@@ -271,6 +277,12 @@ export type NetworkCookie = {
271277
httpOnly: boolean,
272278
secure: boolean,
273279
sameSite: 'Strict' | 'Lax' | 'None',
280+
partitionKey?: {
281+
topLevelSite: string,
282+
hasCrossSiteAncestor: boolean,
283+
},
284+
sameParty?: boolean,
285+
sourcePort?: number,
274286
};
275287

276288
export type NameValue = {

packages/protocol/src/protocol.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,13 @@ SetNetworkCookie:
223223
- Strict
224224
- Lax
225225
- None
226+
partitionKey:
227+
type: object?
228+
properties:
229+
topLevelSite: string
230+
hasCrossSiteAncestor: boolean
231+
sameParty: boolean?
232+
sourcePort: number?
226233

227234

228235
NetworkCookie:
@@ -241,6 +248,13 @@ NetworkCookie:
241248
- Strict
242249
- Lax
243250
- None
251+
partitionKey:
252+
type: object?
253+
properties:
254+
topLevelSite: string
255+
hasCrossSiteAncestor: boolean
256+
sameParty: boolean?
257+
sourcePort: number?
244258

245259

246260
NameValue:

tests/library/browsercontext-cookies-third-party.spec.ts

Lines changed: 183 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,64 +23,129 @@ test.use({
2323
ignoreHTTPSErrors: true,
2424
});
2525

26-
test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac }) => {
27-
httpsServer.setRoute('/empty.html', (req, res) => {
28-
res.setHeader('Set-Cookie', `name=value; SameSite=None; Path=/; Secure;`);
26+
function addCommonCookieHandlers(httpsServer: TestServer) {
27+
// '/set-cookie.html' handlers are added in the tests.
28+
httpsServer.setRoute('/read-cookie.html', (req, res) => {
2929
res.setHeader('Content-Type', 'text/html');
3030
const cookies = req.headers.cookie?.split(';').map(c => c.trim()).sort().join('; ');
3131
res.end(`Received cookie: ${cookies}`);
3232
});
33-
httpsServer.setRoute('/with-frame.html', (req, res) => {
33+
httpsServer.setRoute('/frame-set-cookie.html', (req, res) => {
34+
res.setHeader('Content-Type', 'text/html');
35+
res.end(`<iframe src='${httpsServer.PREFIX}/set-cookie.html'></iframe>`);
36+
});
37+
httpsServer.setRoute('/frame-read-cookie.html', (req, res) => {
3438
res.setHeader('Content-Type', 'text/html');
35-
res.end(`<iframe src='${httpsServer.PREFIX}/empty.html'></iframe>`);
39+
res.end(`<iframe src='${httpsServer.PREFIX}/read-cookie.html'></iframe>`);
3640
});
41+
}
3742

38-
await page.goto(httpsServer.EMPTY_PAGE);
39-
await page.goto(httpsServer.EMPTY_PAGE);
40-
expect(await page.locator('body').textContent()).toBe('Received cookie: name=value');
43+
async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean) {
44+
addCommonCookieHandlers(httpsServer);
45+
httpsServer.setRoute('/set-cookie.html', (req, res) => {
46+
res.setHeader('Set-Cookie', `name=value${req.headers.referer ? '-third-party' : '-top-level'}; SameSite=None; Path=/; Secure;`);
47+
res.setHeader('Content-Type', 'text/html');
48+
res.end();
49+
});
4150

42-
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/with-frame.html');
51+
await page.goto(httpsServer.PREFIX + '/set-cookie.html');
52+
await page.goto(httpsServer.PREFIX + '/read-cookie.html');
53+
expect(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level');
54+
55+
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html');
4356
const frameBody = page.locator('iframe').contentFrame().locator('body');
4457

4558
// WebKit does not support third-party cookies without a 'Partition' attribute.
4659
if (browserName === 'webkit' && isMac)
4760
await expect(frameBody).toHaveText('Received cookie: undefined');
4861
else
49-
await expect(frameBody).toHaveText('Received cookie: name=value');
62+
await expect(frameBody).toHaveText('Received cookie: name=value-top-level');
63+
64+
// Set cookie and do second navigation.
65+
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-set-cookie.html');
66+
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html');
67+
const expectedThirdParty = browserName === 'webkit' ?
68+
'Received cookie: undefined' :
69+
'Received cookie: name=value-third-party';
70+
await expect(frameBody).toHaveText(expectedThirdParty);
71+
72+
// Check again the top-level cookie.
73+
await page.goto(httpsServer.PREFIX + '/read-cookie.html');
74+
const expectedTopLevel = browserName === 'webkit' && isMac ?
75+
'Received cookie: name=value-top-level' :
76+
'Received cookie: name=value-third-party';
77+
expect(await page.locator('body').textContent()).toBe(expectedTopLevel);
78+
79+
return {
80+
expectedTopLevel,
81+
expectedThirdParty,
82+
};
83+
}
84+
85+
test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => {
86+
await runNonPartitionedTest(page, httpsServer, browserName, isMac);
5087
});
5188

52-
test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac }) => {
53-
httpsServer.setRoute('/empty.html', (req, res) => {
89+
test(`save/load third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => {
90+
// Run the test to populate the cookies.
91+
const { expectedTopLevel, expectedThirdParty } = await runNonPartitionedTest(page, httpsServer, browserName, isMac);
92+
93+
async function checkCookies(page: Page) {
94+
// Check top-level cookie first.
95+
await page.goto(httpsServer.PREFIX + '/read-cookie.html');
96+
expect.soft(await page.locator('body').textContent()).toBe(expectedTopLevel);
97+
98+
// Check third-party cookie.
99+
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html');
100+
const frameBody = page.locator('iframe').contentFrame().locator('body');
101+
await expect.soft(frameBody).toHaveText(expectedThirdParty);
102+
}
103+
104+
await checkCookies(page);
105+
106+
await test.step('export via cookies/addCookies', async () => {
107+
const cookies = await page.context().cookies();
108+
const context2 = await browser.newContext();
109+
await context2.addCookies(cookies);
110+
const page2 = await context2.newPage();
111+
await checkCookies(page2);
112+
});
113+
114+
await test.step('export via storageState', async () => {
115+
const storageState = await page.context().storageState();
116+
const context3 = await browser.newContext({ storageState });
117+
const page3 = await context3.newPage();
118+
await checkCookies(page3);
119+
});
120+
});
121+
122+
async function runPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean) {
123+
addCommonCookieHandlers(httpsServer);
124+
httpsServer.setRoute('/set-cookie.html', (req, res) => {
54125
res.setHeader('Set-Cookie', [
55-
`name=value; SameSite=None; Path=/; Secure; Partitioned;`,
126+
`name=value${req.headers.referer ? '-partitioned' : '-top-level'}; SameSite=None; Path=/; Secure; Partitioned;`,
56127
`nonPartitionedName=value; SameSite=None; Path=/; Secure;`
57128
]);
58-
res.setHeader('Content-Type', 'text/html');
59-
const cookies = req.headers.cookie?.split(';').map(c => c.trim()).sort().join('; ');
60-
res.end(`Received cookie: ${cookies}`);
61-
});
62-
httpsServer.setRoute('/with-frame.html', (req, res) => {
63-
res.setHeader('Content-Type', 'text/html');
64-
res.end(`<iframe src='${httpsServer.PREFIX}/empty.html'></iframe>`);
129+
res.end();
65130
});
66131

67-
await page.goto(httpsServer.EMPTY_PAGE);
68-
await page.goto(httpsServer.EMPTY_PAGE);
69-
expect(await page.locator('body').textContent()).toBe('Received cookie: name=value; nonPartitionedName=value');
132+
await page.goto(httpsServer.PREFIX + '/set-cookie.html');
133+
await page.goto(httpsServer.PREFIX + '/read-cookie.html');
134+
expect(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level; nonPartitionedName=value');
70135

71-
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/with-frame.html');
136+
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html');
72137
const frameBody = page.locator('iframe').contentFrame().locator('body');
73138

74139
// Firefox cookie partitioning is disabled in Firefox.
75140
// TODO: reenable cookie partitioning?
76141
if (browserName === 'firefox') {
77-
await expect(frameBody).toHaveText('Received cookie: name=value; nonPartitionedName=value');
142+
await expect(frameBody).toHaveText('Received cookie: name=value-top-level; nonPartitionedName=value');
78143
return;
79144
}
80145

81146
// Linux and Windows WebKit builds do not partition third-party cookies at all.
82147
if (browserName === 'webkit' && !isMac) {
83-
await expect(frameBody).toHaveText('Received cookie: name=value; nonPartitionedName=value');
148+
await expect(frameBody).toHaveText('Received cookie: name=value-top-level; nonPartitionedName=value');
84149
return;
85150
}
86151

@@ -98,11 +163,101 @@ test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServ
98163
// - sets the third-party cookie for the top-level context
99164
// Second navigation:
100165
// - sends the cookie as it was just set for the (top-level site, iframe url) partition.
101-
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/with-frame.html');
166+
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-set-cookie.html');
167+
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html');
102168
if (browserName === 'webkit')
103169
await expect(frameBody).toHaveText('Received cookie: undefined');
104170
else
105-
await expect(frameBody).toHaveText('Received cookie: name=value; nonPartitionedName=value');
171+
await expect(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value');
172+
}
173+
174+
test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => {
175+
await runPartitionedTest(page, httpsServer, browserName, isMac);
176+
});
177+
178+
test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => {
179+
test.fixme(browserName === 'firefox', 'Firefox cookie partitioning is disabled in Firefox.');
180+
test.fixme(browserName === 'webkit' && !isMac, 'Linux and Windows WebKit builds do not partition third-party cookies at all.');
181+
182+
await runPartitionedTest(page, httpsServer, browserName, isMac);
183+
184+
async function checkCookies(page: Page) {
185+
// Check top-level cookie first.
186+
await page.goto(httpsServer.PREFIX + '/read-cookie.html');
187+
expect.soft(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level; nonPartitionedName=value');
188+
189+
// Check third-party cookie.
190+
await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html');
191+
const frameBody = page.locator('iframe').contentFrame().locator('body');
192+
if (browserName === 'webkit')
193+
await expect.soft(frameBody).toHaveText('Received cookie: undefined');
194+
else
195+
await expect.soft(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value');
196+
}
197+
198+
await checkCookies(page);
199+
200+
await test.step('export via cookies/addCookies', async () => {
201+
const cookies = await page.context().cookies();
202+
const context2 = await browser.newContext();
203+
await context2.addCookies(cookies);
204+
const page2 = await context2.newPage();
205+
await checkCookies(page2);
206+
});
207+
208+
await test.step('export via storageState', async () => {
209+
const storageState = await page.context().storageState();
210+
const context3 = await browser.newContext({ storageState });
211+
const page3 = await context3.newPage();
212+
await checkCookies(page3);
213+
});
214+
});
215+
216+
test(`same origin third party 'Partitioned;' cookie with different origin intermediate iframe`, async ({ page, browserName, httpsServer, isMac, browser }) => {
217+
addCommonCookieHandlers(httpsServer);
218+
httpsServer.setRoute('/set-cookie.html', (req, res) => {
219+
res.setHeader('Set-Cookie', [
220+
`name=value${req.headers.referer ? '-partitioned' : '-top-level'}; SameSite=None; Path=/; Secure; Partitioned;`,
221+
`nonPartitionedName=value; SameSite=None; Path=/; Secure;`
222+
]);
223+
res.end();
224+
});
225+
// main frame: origin1 -> iframe1: origin2 -> iframe2: origin1
226+
// In this case the cookie in iframe2 will have hasCrossSiteAncestor=true even though
227+
// the innermost frame is in origin1.
228+
httpsServer.setRoute('/top-frame-set-cookie.html', (req, res) => {
229+
res.setHeader('Content-Type', 'text/html');
230+
res.end(`<iframe src='${httpsServer.CROSS_PROCESS_PREFIX}/frame-set-cookie.html'></iframe>`);
231+
});
232+
httpsServer.setRoute('/top-frame-read-cookie.html', (req, res) => {
233+
res.setHeader('Content-Type', 'text/html');
234+
res.end(`<iframe src='${httpsServer.CROSS_PROCESS_PREFIX}/frame-read-cookie.html'></iframe>`);
235+
});
236+
237+
await page.goto(httpsServer.PREFIX + '/top-frame-set-cookie.html');
238+
239+
async function checkCookies(page: Page) {
240+
await page.goto(httpsServer.PREFIX + '/top-frame-read-cookie.html');
241+
const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body');
242+
await expect.soft(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value');
243+
}
244+
245+
await checkCookies(page);
246+
247+
await test.step('export via cookies/addCookies', async () => {
248+
const cookies = await page.context().cookies();
249+
const context2 = await browser.newContext();
250+
await context2.addCookies(cookies);
251+
const page2 = await context2.newPage();
252+
await checkCookies(page2);
253+
});
254+
255+
await test.step('export via storageState', async () => {
256+
const storageState = await page.context().storageState();
257+
const context3 = await browser.newContext({ storageState });
258+
const page3 = await context3.newPage();
259+
await checkCookies(page3);
260+
});
106261
});
107262

108263
test('should be able to send third party cookies via an iframe', async ({ browser, httpsServer, browserName, isMac }) => {

0 commit comments

Comments
 (0)