From 8fac4a48cb012f277e321e03a7105a41fc6eabfd Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Fri, 20 Feb 2026 08:06:45 -0800 Subject: [PATCH 01/10] Extract env-configurable cookies from /secrets calls and pass into the final action secrets. --- action-server/src/app.ts | 5 +++++ action-server/src/utils/auth.ts | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/action-server/src/app.ts b/action-server/src/app.ts index dc55612ea6..643af8b24b 100644 --- a/action-server/src/app.ts +++ b/action-server/src/app.ts @@ -1,6 +1,7 @@ import express, { Response } from "express"; import {authMiddleware, corsMiddleware, jsonErrorMiddleware} from "./middleware"; import { ActionRunner } from "./type/actionRunner"; +import { extractCookies } from "./utils/auth"; // init express app and middleware @@ -24,8 +25,12 @@ app.post( const { action_run_id, secrets } = req.body; const actionRunId = action_run_id as string; + const cookieNames = (process.env.ACTION_COOKIE_NAMES ?? '').split(',').map(s => s.trim()).filter(Boolean); + const forwardedCookies = extractCookies(req.headers.cookie ?? '', cookieNames); + const fullSecrets = { ...secrets, + ...forwardedCookies, authorization: res.locals.authorization, user: JSON.stringify(res.locals.user), userRole: res.locals.userRole diff --git a/action-server/src/utils/auth.ts b/action-server/src/utils/auth.ts index 06f8e86104..3006094337 100644 --- a/action-server/src/utils/auth.ts +++ b/action-server/src/utils/auth.ts @@ -63,6 +63,25 @@ export type UserRoles = { }; +/** + * Extracts specified cookies from a cookie header string. + * Returns a record of cookie name to cookie value for matching cookies found in the header. + */ +export function extractCookies(cookieHeader: string, cookieNames: string[]): Record { + const cookies: Record = {}; + + if (cookieNames.length > 0) { + for (const pair of cookieHeader.split(';')) { + const [name, ...rest] = pair.trim().split('='); + if (cookieNames.includes(name)) { + cookies[name] = rest.join('='); + } + } + } + + return cookies; +} + export function authorizationHeaderToToken(authorizationHeader: string | undefined | null): JsonWebToken | never { if (authorizationHeader !== null && authorizationHeader !== undefined) { if (authorizationHeader.startsWith('Bearer ')) { From 8356add4c100cc33aa2b480242c54ebd0ef30817 Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Fri, 20 Feb 2026 08:07:35 -0800 Subject: [PATCH 02/10] Optionally enforce Access-Control-Allow-Origin on env-configurable origin. Falls back to reflecting request origin and then falls back to *. --- action-server/src/middleware.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/action-server/src/middleware.ts b/action-server/src/middleware.ts index c2d1744656..e768a21ca9 100644 --- a/action-server/src/middleware.ts +++ b/action-server/src/middleware.ts @@ -12,10 +12,10 @@ export const jsonErrorMiddleware: ErrorRequestHandler = (err, req, res, next) => }); }; -// temporary CORS middleware to allow access from all origins -// TODO: set more strict CORS rules export const corsMiddleware: RequestHandler = (req, res, next) => { - res.setHeader("Access-Control-Allow-Origin", "*"); + const allowedOrigin = process.env.ACTION_CORS_ALLOWED_ORIGIN || req.headers.origin || "*"; + res.setHeader("Access-Control-Allow-Origin", allowedOrigin); + res.setHeader("Access-Control-Allow-Credentials", "true"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, authorization, x-hasura-role"); next(); From b9887d602c118fabc479f490359ff13c15f0b866 Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Fri, 20 Feb 2026 08:07:55 -0800 Subject: [PATCH 03/10] Add new env vars to main docker-compose --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 7c3d33171e..1b90a9effe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: MERLIN_GRAPHQL_URL: http://hasura:8080/v1/graphql AERIE_DB_HOST: postgres AERIE_DB_PORT: 5432 + ACTION_COOKIE_NAMES: "${ACTION_COOKIE_NAMES}" + ACTION_CORS_ALLOWED_ORIGIN: "${ACTION_CORS_ALLOWED_ORIGIN}" ACTION_DB_USER: "${SEQUENCING_USERNAME}" ACTION_DB_PASSWORD: "${SEQUENCING_PASSWORD}" ACTION_LOCAL_STORE: /usr/src/app/action_file_store From e6faacd6dca297f8c46154957a18907981ea4cc7 Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Fri, 20 Feb 2026 08:08:02 -0800 Subject: [PATCH 04/10] Add action server test for cookie handling --- action-server/tests/app.test.mts | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/action-server/tests/app.test.mts b/action-server/tests/app.test.mts index 14857cc0ff..34ec04d0bb 100644 --- a/action-server/tests/app.test.mts +++ b/action-server/tests/app.test.mts @@ -70,3 +70,55 @@ test('auth middleware', async () => { assert.equal(res.status, 401); }); }); + +test('cookie forwarding', async () => { + const validToken = jwt.sign({ sub: 'user-123' }, key, { algorithm: 'HS256', expiresIn: '1h' }); + + await test('should forward configured cookies as secrets', async () => { + process.env.ACTION_COOKIE_NAMES = 'ssosession,other_cookie'; + called.length = 0; // reset array, can't re-assign since our mock has a static reference to this array + + const res = await request(app) + .post('/secrets') + .send({ action_run_id: 'test-run-1', secrets: { mySecret: 'value' } }) + .set('Authorization', `Bearer ${validToken}`) + .set('Cookie', 'ssosession=token123; other_cookie=abc; unrelated=xyz'); + + assert.equal(res.status, 200); + assert.equal(called.length, 1); + assert.equal(called[0].secrets.ssosession, 'token123'); + assert.equal(called[0].secrets.other_cookie, 'abc'); + assert.equal(called[0].secrets.unrelated, undefined); + assert.equal(called[0].secrets.mySecret, 'value'); + }); + + await test('should not forward cookies when ACTION_COOKIE_NAMES is unset', async () => { + delete process.env.ACTION_COOKIE_NAMES; + called.length = 0; + + const res = await request(app) + .post('/secrets') + .send({ action_run_id: 'test-run-2', secrets: {} }) + .set('Authorization', `Bearer ${validToken}`) + .set('Cookie', 'ssosession=token123'); + + assert.equal(res.status, 200); + assert.equal(called.length, 1); + assert.equal(called[0].secrets.ssosession, undefined); + }); + + await test('should handle cookies with equals signs in values', async () => { + process.env.ACTION_COOKIE_NAMES = 'ssosession'; + called.length = 0; + + const res = await request(app) + .post('/secrets') + .send({ action_run_id: 'test-run-3', secrets: {} }) + .set('Authorization', `Bearer ${validToken}`) + .set('Cookie', 'ssosession=base64value==;'); + + assert.equal(res.status, 200); + assert.equal(called.length, 1); + assert.equal(called[0].secrets.ssosession, 'base64value=='); + }); +}); From 2b8169943d52efe7f84708ddb41c87c894090dfa Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Fri, 20 Feb 2026 08:17:19 -0800 Subject: [PATCH 05/10] Use action-server configuration system instead of reading directly from process.env --- action-server/src/app.ts | 7 ++++--- action-server/src/config.ts | 6 ++++-- action-server/src/middleware.ts | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/action-server/src/app.ts b/action-server/src/app.ts index 643af8b24b..604921d247 100644 --- a/action-server/src/app.ts +++ b/action-server/src/app.ts @@ -1,4 +1,5 @@ -import express, { Response } from "express"; +import express from "express"; +import { configuration } from "./config"; import {authMiddleware, corsMiddleware, jsonErrorMiddleware} from "./middleware"; import { ActionRunner } from "./type/actionRunner"; import { extractCookies } from "./utils/auth"; @@ -25,8 +26,8 @@ app.post( const { action_run_id, secrets } = req.body; const actionRunId = action_run_id as string; - const cookieNames = (process.env.ACTION_COOKIE_NAMES ?? '').split(',').map(s => s.trim()).filter(Boolean); - const forwardedCookies = extractCookies(req.headers.cookie ?? '', cookieNames); + const { ACTION_COOKIE_NAMES } = configuration(); + const forwardedCookies = extractCookies(req.headers.cookie ?? '', ACTION_COOKIE_NAMES); const fullSecrets = { ...secrets, diff --git a/action-server/src/config.ts b/action-server/src/config.ts index 13c75da974..ef74dfeb5a 100644 --- a/action-server/src/config.ts +++ b/action-server/src/config.ts @@ -1,9 +1,9 @@ -import {Algorithm} from "jsonwebtoken"; - export interface Config { AERIE_DB: string; AERIE_DB_HOST: string; AERIE_DB_PORT: string; + ACTION_COOKIE_NAMES: string[]; + ACTION_CORS_ALLOWED_ORIGIN: string; ACTION_DB_USER: string; ACTION_DB_PASSWORD: string; ACTION_LOCAL_STORE: string; @@ -26,6 +26,8 @@ export const configuration = (): Config => { AERIE_DB: env.AERIE_DB ?? "aerie", AERIE_DB_HOST: env.AERIE_DB_HOST ?? "postgres", AERIE_DB_PORT: env.AERIE_DB_PORT ?? "5432", + ACTION_COOKIE_NAMES: (env.ACTION_COOKIE_NAMES ?? '').split(',').map(s => s.trim()).filter(Boolean), + ACTION_CORS_ALLOWED_ORIGIN: env.ACTION_CORS_ALLOWED_ORIGIN ?? "", ACTION_DB_USER: env.ACTION_DB_USER ?? "", ACTION_DB_PASSWORD: env.ACTION_DB_PASSWORD ?? "", ACTION_LOCAL_STORE: env.ACTION_LOCAL_STORE ?? "/usr/src/app/action_file_store", diff --git a/action-server/src/middleware.ts b/action-server/src/middleware.ts index e768a21ca9..06bb17671e 100644 --- a/action-server/src/middleware.ts +++ b/action-server/src/middleware.ts @@ -1,4 +1,5 @@ import { ErrorRequestHandler, RequestHandler, NextFunction, Request, Response } from "express"; +import { configuration } from "./config"; import {decodeJwt} from "./utils/auth"; // custom error handling middleware so we always return a JSON object for errors @@ -13,7 +14,8 @@ export const jsonErrorMiddleware: ErrorRequestHandler = (err, req, res, next) => }; export const corsMiddleware: RequestHandler = (req, res, next) => { - const allowedOrigin = process.env.ACTION_CORS_ALLOWED_ORIGIN || req.headers.origin || "*"; + const { ACTION_CORS_ALLOWED_ORIGIN } = configuration(); + const allowedOrigin = ACTION_CORS_ALLOWED_ORIGIN || req.headers.origin || "*"; res.setHeader("Access-Control-Allow-Origin", allowedOrigin); res.setHeader("Access-Control-Allow-Credentials", "true"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); From e1357b8e6a2ad28eaa0017d95432f4beb7d4ca47 Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Fri, 20 Feb 2026 08:22:05 -0800 Subject: [PATCH 06/10] Env/configuration file updates --- .env.template | 6 ++++++ deployment/.env | 8 +++++++- deployment/Environment.md | 17 +++++++++++++++++ deployment/docker-compose.yml | 2 ++ .../kubernetes/aerie-action-deployment.yaml | 12 ++++++++++++ e2e-tests/docker-compose-many-workers.yml | 2 ++ e2e-tests/docker-compose-test.yml | 2 ++ 7 files changed, 48 insertions(+), 1 deletion(-) diff --git a/.env.template b/.env.template index 3909a2f377..8c5a4b88fd 100644 --- a/.env.template +++ b/.env.template @@ -18,3 +18,9 @@ POSTGRES_PASSWORD= HASURA_GRAPHQL_ADMIN_SECRET= HASURA_GRAPHQL_JWT_SECRET= + +# Optional: comma-separated list of browser cookie names to forward to actions as secrets (e.g. ssosession) +# ACTION_COOKIE_NAMES= + +# Optional: allowed CORS origin for the action server. If unset, reflects the request's Origin header +# ACTION_CORS_ALLOWED_ORIGIN= diff --git a/deployment/.env b/deployment/.env index b61f52bc10..2a535b8c48 100644 --- a/deployment/.env +++ b/deployment/.env @@ -24,6 +24,12 @@ HASURA_GRAPHQL_JWT_SECRET= POSTGRES_USER= POSTGRES_PASSWORD= -# Optionally define the host PlanDev will run on, if you need HTTPS / TLS +# Optional: defines the host PlanDev will run on, if you need HTTPS / TLS # See the deployment/proxy folder for more info # AERIE_HOST= + +# Optional: comma-separated list of browser cookie names to forward to actions as secrets (e.g. ssosession) +# ACTION_COOKIE_NAMES= + +# Optional: allowed CORS origin for the action server. If unset, reflects the request's Origin header +# ACTION_CORS_ALLOWED_ORIGIN= diff --git a/deployment/Environment.md b/deployment/Environment.md index 994e71938c..fa49ab7a06 100644 --- a/deployment/Environment.md +++ b/deployment/Environment.md @@ -6,6 +6,7 @@ This document provides detailed information about environment variables for each - [PlanDev Merlin](#plandev-merlin) - [PlanDev Scheduler](#plandev-scheduler) - [PlanDev Sequencing](#plandev-sequencing) +- [PlanDev Action Server](#plandev-action-server) - [PlanDev UI](#plandev-ui) - [Hasura](#hasura) - [Postgres](#postgres) @@ -86,6 +87,22 @@ See the [environment variables document](https://github.com/NASA-AMMOS/aerie-gat | `SEQUENCING_SERVER_PORT` | Port the server listens on | `number` | 27184 | | `SEQUENCING_LANGUAGE` | The language that sequences are generated in when using templates ("SEQN", "STOL", or "TEXT") | `string` | "SEQN" | +## PlanDev Action Server + +| Name | Description | Type | Default | +|-------------------------------|-------------------------------------------------------------------------------------------------------------------|----------|---------| +| `ACTION_COOKIE_NAMES` | Comma-separated list of browser cookie names to extract and forward to actions as secrets (e.g. `ssosession`) | `string` | | +| `ACTION_CORS_ALLOWED_ORIGIN` | Allowed CORS origin for the action server. If unset, reflects the request's `Origin` header | `string` | | +| `ACTION_DB_USER` | Username of the Action Server DB User | `string` | | +| `ACTION_DB_PASSWORD` | Password of the Action Server DB User | `string` | | +| `ACTION_LOCAL_STORE` | Local storage for the action server in the container | `string` | /usr/src/app/action_file_store | +| `ACTION_WORKER_NUM` | Number of action worker threads | `number` | 1 | +| `ACTION_MAX_WORKER_NUM` | Maximum number of action worker threads | `number` | 1 | +| `HASURA_GRAPHQL_JWT_SECRET` | The JWT secret for JSON web token auth | `string` | | +| `LOG_FILE` | Either an output filepath to log to, or 'console' | `string` | console | +| `LOG_LEVEL` | Logging level for filtering logs | `string` | debug | +| `MERLIN_GRAPHQL_URL` | URI of the PlanDev GraphQL API | `string` | http://hasura:8080/v1/graphql | + ## PlanDev UI See the [environment variables document](https://github.com/NASA-AMMOS/aerie-ui/blob/develop/docs/ENVIRONMENT.md) in the PlanDev UI repository. diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 4a5e49e822..c1a05eea13 100755 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -9,6 +9,8 @@ services: MERLIN_GRAPHQL_URL: http://hasura:8080/v1/graphql AERIE_DB_HOST: postgres AERIE_DB_PORT: 5432 + ACTION_COOKIE_NAMES: "${ACTION_COOKIE_NAMES}" + ACTION_CORS_ALLOWED_ORIGIN: "${ACTION_CORS_ALLOWED_ORIGIN}" ACTION_DB_USER: "${SEQUENCING_USERNAME}" ACTION_DB_PASSWORD: "${SEQUENCING_PASSWORD}" ACTION_LOCAL_STORE: /usr/src/app/action_file_store diff --git a/deployment/kubernetes/aerie-action-deployment.yaml b/deployment/kubernetes/aerie-action-deployment.yaml index 1e789755fb..ef26375b99 100644 --- a/deployment/kubernetes/aerie-action-deployment.yaml +++ b/deployment/kubernetes/aerie-action-deployment.yaml @@ -38,6 +38,18 @@ spec: value: "5432" - name: AERIE_DB_HOST value: postgres + - name: ACTION_COOKIE_NAMES + valueFrom: + secretKeyRef: + name: dev-env + key: ACTION_COOKIE_NAMES + optional: true + - name: ACTION_CORS_ALLOWED_ORIGIN + valueFrom: + secretKeyRef: + name: dev-env + key: ACTION_CORS_ALLOWED_ORIGIN + optional: true - name: ACTION_LOCAL_STORE value: /usr/src/app/action_file_store - name: ACTION_DB_USER diff --git a/e2e-tests/docker-compose-many-workers.yml b/e2e-tests/docker-compose-many-workers.yml index de98f4aad8..7aaca095e2 100644 --- a/e2e-tests/docker-compose-many-workers.yml +++ b/e2e-tests/docker-compose-many-workers.yml @@ -13,6 +13,8 @@ services: ACTION_SERVER_PORT: 27186 AERIE_DB_HOST: postgres AERIE_DB_PORT: 5432 + ACTION_COOKIE_NAMES: "${ACTION_COOKIE_NAMES}" + ACTION_CORS_ALLOWED_ORIGIN: "${ACTION_CORS_ALLOWED_ORIGIN}" ACTION_DB_USER: "${SEQUENCING_USERNAME}" ACTION_DB_PASSWORD: "${SEQUENCING_PASSWORD}" ACTION_LOCAL_STORE: /usr/src/app/action_file_store diff --git a/e2e-tests/docker-compose-test.yml b/e2e-tests/docker-compose-test.yml index a4ebee6e45..dcc9dd06dd 100644 --- a/e2e-tests/docker-compose-test.yml +++ b/e2e-tests/docker-compose-test.yml @@ -16,6 +16,8 @@ services: MERLIN_GRAPHQL_URL: http://hasura:8080/v1/graphql AERIE_DB_HOST: postgres AERIE_DB_PORT: 5432 + ACTION_COOKIE_NAMES: "${ACTION_COOKIE_NAMES}" + ACTION_CORS_ALLOWED_ORIGIN: "${ACTION_CORS_ALLOWED_ORIGIN}" ACTION_DB_USER: "${SEQUENCING_USERNAME}" ACTION_DB_PASSWORD: "${SEQUENCING_PASSWORD}" ACTION_LOCAL_STORE: /usr/src/app/action_file_store From f038d2db6e4c5190c1f0ddaa5278e7c900747128 Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Fri, 20 Feb 2026 09:11:16 -0800 Subject: [PATCH 07/10] Only set `Access-Control-Allow-Credentials` to true when `ACTION_CORS_ALLOWED_ORIGIN` env is configured to prevent any origin to make credentialed requests. --- action-server/src/middleware.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/action-server/src/middleware.ts b/action-server/src/middleware.ts index 06bb17671e..44674cad0c 100644 --- a/action-server/src/middleware.ts +++ b/action-server/src/middleware.ts @@ -15,9 +15,15 @@ export const jsonErrorMiddleware: ErrorRequestHandler = (err, req, res, next) => export const corsMiddleware: RequestHandler = (req, res, next) => { const { ACTION_CORS_ALLOWED_ORIGIN } = configuration(); - const allowedOrigin = ACTION_CORS_ALLOWED_ORIGIN || req.headers.origin || "*"; - res.setHeader("Access-Control-Allow-Origin", allowedOrigin); - res.setHeader("Access-Control-Allow-Credentials", "true"); + + if (ACTION_CORS_ALLOWED_ORIGIN) { + // Explicit origin configured: strict CORS with credentials support + res.setHeader("Access-Control-Allow-Origin", ACTION_CORS_ALLOWED_ORIGIN); + res.setHeader("Access-Control-Allow-Credentials", "true"); + } else { + // No origin configured: reflect request origin for compatibility, but without credentials + res.setHeader("Access-Control-Allow-Origin", req.headers.origin || "*"); + } res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, authorization, x-hasura-role"); next(); From 3cb7fb819f082ea4b3a983e9cdf3329108033082 Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Mon, 23 Feb 2026 12:45:39 -0800 Subject: [PATCH 08/10] Use * instead of reflecting for simplicity when no origin configured --- action-server/src/middleware.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action-server/src/middleware.ts b/action-server/src/middleware.ts index 44674cad0c..be01b72a09 100644 --- a/action-server/src/middleware.ts +++ b/action-server/src/middleware.ts @@ -21,8 +21,8 @@ export const corsMiddleware: RequestHandler = (req, res, next) => { res.setHeader("Access-Control-Allow-Origin", ACTION_CORS_ALLOWED_ORIGIN); res.setHeader("Access-Control-Allow-Credentials", "true"); } else { - // No origin configured: reflect request origin for compatibility, but without credentials - res.setHeader("Access-Control-Allow-Origin", req.headers.origin || "*"); + // No origin configured: allow access from all origins but without credentials + res.setHeader("Access-Control-Allow-Origin", "*"); } res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, authorization, x-hasura-role"); From b8f7fd5cb3878402715530d53818d35bd4aa7db1 Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Tue, 24 Feb 2026 06:21:10 -0800 Subject: [PATCH 09/10] Updates to reflect new UI env var `PUBLIC_ACTION_INCLUDE_CREDENTIALS` --- .env.template | 2 +- deployment/.env | 2 +- deployment/docker-compose.yml | 1 + deployment/kubernetes/aerie-ui-deployment.yaml | 6 ++++++ docker-compose.yml | 1 + e2e-tests/docker-compose-many-workers.yml | 1 + e2e-tests/docker-compose-test.yml | 1 + 7 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index 8c5a4b88fd..b5c13c9cca 100644 --- a/.env.template +++ b/.env.template @@ -22,5 +22,5 @@ HASURA_GRAPHQL_JWT_SECRET= # Optional: comma-separated list of browser cookie names to forward to actions as secrets (e.g. ssosession) # ACTION_COOKIE_NAMES= -# Optional: allowed CORS origin for the action server. If unset, reflects the request's Origin header +# Optional: allowed CORS origin for the action server. Required for cross-origin deployments with cookie forwarding. # ACTION_CORS_ALLOWED_ORIGIN= diff --git a/deployment/.env b/deployment/.env index 2a535b8c48..086b585839 100644 --- a/deployment/.env +++ b/deployment/.env @@ -31,5 +31,5 @@ POSTGRES_PASSWORD= # Optional: comma-separated list of browser cookie names to forward to actions as secrets (e.g. ssosession) # ACTION_COOKIE_NAMES= -# Optional: allowed CORS origin for the action server. If unset, reflects the request's Origin header +# Optional: allowed CORS origin for the action server. Required for cross-origin deployments with cookie forwarding. # ACTION_CORS_ALLOWED_ORIGIN= diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index c1a05eea13..2c056dbb1f 100755 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -192,6 +192,7 @@ services: ORIGIN: http://localhost PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/" PUBLIC_ACTION_CLIENT_URL: http://localhost:27186 + PUBLIC_ACTION_INCLUDE_CREDENTIALS: "false" PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000 PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000 PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql diff --git a/deployment/kubernetes/aerie-ui-deployment.yaml b/deployment/kubernetes/aerie-ui-deployment.yaml index fbccd8e9bb..5dce77af58 100644 --- a/deployment/kubernetes/aerie-ui-deployment.yaml +++ b/deployment/kubernetes/aerie-ui-deployment.yaml @@ -19,6 +19,12 @@ spec: value: http://127.0.0.1 - name: PUBLIC_AERIE_FILE_STORE_PREFIX value: /usr/src/app/merlin_file_store/ + - name: PUBLIC_ACTION_INCLUDE_CREDENTIALS + valueFrom: + secretKeyRef: + name: dev-env + key: PUBLIC_ACTION_INCLUDE_CREDENTIALS + optional: true - name: PUBLIC_GATEWAY_CLIENT_URL value: http://localhost:9000 - name: PUBLIC_GATEWAY_SERVER_URL diff --git a/docker-compose.yml b/docker-compose.yml index 1b90a9effe..8dd2d2154f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -165,6 +165,7 @@ services: PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/" ORIGIN: http://localhost PUBLIC_ACTION_CLIENT_URL: http://localhost:27186 + PUBLIC_ACTION_INCLUDE_CREDENTIALS: "false" PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000 PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000 PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql diff --git a/e2e-tests/docker-compose-many-workers.yml b/e2e-tests/docker-compose-many-workers.yml index 7aaca095e2..a188439b9c 100644 --- a/e2e-tests/docker-compose-many-workers.yml +++ b/e2e-tests/docker-compose-many-workers.yml @@ -160,6 +160,7 @@ services: PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/" ORIGIN: http://localhost PUBLIC_ACTION_CLIENT_URL: http://localhost:27186 + PUBLIC_ACTION_INCLUDE_CREDENTIALS: "false" PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000 PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000 PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql diff --git a/e2e-tests/docker-compose-test.yml b/e2e-tests/docker-compose-test.yml index dcc9dd06dd..d5c941aef1 100644 --- a/e2e-tests/docker-compose-test.yml +++ b/e2e-tests/docker-compose-test.yml @@ -158,6 +158,7 @@ services: ORIGIN: http://localhost PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/" PUBLIC_ACTION_CLIENT_URL: http://localhost:27186 + PUBLIC_ACTION_INCLUDE_CREDENTIALS: "false" PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000 PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000 PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql From a8673abd9e9fbb80624cc565a38f0703f3de4040 Mon Sep 17 00:00:00 2001 From: dandelany Date: Wed, 25 Feb 2026 10:21:30 -0800 Subject: [PATCH 10/10] update extractCookies to use cookie library parser --- action-server/package-lock.json | 22 ++++++++++++++++++---- action-server/package.json | 1 + action-server/src/utils/auth.ts | 20 ++++++++++---------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/action-server/package-lock.json b/action-server/package-lock.json index 99052143e9..37de1d2a53 100644 --- a/action-server/package-lock.json +++ b/action-server/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@nasa-jpl/aerie-actions": "1.1.1", + "cookie": "^1.1.1", "express": "^5.0.1", "jsonwebtoken": "9.0.2", "pg": "^8.13.3", @@ -3173,12 +3174,16 @@ } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cookie-signature": { @@ -5440,6 +5445,15 @@ "node": ">= 18" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/action-server/package.json b/action-server/package.json index d30f50186f..fd18d5a93e 100644 --- a/action-server/package.json +++ b/action-server/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@nasa-jpl/aerie-actions": "1.1.1", + "cookie": "^1.1.1", "express": "^5.0.1", "jsonwebtoken": "9.0.2", "pg": "^8.13.3", diff --git a/action-server/src/utils/auth.ts b/action-server/src/utils/auth.ts index 3006094337..b724b8cf50 100644 --- a/action-server/src/utils/auth.ts +++ b/action-server/src/utils/auth.ts @@ -1,6 +1,7 @@ import { Request } from 'express'; import { configuration } from "../config"; import jwt, {Algorithm} from "jsonwebtoken"; +import { parseCookie } from "cookie"; export type JsonWebToken = string; @@ -68,18 +69,17 @@ export type UserRoles = { * Returns a record of cookie name to cookie value for matching cookies found in the header. */ export function extractCookies(cookieHeader: string, cookieNames: string[]): Record { - const cookies: Record = {}; - - if (cookieNames.length > 0) { - for (const pair of cookieHeader.split(';')) { - const [name, ...rest] = pair.trim().split('='); - if (cookieNames.includes(name)) { - cookies[name] = rest.join('='); - } + if (!cookieHeader || cookieNames.length === 0) return {}; + + const parsedCookies = parseCookie(cookieHeader); + const selectedCookies: Record = {}; + + for (const name of cookieNames) { + if (parsedCookies[name] !== undefined) { + selectedCookies[name] = parsedCookies[name]; } } - - return cookies; + return selectedCookies; } export function authorizationHeaderToToken(authorizationHeader: string | undefined | null): JsonWebToken | never {