Skip to content

Support HttpOnly session cookie in XUI#1036

Open
vharseko wants to merge 14 commits into
OpenIdentityPlatform:masterfrom
vharseko:issues/xui
Open

Support HttpOnly session cookie in XUI#1036
vharseko wants to merge 14 commits into
OpenIdentityPlatform:masterfrom
vharseko:issues/xui

Conversation

@vharseko

@vharseko vharseko commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

Make the XUI work correctly when the OpenAM session cookie (e.g.
iPlanetDirectoryPro) is issued with the HttpOnly flag, where JavaScript can no
longer read the token from document.cookie. The change spans both the client
(XUI) and the server (REST authenticate flow), and it hardens the
/json/authenticate response so that a successful login does not echo a
replayable SSO token in the response body when the cookie is HttpOnly.

Motivation

HttpOnly cookies are inaccessible to JavaScript and are only sent automatically
by the browser on HTTP requests. The legacy XUI relied on reading the tokenId
from the cookie (and from the /json/authenticate response body) to:

  • detect the current session,
  • drive logout,
  • and supply sessionUpgradeSSOTokenId for an agent-driven session upgrade
    (step-up) after a fresh page load.

With HttpOnly enabled, none of those reads are possible from the browser, so the
XUI must rely on the auto-sent cookie and on server-side fallbacks instead.

Changes

Client (XUI)

  • The XUI no longer assumes it can read the token from document.cookie. It
    relies on the auto-sent cookie and on idFromSession to detect the session.
  • It no longer consumes body.tokenId from the /json/authenticate response in
    HttpOnly mode.

Server

  • Session-upgrade (step-up) cookie fallback. When the XUI cannot send
    sessionUpgradeSSOTokenId (because the token is not readable in HttpOnly
    mode), the REST authenticate flow falls back to the session carried by the
    auto-sent HttpOnly cookie as the upgrade target. This fallback is:

    • gated to HttpOnly mode (token-readable deployments keep the existing
      behaviour unchanged), and
    • gated to real step-up requests only — it resolves the cookie only when an
      upgrade index/advice or ForceAuth is present
      (stepUpRequested = (indexType != null && indexType != AuthIndexType.NONE) || forceAuth).
      A plain "am I logged in" probe (POST /json/authenticate with body {}) does
      not resolve the cookie and therefore cannot trigger an immediate
      completion that would echo a token.
  • Token is not echoed in the response body in HttpOnly mode (default). A
    successful /json/authenticate response (the LoginStage.COMPLETE branch) no
    longer writes tokenId into the JSON body when the cookie is HttpOnly. The
    token is delivered to the browser solely via the Set-Cookie header. This
    closes a token-exfiltration path: an XSS on the origin can otherwise fetch the
    endpoint and read a full, replayable SSO token (including a freshly upgraded
    one) — precisely what HttpOnly is meant to prevent.

    Note: the server-side audit property (AuditRequestContext.putProperty(TOKEN_ID, …))
    is unchanged — it is never exposed to the client.

New configuration property

Property Default Description
org.openidentityplatform.openam.httponly.allowTokenInBody false Only relevant when the session cookie is HttpOnly (com.sun.identity.cookie.httponly=true). Controls whether the SSO tokenId is returned in the /json/authenticate response body.

Behavior matrix (/json/authenticate success response body)

com.sun.identity.cookie.httponly …httponly.allowTokenInBody tokenId in response body?
false (default) (ignored) Yes — unchanged legacy behaviour
true false (default) No — token delivered only via Set-Cookie
true true Yes — opt-in legacy behaviour in HttpOnly mode

In all cases the session cookie is still set via the Set-Cookie header, so the
browser keeps working transparently.

Compatibility / BREAKING CHANGE

When the cookie is HttpOnly, the default response body of a successful
/json/authenticate no longer contains tokenId. This is a deliberate,
default-secure behaviour change for HttpOnly deployments.

  • Browser / XUI clients: no action required — they use the auto-sent cookie.
  • Non-browser / raw-REST integrations that need the token in the body while
    the cookie is HttpOnly should either read the token from the Set-Cookie
    header, or opt back in by setting
    org.openidentityplatform.openam.httponly.allowTokenInBody=true.
  • Deployments that do not use HttpOnly cookies are unaffected — tokenId is
    always returned, exactly as before.

