From e9f296248a745dd741a68e60268f2f7d18b0b8d3 Mon Sep 17 00:00:00 2001 From: Mike Dean <97438588+mike-dean-talis@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:37:01 +0100 Subject: [PATCH] feat(TA-1034): add partitioned cookie configuration (#94) ## Problem * [TA-1034](https://techfromsage.atlassian.net/browse/TA-1034) On analysing Elevate's use of LTI.js, we identified that it requires third party cookies. The library has not yet been updated to change this particular feature. In order for LTI launches to continue to work in Elevate (at least for Chrome-based browsers), we require a solution that allows these cookies to be processed in a PKCE workflow. The current solution to this issue is [partioned cookies](https://developer.mozilla.org/en-US/docs/Web/Privacy/Privacy_sandbox/Partitioned_cookies). Elevate cannot support this at an LTI.js level, nor at the Express.js level so we must resort to a supporting configuration in nginx. This new property will load a supporting nginx configuration. [TA-1034]: https://techfromsage.atlassian.net/browse/TA-1034?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../__snapshots__/chart.test.ts.snap | 134 ++++++----- lib/web-service/nginx-util.ts | 68 +++++- lib/web-service/nginx/default.conf | 13 +- .../__snapshots__/nginx-util.test.ts.snap | 215 ++++++++++++++++-- test/web-service/nginx-util.test.ts | 22 ++ 5 files changed, 362 insertions(+), 90 deletions(-) diff --git a/examples/advanced-web-service/__snapshots__/chart.test.ts.snap b/examples/advanced-web-service/__snapshots__/chart.test.ts.snap index f239eda..fb5ae7b 100644 --- a/examples/advanced-web-service/__snapshots__/chart.test.ts.snap +++ b/examples/advanced-web-service/__snapshots__/chart.test.ts.snap @@ -82,15 +82,17 @@ server { gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; location / { - proxy_pass http://application; - proxy_http_version 1.1; - - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } location /livez { access_log off; @@ -106,6 +108,8 @@ server { allow 172.16.0.0/12; deny all; } + + } ", "samesite.conf": "# Implements SameSite cookie flags to ensure that our Login Server cookies are flagged as \`SameSite=None\` and \`Secure\`. @@ -185,7 +189,7 @@ proxy_cookie_path / "/$cookie_path_patches"; "region": "local", "service": "advanced-development-local", }, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", "namespace": "advanced-test", }, }, @@ -617,7 +621,7 @@ proxy_cookie_path / "/$cookie_path_patches"; { "configMap": { "defaultMode": 292, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", }, "name": "nginx-config", }, @@ -711,15 +715,17 @@ server { gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; location / { - proxy_pass http://application; - proxy_http_version 1.1; - - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } location /livez { access_log off; @@ -735,6 +741,8 @@ server { allow 172.16.0.0/12; deny all; } + + } ", "samesite.conf": "# Implements SameSite cookie flags to ensure that our Login Server cookies are flagged as \`SameSite=None\` and \`Secure\`. @@ -814,7 +822,7 @@ proxy_cookie_path / "/$cookie_path_patches"; "region": "local", "service": "advanced-development-local", }, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", "namespace": "advanced-test", }, }, @@ -1103,7 +1111,7 @@ proxy_cookie_path / "/$cookie_path_patches"; { "configMap": { "defaultMode": 292, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", }, "name": "nginx-config", }, @@ -1466,7 +1474,7 @@ proxy_cookie_path / "/$cookie_path_patches"; { "configMap": { "defaultMode": 292, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", }, "name": "nginx-config", }, @@ -1560,15 +1568,17 @@ server { gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; location / { - proxy_pass http://application; - proxy_http_version 1.1; - - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } location /livez { access_log off; @@ -1584,6 +1594,8 @@ server { allow 172.16.0.0/12; deny all; } + + } ", "samesite.conf": "# Implements SameSite cookie flags to ensure that our Login Server cookies are flagged as \`SameSite=None\` and \`Secure\`. @@ -1663,7 +1675,7 @@ proxy_cookie_path / "/$cookie_path_patches"; "region": "local", "service": "advanced-development-local", }, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", "namespace": "advanced-test", }, }, @@ -2095,7 +2107,7 @@ proxy_cookie_path / "/$cookie_path_patches"; { "configMap": { "defaultMode": 292, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", }, "name": "nginx-config", }, @@ -2189,15 +2201,17 @@ server { gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; location / { - proxy_pass http://application; - proxy_http_version 1.1; - - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } location /livez { access_log off; @@ -2213,6 +2227,8 @@ server { allow 172.16.0.0/12; deny all; } + + } ", "samesite.conf": "# Implements SameSite cookie flags to ensure that our Login Server cookies are flagged as \`SameSite=None\` and \`Secure\`. @@ -2292,7 +2308,7 @@ proxy_cookie_path / "/$cookie_path_patches"; "region": "local", "service": "advanced-development-local", }, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", "namespace": "advanced-test", }, }, @@ -2580,7 +2596,7 @@ proxy_cookie_path / "/$cookie_path_patches"; { "configMap": { "defaultMode": 292, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", }, "name": "nginx-config", }, @@ -2943,7 +2959,7 @@ proxy_cookie_path / "/$cookie_path_patches"; { "configMap": { "defaultMode": 292, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", }, "name": "nginx-config", }, @@ -3037,15 +3053,17 @@ server { gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; location / { - proxy_pass http://application; - proxy_http_version 1.1; - - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } location /livez { access_log off; @@ -3061,6 +3079,8 @@ server { allow 172.16.0.0/12; deny all; } + + } ", "samesite.conf": "# Implements SameSite cookie flags to ensure that our Login Server cookies are flagged as \`SameSite=None\` and \`Secure\`. @@ -3140,7 +3160,7 @@ proxy_cookie_path / "/$cookie_path_patches"; "region": "local", "service": "advanced-development-local", }, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", "namespace": "advanced-test", }, }, @@ -3571,7 +3591,7 @@ proxy_cookie_path / "/$cookie_path_patches"; { "configMap": { "defaultMode": 292, - "name": "nginx-config-82htd5f6t7", + "name": "nginx-config-g87b2gc9mg", }, "name": "nginx-config", }, diff --git a/lib/web-service/nginx-util.ts b/lib/web-service/nginx-util.ts index f43b93f..9833c45 100644 --- a/lib/web-service/nginx-util.ts +++ b/lib/web-service/nginx-util.ts @@ -29,6 +29,15 @@ interface NginxConfigMapProps { * @default false */ readonly includeSameSiteCookiesConfig?: boolean; + + /** + * Whether to include a config that patches Set-Cookies header to include `Partitioned` + * For further details on partitioned cookies visit: + * + * https://developer.mozilla.org/en-US/docs/Web/Privacy/Privacy_sandbox/Partitioned_cookies + * @default undefined + */ + readonly usePartionedCookiesLocations?: string[]; } /** @@ -39,7 +48,11 @@ function createConfigMap( props: NginxConfigMapProps, data: { [key: string]: string } = {}, ): ConfigMap { - if (props.includeDefaultConfig) { + const usePartitionedCookies = + props.usePartionedCookiesLocations && + props.usePartionedCookiesLocations.length > 0; + + if (props.includeDefaultConfig || usePartitionedCookies) { data["default.conf"] = getDefaultConfig(props); } @@ -60,7 +73,10 @@ function createConfigMap( * The output of this function is used with `createConfigMap` with `includeDefaultConfig` enabled. */ function getDefaultConfig( - props: Pick, + props: Pick< + NginxConfigMapProps, + "applicationPort" | "nginxPort" | "usePartionedCookiesLocations" + >, ): string { const { applicationPort, nginxPort } = props; @@ -72,10 +88,21 @@ function getDefaultConfig( throw new Error("Application and nginx ports must be different"); } + const defaultRouteLocation = createProxyRouteConfig( + "/", + "http://application", + ); + + const partitionedCookieLocations = getPartitionedCookiesConfig( + props.usePartionedCookiesLocations, + ); + return fs .readFileSync(resolvePath("nginx/default.conf"), "utf8") .replaceAll("{{applicationPort}}", applicationPort.toString()) - .replaceAll("{{nginxPort}}", nginxPort.toString()); + .replaceAll("{{nginxPort}}", nginxPort.toString()) + .replaceAll("{{defaultRouteLocation}}", defaultRouteLocation) + .replaceAll("{{partitionedCookieLocations}}", partitionedCookieLocations); } /** @@ -88,6 +115,41 @@ function getSameSiteCookiesConfig(): string { return fs.readFileSync(resolvePath("nginx/samesite.conf"), "utf8"); } +function getPartitionedCookiesConfig(locations?: string[]): string { + if (!locations) { + return ""; + } + + return locations + .map((location) => + createProxyRouteConfig(location, "http://application", [ + `proxy_cookie_path / "/; Partitioned";`, + ]), + ) + .join("\n\n"); +} + +function createProxyRouteConfig( + location: string, + proxyPath: string, + additionalSettings?: string[], +): string { + const additional = additionalSettings ? additionalSettings.join("\n") : ""; + + return `location ${location} { + proxy_pass ${proxyPath}; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + ${additional} + }`; +} + export const nginxUtil = { createConfigMap, getDefaultConfig, diff --git a/lib/web-service/nginx/default.conf b/lib/web-service/nginx/default.conf index a3a891e..56d37f0 100644 --- a/lib/web-service/nginx/default.conf +++ b/lib/web-service/nginx/default.conf @@ -16,16 +16,7 @@ server { gzip_comp_level 4; gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; - location / { - proxy_pass http://application; - proxy_http_version 1.1; - - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } + {{defaultRouteLocation}} location /livez { access_log off; @@ -41,4 +32,6 @@ server { allow 172.16.0.0/12; deny all; } + + {{partitionedCookieLocations}} } diff --git a/test/web-service/__snapshots__/nginx-util.test.ts.snap b/test/web-service/__snapshots__/nginx-util.test.ts.snap index 738ade4..ba170e4 100644 --- a/test/web-service/__snapshots__/nginx-util.test.ts.snap +++ b/test/web-service/__snapshots__/nginx-util.test.ts.snap @@ -45,15 +45,17 @@ server { gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; location / { - proxy_pass http://application; - proxy_http_version 1.1; - - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } location /livez { access_log off; @@ -69,6 +71,8 @@ server { allow 172.16.0.0/12; deny all; } + + } ", }, @@ -77,7 +81,7 @@ server { "labels": { "prunable": "true", }, - "name": "test-nginx-config-c88fe926-tbmmh76262", + "name": "test-nginx-config-c88fe926-9d5ctddt9h", }, }, ] @@ -107,15 +111,17 @@ server { gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; location / { - proxy_pass http://application; - proxy_http_version 1.1; - - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } location /livez { access_log off; @@ -131,6 +137,8 @@ server { allow 172.16.0.0/12; deny all; } + + } ", }, @@ -139,7 +147,7 @@ server { "labels": { "prunable": "true", }, - "name": "test-nginx-config-c88fe926-6992742cdt", + "name": "test-nginx-config-c88fe926-7b7f6cfmf4", }, }, ] @@ -164,6 +172,173 @@ exports[`nginx-util > createConfigMap > Empty 1`] = ` ] `; +exports[`nginx-util > createConfigMap > Partitioned cookies config 1`] = ` +[ + { + "apiVersion": "v1", + "data": { + "default.conf": "map $http_upgrade $connection_upgrade { + default "upgrade"; + "" ""; +} + +upstream application { + server localhost:8080; + keepalive 256; +} + +server { + listen 80; + server_name localhost; + + gzip on; + gzip_comp_level 4; + gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } + + location /livez { + access_log off; + add_header Content-Type text/plain; + return 200 'OK'; + } + + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + allow 10.0.0.0/8; + allow 172.16.0.0/12; + deny all; + } + + location /api/oidclogin { + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_cookie_path / "/; Partitioned"; + } +} +", + }, + "kind": "ConfigMap", + "metadata": { + "labels": { + "prunable": "true", + }, + "name": "test-nginx-config-c88fe926-99528m62bg", + }, + }, +] +`; + +exports[`nginx-util > createConfigMap > Partitioned cookies config with multiple locations 1`] = ` +[ + { + "apiVersion": "v1", + "data": { + "default.conf": "map $http_upgrade $connection_upgrade { + default "upgrade"; + "" ""; +} + +upstream application { + server localhost:8080; + keepalive 256; +} + +server { + listen 80; + server_name localhost; + + gzip on; + gzip_comp_level 4; + gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } + + location /livez { + access_log off; + add_header Content-Type text/plain; + return 200 'OK'; + } + + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + allow 10.0.0.0/8; + allow 172.16.0.0/12; + deny all; + } + + location /api/oidclogin { + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_cookie_path / "/; Partitioned"; + } + +location /api/auth/login { + proxy_pass http://application; + proxy_http_version 1.1; + + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_cookie_path / "/; Partitioned"; + } +} +", + }, + "kind": "ConfigMap", + "metadata": { + "labels": { + "prunable": "true", + }, + "name": "test-nginx-config-c88fe926-mdm92m6fhh", + }, + }, +] +`; + exports[`nginx-util > createConfigMap > Same site cookies config 1`] = ` [ { diff --git a/test/web-service/nginx-util.test.ts b/test/web-service/nginx-util.test.ts index 6ce65bf..b71a369 100644 --- a/test/web-service/nginx-util.test.ts +++ b/test/web-service/nginx-util.test.ts @@ -95,5 +95,27 @@ describe("nginx-util", () => { const results = Testing.synth(chart); expect(results).toMatchSnapshot(); }); + + test("Partitioned cookies config", () => { + const chart = Testing.chart(); + nginxUtil.createConfigMap(chart, { + applicationPort: 8080, + nginxPort: 80, + usePartionedCookiesLocations: ["/api/oidclogin"], + }); + const results = Testing.synth(chart); + expect(results).toMatchSnapshot(); + }); + + test("Partitioned cookies config with multiple locations", () => { + const chart = Testing.chart(); + nginxUtil.createConfigMap(chart, { + applicationPort: 8080, + nginxPort: 80, + usePartionedCookiesLocations: ["/api/oidclogin", "/api/auth/login"], + }); + const results = Testing.synth(chart); + expect(results).toMatchSnapshot(); + }); }); });