Skip to content

feat(authProviders): add SESSION_COOKIE_DOMAIN for cross-subdomain OIDC - #10929

Open
MichaelUray wants to merge 5 commits into
hcengineering:developfrom
MichaelUray:feat/auth-session-cookie-domain
Open

MichaelUray wants to merge 5 commits into
hcengineering:developfrom
MichaelUray:feat/auth-session-cookie-domain

Conversation

@MichaelUray

@MichaelUray MichaelUray commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds explicit configuration for retaining OIDC session state when a login
starts on one subdomain and returns to another.

  • SESSION_COOKIE_DOMAIN=.example.com scopes the auth session cookie to a
    parent domain.
  • A valid domain-scoped session uses huly.sess (and its signature) to avoid
    collisions with koa-session applications on sibling subdomains. Without
    SESSION_COOKIE_DOMAIN, the existing host-scoped koa.sess key remains.
  • SESSION_COOKIE_SECURE is an independent option for deployments where TLS
    terminates before this plain-HTTP process. It accepts true/false,
    1/0, and yes/no.
  • Neither setting changes SameSite, retaining compatibility with identity
    providers that use response_mode=form_post.
  • Adds operator guidance for domain validation, public suffixes, TLS
    termination, and cookie defaults.

Compatibility

SESSION_COOKIE_DOMAIN changes cookie scope only; it does not assert TLS or
implicitly set Secure or SameSite.

Enabling SESSION_COOKIE_DOMAIN also changes the session key from koa.sess
to huly.sess. There is no dual-read or migration: users with an existing
session must sign in again, and in-flight OIDC logins must restart because the
stored state, nonce, and PKCE values remain in the old cookie. Plan this as a
one-time, short-lived login interruption.

Validation

  • pods/authProviders/node_modules/.bin/format src --force — passed.
  • pods/authProviders jest — 34 tests passed (cookie-domain unit tests and the session-cookie integration suite).

@huly-github-staging

Copy link
Copy Markdown

Connected to Huly®: UBERF-16532

@MichaelUray

Copy link
Copy Markdown
Contributor Author

Pushed a fix for the previous revision (32bde5d), and updated the description.

Problem: passing secure: true to koa-session made /auth/openid answer 500 with Cannot send secure cookie over unencrypted connection behind a TLS-terminating proxy. The account service listens on plain HTTP and app.proxy is off, so Koa sees ctx.request.secure === false and the cookies module refuses to write a Secure cookie.

Fix: when SESSION_COOKIE_DOMAIN is set, a small middleware registered directly before koa-session marks the per-request cookie jar as secure (ctx.cookies.secure = true). No forwarded headers are trusted: app.proxy stays off and ctx.secure, ctx.host and ctx.ip are unchanged. SESSION_COOKIE_SECURE=false opts out for local plain-HTTP development. Without SESSION_COOKIE_DOMAIN nothing changes.

Tests: besides the unit matrix for the option decision, a request-level test serves a minimal Koa app over plain HTTP and checks the Secure cross-subdomain session cookie, the opt-out, the unset case and that an explicit secure: false cookie is left alone. A negative control (secure: true without the marker) reproduces the 500.

@MichaelUray

Copy link
Copy Markdown
Contributor Author

@ArtyomSavchenko gentle ping when you have time: the cookie fix from 2026-10-02 is in, the branch merges cleanly into current develop, and it has been running on a TLS-terminated deployment since then without issues. Happy to adjust anything.

@ArtyomSavchenko

Copy link
Copy Markdown
Member

@MichaelUray Please take a look at review comments:

Main issues

  1. The description is stale. It says only the cookie scope changes and that Secure stays "where the original config has it". The original config is session({}), which has no Secure. The diff forces Secure and SameSite=Lax, adds an undocumented SESSION_COOKIE_SECURE variable, and makes any cookie written without an explicit secure option default to Secure.
  2. Overloaded setting. SESSION_COOKIE_DOMAIN now also means "TLS is terminated in front of this service". An operator who sets it on plain HTTP gets cookies the browser drops and a broken login, with no warning. Document this prominently, or add an explicit setting.
  3. Missing docs. The description promises deployment-doc guidance (the OK / NOT OK domain examples), but no docs are in the diff.
  4. Cookie-name collisions. The session cookie stays koa.sess. Another koa-session app on a sibling subdomain under the same parent domain would overwrite or invalidate it. Set a distinct key (for example huly.sess) only when the domain is set, so existing deployments are unaffected.

