UNOMI-975: Bind a public context request to the profile its own cookie names - #851
Merged
Merged
Conversation
Closed
10 tasks
…e names /context.json and /eventcollector accept a profile id in the request body and a session id with the request. For a public caller these are now treated as claims to be checked rather than as instructions: the body profileId is honoured only when it matches the caller's own context-profile-id cookie, and a supplied session is continued only when that cookie already owns it. The cookie is the single source of truth for who a public caller is. A session that is not continued is detached rather than rebound, and its id is not echoed back in the response - a client that saw its own id returned would keep replaying an id the server has not accepted. Anonymous browsing, personas and profile overrides are unchanged. A caller with no cookie is still issued a profile, which is how tracking has always worked. The server-side path is preserved: a caller holding the tenant private key still sets the body profileId, gated on hasSystemAccess() behind isTrustedProfileCaller() so the distinction has one seam rather than being spread across the call sites. The shipped profile cookie now defaults to HttpOnly. The model above rests on that cookie not being readable from page script, so the default is part of the design rather than a preference. ContextEndpointBaselineIT records the behaviour on both sides of the change: its compat_* tests must pass either way, and its hardened_* tests pin the new guarantees. The nine binding tests in ContextServletIT pin what must not change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
asf-gitbox-commits
force-pushed
the
UNOMI-975-public-profile-cookie-binding
branch
from
August 14, 2026 14:32
9912918 to
e3741c6
Compare
…ofile-cookie-binding # Conflicts: # manual/src/main/asciidoc/recipes.adoc
The ownership rule the previous commit added only reached named sessions. An anonymous session records no owner at all - PrivacyService#getAnonymousProfile returns a profile with no itemId, so the session's profileId is null - which left two ways past it. The de-anonymising branch rebound such a session to whoever presented its id and saved it, and the invalidateSession guard tested "owner differs", which a null owner passed. Either one handed a visitor's session to a caller that merely knew the id. Ownership is now established positively rather than by absence of a mismatch, through a single isOwnedByCookieBearer() used by both call sites, so an unowned session answers "not yours". Binding an anonymous session back to a named profile is reserved for trusted callers; a public caller leaves it anonymous and the visitor picks up a named session once its client rotates the session id. That also stops the rebinding from retroactively re-attributing every event already recorded in the session to a real profile, which is the outcome anonymous browsing was asked for to begin with. The refusal to bind a body profileId now logs at WARN rather than DEBUG, matching the two sibling refusals: an integration that used to bind a profile this way stops working at that line, and DEBUG left an operator with nothing to find. The anonymous case stays at INFO on purpose - it cannot tell a takeover attempt from the visitor who just turned anonymity off, and it repeats until the session id rotates, so a WARN there would devalue the ones that mean something. Also in the same pass: sanitize the one new log statement that interpolated a request-supplied id raw, drop the null guard on a mandatory @reference so a missing identity service fails loudly instead of silently downgrading every caller to untrusted, and reorder the profile-switch branch so the security condition reads without a double negative. Tests: the anonymous takeover is pinned at unit level for both the rebinding and invalidateSession routes, plus the trusted caller that must still be allowed through. ContextEndpointBaselineIT gains end-to-end coverage for the anonymous takeover, for /eventcollector - previously only exercised for compatibility, never for the hardening it shares with /context.json - and a runtime assertion that the profile cookie really is issued HttpOnly rather than only that the shipped default says so. The foreign-session test now also asserts the refused id is not echoed and that the rightful owner still holds the session afterwards. Docs: the migration guide gains the "Client-facing hardening (3.1)" section that four pages already linked to but which was never written, covering each field's 3.0 and 3.1 behaviour, the HttpOnly default and the widened cookie validation. Corrected the claim in builtin-event-types and recipes that cross-profile targetId and systemProperties.* writes require a trusted caller - that gate is source-address based and independent of this distinction - and the session rules in how-profile-tracking-works, which stated the guarantee more broadly than the code delivered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/context.json and /eventcollector accept a profile id in the request body and a session id with the
request. For a public caller these are now treated as claims to be checked rather than as
instructions: the body profileId is honoured only when it matches the caller's own
context-profile-id cookie, and a supplied session is continued only when that cookie already owns
it. The cookie is the single source of truth for who a public caller is.
A session that is not continued is detached rather than rebound, and its id is not echoed back in
the response - a client that saw its own id returned would keep replaying an id the server has not
accepted.
Anonymous browsing, personas and profile overrides are unchanged. A caller with no cookie is still
issued a profile, which is how tracking has always worked. The server-side path is preserved: a
caller holding the tenant private key still sets the body profileId, gated on hasSystemAccess()
behind isTrustedProfileCaller() so the distinction has one seam rather than being spread across the
call sites.
The shipped profile cookie now defaults to HttpOnly. The model above rests on that cookie not being
readable from page script, so the default is part of the design rather than a preference.
ContextEndpointBaselineIT records the behaviour on both sides of the change: its compat_* tests must
pass either way, and its hardened_* tests pin the new guarantees. The nine binding tests in
ContextServletIT pin what must not change.
Jira: https://issues.apache.org/jira/browse/UNOMI-975