Testing

  • RestAuthenticationHandlerTest (unit) covers:
    • the response body omits tokenId when the cookie is HttpOnly and
      allowTokenInBody=false (default);
    • the response body includes tokenId when the cookie is HttpOnly and
      allowTokenInBody=true;
    • the step-up cookie fallback is gated to real step-up requests and to
      HttpOnly mode.
  • e2e/xui/xui-httponly.spec.mjs (Playwright, mode-agnostic) covers
    login/session/logout, staying logged in after a page reload, and step-up after
    a fresh page load being recognised as a session upgrade. It asks the server for
    its actual mode (GET /json/serverinfo/*, field cookieHttpOnly) and asserts
    the browser cookie and XUI behaviour match that mode. In HttpOnly mode it also
    asserts that the success response body does not echo tokenId.

vharseko added 4 commits June 4, 2026 14:21
Allow the XUI to work correctly when the OpenAM session cookie
(iPlanetDirectoryPro) is issued with the HttpOnly flag, where the token
cannot be read from JavaScript via document.cookie.

Server
- ServerInfoResource: expose the new "cookieHttpOnly" flag in
  /json/serverinfo/* so the XUI can detect the mode at runtime.

XUI
- SessionToken: detect HttpOnly mode via Configuration.globalData.cookieHttpOnly.
  When enabled, keep the token in memory for the page lifetime instead of
  reading/writing the cookie, return an HTTP_ONLY_SESSION_TOKEN sentinel from
  get(), and add isHttpOnly()/isResolvable() helpers. set()/remove() become
  no-ops on the cookie since it is managed server-side.
- SessionService: omit the tokenId query param for getSessionInfo/logout when
  the token is not client-readable, so the server resolves the session from the
  automatically-sent HttpOnly cookie; suppress expected 400/401 for the
  no-token case.
- AuthNService: only send sessionUpgradeSSOTokenId when the token is resolvable.
- SiteConfigurationService: tolerate a missing/invalid session in
  checkForDifferences() and continue rendering instead of stalling.

Tests / CI
- e2e/xui/xui-httponly.spec.mjs: mode-agnostic Playwright spec that asserts the
  cookie HttpOnly attribute, JS visibility, login/idFromSession detection and
  logout match the server mode.
- build.yml: run the UI smoke tests in both modes — default (HttpOnly disabled)
  and after restarting the IDP with
  -Dcom.sun.identity.cookie.httponly=true (HttpOnly enabled).

Docs
- dev-guide: document the new cookieHttpOnly field in the serverinfo example.
The HttpOnly spec waited for "#loginButton", which does not exist in this
XUI build, so the login click hung until the global test timeout.

- Match the submit button by type, like the working SAML spec, and keep the
  id as a fallback: "#loginButton, input[type=submit], button[type=submit]".
- Click the first visible match to avoid a strict-mode violation when several
  elements match.
Stabilize the XUI HttpOnly Playwright spec so it passes against the real
XUI build with the session cookie both with and without the HttpOnly flag.

- Login: match the submit button by type with the id as a fallback
  ("#loginButton, input[type=submit], button[type=submit]") and click the
  first visible match, since "#loginButton" is absent in this build.
- Logout: XUI redirects to "#loggedOut/" (not "#login/"), so accept either
  route when waiting for the session to end.
- Logout assertion: verify the session is invalidated server-side via
  idFromSession instead of checking the browser cookie. In HttpOnly mode JS
  cannot clear the cookie and the REST logout may not emit a Set-Cookie, so a
  stale-but-dead cookie can linger; server-side invalidation holds in both modes.
@vharseko vharseko requested a review from maximthomas June 4, 2026 18:18
@maximthomas

Copy link
Copy Markdown
Contributor

I suggest this commit 3913a59 can be reverted for now.
Please also add e2e test to ensure a user stays logged in into admin console after reloading page in a browser.

vharseko added 2 commits June 4, 2026 22:36
Reverts the workaround from 3913a59 that disabled the XUI entirely when the session cookie is HttpOnly. The XUI now supports HttpOnly session cookies, so it must stay enabled in that mode.
Add an e2e check that an admin remains authenticated in the console after a full browser page reload (the reload drops any in-memory token, so the session must be re-detected from the auto-sent cookie). Extract shared loginViaXui()/idFromSession() helpers and update the PR description.
@vharseko vharseko requested a review from tsujiguchitky June 5, 2026 05:26
@tsujiguchitky

Copy link
Copy Markdown
Contributor

Nice work on the HttpOnly support - login/reload/logout look solid.

One thing I wanted to check with you: does agent-driven session upgrade (step-up) still work in HttpOnly mode?

The way I read it, the step-up redirect is a fresh page load, so inMemoryToken is empty and XUI can't send sessionUpgradeSSOTokenId. And server-side that param looks like the only source for the session to upgrade - LoginAuthenticator resolves it via getExistingValidSSOToken(new SessionID(getSSOTokenId())) and never reads the cookie. So wouldn't it fall through to a brand-new login instead of an upgrade (old session orphaned, properties/sessionHandle lost, composite-advice cases possibly looping)?

If that's right, would it make sense to fall back to the session cookie as the upgrade target in the REST authenticate flow when sessionUpgradeSSOTokenId is absent?

…arget

When the session cookie is configured as HttpOnly, the XUI cannot read the
tokenId from JavaScript and therefore cannot send the sessionUpgradeSSOTokenId
query parameter on an agent-driven session upgrade (step-up), which is performed
via a fresh page load with an empty in-memory token. Server-side that parameter
was the only source for the session to upgrade, so the request fell through to a
brand-new login: the existing session was orphaned, its properties/sessionHandle
were lost, and composite-advice step-up could loop.

RestAuthenticationHandler now resolves the upgrade target from the auto-sent
HttpOnly session cookie when sessionUpgradeSSOTokenId is absent. The fallback is
limited to the HttpOnly deployment mode (CookieUtils.isCookieHttpOnly()), so the
behaviour of all other token-readable deployments is unchanged.

Also clean up leftover merge-conflict markers in the file's license header.

Changes:
- RestAuthenticationHandler: add resolveSessionUpgradeTarget() and apply it in
  the authenticate flow before resolving the auth index.
- RestAuthenticationHandlerTest: cover the cookie fallback in HttpOnly mode and
  the unchanged behaviour when a token is supplied / HttpOnly is off.
- e2e/xui/xui-httponly.spec.mjs: add a step-up scenario asserting the existing
  session is recognised as the upgrade target (no fresh authId/callbacks) in
  HttpOnly mode; consolidated from the separate session-upgrade spec.
@vharseko vharseko requested a review from maximthomas June 9, 2026 11:11
@vharseko

vharseko commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

One thing I wanted to check with you: does agent-driven session upgrade (step-up) still work in HttpOnly mode?

The way I read it, the step-up redirect is a fresh page load, so inMemoryToken is empty and XUI can't send sessionUpgradeSSOTokenId. And server-side that param looks like the only source for the session to upgrade - LoginAuthenticator resolves it via getExistingValidSSOToken(new SessionID(getSSOTokenId())) and never reads the cookie. So wouldn't it fall through to a brand-new login instead of an upgrade (old session orphaned, properties/sessionHandle lost, composite-advice cases possibly looping)?

If that's right, would it make sense to fall back to the session cookie as the upgrade target in the REST authenticate flow when sessionUpgradeSSOTokenId is absent?

Thanks, please check d742217

@tsujiguchitky

Copy link
Copy Markdown
Contributor

Sorry for the late review on this one. The step-up fix itself looks correct.

One concern I'd like to raise before this lands: I think the fallback re-opens a token-exfiltration path that HttpOnly is meant to close.

In HttpOnly mode, a bare POST /json/authenticate with body {} and just the auto-sent cookie now returns the real tokenId in the response body (the new e2e actually asserts this). The chain is: no index -> AuthIndexType.NONE -> checkSessionUpgrade returns false -> noMoreAuthenticationRequired is true -> CompletedLoginProcess echoes the existing session's tokenId.

Before this fix, the same request started a fresh login (authId + callbacks) and never returned the token. So now any XSS on the origin can grab a full, replayable SSO token at any time with a single fetch - regardless of reload state - which is precisely what HttpOnly was supposed to prevent. (A genuine step-up that needs more auth still returns callbacks first, so that path is fine - the leak is only the "already-satisfied / immediate completion" case.)

Could we limit the fallback to actual step-up requests (i.e. only resolve the cookie when an upgrade index/advice or forceAuth is present), so a plain "am I logged in" probe doesn't echo the token? That would keep the step-up fix intact while shrinking the attack surface.

When the session cookie is configured as HttpOnly, the XUI cannot read the
tokenId from JavaScript and relies on the auto-sent cookie (and idFromSession)
to detect the session. Yet a successful /json/authenticate response still echoed
the tokenId in the response body. That re-opened the very token-exfiltration
path HttpOnly is meant to close: any XSS on the origin could read a full,
replayable SSO token with a single fetch — including a freshly upgraded one
returned by a step-up.

Gating the cookie-based step-up fallback alone was not enough: an already-
satisfied request (e.g. an auth-level upgrade the session already meets) still
completes immediately and would echo the existing token, and a genuine upgrade
returns a brand-new token in the body. Both cases leak. The robust fix is to
suppress the token in the response body whenever the cookie is HttpOnly,
regardless of whether it changed, because the browser receives the token only
via the Set-Cookie header and the XUI never consumes body.tokenId in this mode.

Server change (RestAuthenticationHandler.processAuthentication, COMPLETE stage):
- Only put TOKEN_ID into the JSON response when CookieUtils.isCookieHttpOnly()
  is false. successUrl/realm/authId/callbacks are unchanged.
- Keep AuditRequestContext.putProperty(TOKEN_ID, ...) — that is server-side
  audit only and is never sent to the client.

The session-upgrade cookie fallback (resolveSessionUpgradeTarget) remains scoped
to HttpOnly mode via isCookieHttpOnly(), so token-readable deployments keep their
existing behaviour and the contract change is confined to HttpOnly mode.

Tests:
- RestAuthenticationHandlerTest:
  - shouldNotEchoTokenIdInResponseBodyWhenCookieIsHttpOnly — successful auth
    returns realm/successUrl but no tokenId.
  - shouldEchoTokenIdInResponseBodyWhenCookieIsNotHttpOnly — default mode still
    returns tokenId.
- e2e/xui/xui-httponly.spec.mjs (step-up scenario):
  - Assert body.tokenId is absent in HttpOnly mode and that the existing session
    is recognised via the absence of a fresh login (no authId/callbacks), a
    successful completion (successUrl/realm) and idFromSession resolving to the
    same user — instead of reading a token from the body.
  - Header comment updated to document that the token never leaves the body in
    HttpOnly mode.

BREAKING CHANGE: in HttpOnly cookie mode, POST /json/authenticate no longer
returns tokenId in the response body; the session token is delivered only via
the Set-Cookie header. Non-browser/raw-REST clients that previously read
body.tokenId in HttpOnly deployments must obtain the token from the cookie.
Token-readable (non-HttpOnly) deployments are unaffected.
@vharseko

Copy link
Copy Markdown
Member Author

@maximthomas @tsujiguchitky please review 85eeff5

@vharseko vharseko requested a review from maximthomas June 12, 2026 10:34
@maximthomas

maximthomas commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

@vharseko, @tsujiguchitky the 85eeff5 commit could potentially break existing API integrations, as it changes the response body. At minimum, this should be documented. I suggest using another option to remove the tokenId attribute from the response body, because users may need both tokenId and httpOnlyCookie

Edit: clarify the comment

vharseko added 6 commits June 12, 2026 19:01
Allow OpenAM to be installed against an external OpenDJ when the base DN
(root suffix) has not been pre-created, removing the need for the OpenDJ
"--addBaseEntry" / ADD_BASE_ENTRY option.

- AMSetupDSConfig: add createBaseEntry() to create the root suffix when
  missing, deriving the objectClass from the RDN (dc/o/ou), with an
  existence check and ENTRY_ALREADY_EXISTS handling for idempotency.
- AMSetupServlet: call createBaseEntry() before loading schema files for
  an external (dsSmsSchema) configuration store, mirroring the embedded
  behaviour that creates the suffix via openam_suffix.ldif.
- ServicesDefaultValues: create the base entry instead of failing with
  configurator.invalidsuffix when the suffix does not yet exist.
- Step3 wizard: treat a missing root suffix (NO_SUCH_OBJECT) as valid so
  the Next button is enabled; only real connection/auth failures block.
- Add IT_SetupWithOpenDJ integration tests covering external OpenDJ both
  with and without a pre-created base DN; deploy a separate /am2 context,
  raise Tomcat heap to 2g and extend startup/install timeouts.
…ly cookie mode

In HttpOnly session-cookie mode the browser cannot read the token from
document.cookie, so the XUI relies on the auto-sent cookie. However, a
successful /json/authenticate response still echoed the SSO tokenId in the
JSON body. That re-opened a token-exfiltration path HttpOnly is meant to
close: an XSS on the origin could fetch the endpoint and read a full,
replayable SSO token (including a freshly upgraded one) with a single call.

Stop writing tokenId into the success (LoginStage.COMPLETE) response body
when the session cookie is HttpOnly. The token is delivered to the browser
solely via the Set-Cookie header. The server-side audit property
(AuditRequestContext) is unchanged and is never exposed to the client.

Because some non-browser/raw-REST integrations need both the HttpOnly
cookie and the token in the body, the suppression is configurable via a new
property:

  org.openidentityplatform.openam.httponly.allowTokenInBody (default: false)

Behaviour matrix (success response body):
  - httponly=false                       -> tokenId returned (legacy, unchanged)
  - httponly=true,  allowTokenInBody=false-> tokenId NOT returned (default-secure)
  - httponly=true,  allowTokenInBody=true -> tokenId returned (opt-in legacy)

The step-up cookie fallback (resolveSessionUpgradeTarget) remains gated to
HttpOnly mode and to real step-up requests only (upgrade index/advice or
ForceAuth), so a plain "am I logged in" probe (POST {}) cannot trigger an
immediate completion that echoes a token.

BREAKING CHANGE: in HttpOnly cookie mode a successful /json/authenticate
response no longer contains tokenId by default. Browser/XUI clients are
unaffected (they use the auto-sent cookie). Raw-REST integrations should read
the token from the Set-Cookie header, or opt back in by setting
org.openidentityplatform.openam.httponly.allowTokenInBody=true. Deployments
that do not use HttpOnly cookies are unaffected.

Changes:
- Constants: add AM_COOKIE_HTTPONLY_ALLOW_TOKEN_IN_BODY
- CookieUtils: add httpOnlyAllowTokenInBody flag + isHttpOnlyAllowTokenInBody()
- RestAuthenticationHandler: gate body tokenId on
  !isCookieHttpOnly() || isHttpOnlyAllowTokenInBody()
- RestAuthenticationHandlerTest: cover both default-secure and opt-in cases
- e2e/xui/xui-httponly.spec.mjs: assert no body.tokenId in default HttpOnly mode
- docs/pr/xui-httponly-session-cookie.md: document property and behaviour matrix
@vharseko

Copy link
Copy Markdown
Member Author

@vharseko, @tsujiguchitky the 85eeff5 commit could potentially break existing API integrations, as it changes the response body. At minimum, this should be documented. I suggest using another option to remove the tokenId attribute from the response body, because users may need both tokenId and httpOnlyCookie

Edit: clarify the comment

please check 797e816

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.

3 participants