Smaller points

  • SESSION_COOKIE_SECURE is only recognised as false. Values like 0 or no silently keep Secure on.
  • The warning logs value: process.env.SESSION_COOKIE_DOMAIN even for the SESSION_COOKIE_SECURE ignored case, where the domain is unset and the value is misleading.
  • The domain regex requires an alphabetic-only TLD, so punycode TLDs such as xn--p1ai are rejected. It accepts public suffixes like co.uk, which browsers silently refuse, so the "TLD-wide footgun" comment overstates the protection.
  • SameSite=Lax breaks IdPs that use response_mode=form_post, because Lax cookies are not sent on cross-site POSTs. Worth a note in the docs.

@MichaelUray

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. The requested domain-scoped cookie isolation is
retained: a valid SESSION_COOKIE_DOMAIN uses huly.sess, while deployments
without that setting keep the existing koa.sess key.

The rename has an intentional one-time compatibility effect. koa-session
reads only huly.sess after domain isolation is enabled, so it does not reuse
or migrate a prior koa.sess session. Users must sign in again, and any OIDC
login started before deployment must be restarted because its state, nonce,
and PKCE values are in the old session. This is documented in both the
operator README and PR description. The session-cookie integration coverage
also verifies that an old koa.sess OIDC session is not reused after the
switch.

The other review changes remain in place:

  • SESSION_COOKIE_DOMAIN changes cookie scope only; it does not imply TLS,
    force Secure, or force SameSite=Lax.
  • SESSION_COOKIE_SECURE is an explicit independent option. It accepts
    true/false, 1/0, and yes/no; invalid values identify their own
    variable and value in the warning.
  • The operator documentation covers valid domain examples, public-suffix
    limitations, TLS termination, and response_mode=form_post compatibility.
  • Domain validation accepts syntactically valid punycode labels and leaves
    public-suffix enforcement to browsers.

Formatting passes, and the full pods/authProviders jest suite passes (34 tests, including the session-cookie integration tests).

…n OIDC

koa-session defaults the cookie domain to the request host. OIDC flows
that start on one subdomain (dev.example.com) and get the callback
on another (app.example.com, the host registered with the IdP) lose
the Passport state — the cookie set on dev isn't sent to app, so the
callback handler errors with 'did not find expected authorization
request details in session'.

SESSION_COOKIE_DOMAIN=.example.com scopes the cookie to the parent
domain so both subdomains share it. Empty / unset preserves the prior
behaviour for setups that only use a single host.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
…domain validation

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
… asserts TLS termination (opt-out SESSION_COOKIE_SECURE=false)

With SESSION_COOKIE_DOMAIN set, koa-session was configured with
secure: true. The account service listens on plain HTTP behind a
TLS-terminating proxy and does not set app.proxy, so Koa builds
ctx.cookies with secure: false and the cookies module throws
"Cannot send secure cookie over unencrypted connection": /auth/openid
answered 500.

Setting SESSION_COOKIE_DOMAIN is now treated as the operator's
assertion that HTTPS is terminated in front of the service. A small
middleware registered before koa-session marks the per-request cookie
jar as secure (ctx.cookies.secure = true), so the session cookie is
written with Domain, SameSite=Lax and Secure. No forwarded header is
trusted: app.proxy stays false and ctx.secure, ctx.host,
ctx.request.href and ctx.ip are unchanged. There are no proxy
requirements.

SESSION_COOKIE_SECURE=false opts out for local plain-HTTP development
(domain cookie without Secure, with a warning). Unset or invalid
SESSION_COOKIE_DOMAIN keeps the prior behaviour.

Cookies written with an explicit secure option keep it, so the login
cookie from PUT/DELETE /cookie (secure: ctx.request.secure) is
unchanged; only a cookie set without a secure option would now default
to Secure, and the account service writes none.

Tests: decision matrix for resolveSessionCookieOptions, and
request-level tests over plain HTTP (http.createServer + fetch)
covering the 302 with Secure session cookie, the explicit/implicit
secure behaviour of other cookies, the opt-out, the unset case, and a
negative control that reproduces the 500 without the marker.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
…cumented SESSION_COOKIE_SECURE; address review

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
@MichaelUray
MichaelUray force-pushed the feat/auth-session-cookie-domain branch from 0e5de3a to fd27aa2 Compare October 14, 2026 16:52
@MichaelUray

Copy link
Copy Markdown
Contributor Author

Rebased onto current develop, which now includes the OIDC callback hardening from #10931 (fd27aa2). No conflicts and no code changes: the session middleware is still installed before the hardened provider registration, and the 58 tests in pods/authProviders pass. The test fixtures now use .example.com.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants