[#1080] OAuth2 consent: accept the resource owner's session id as the csrf value - #1083
[#1080] OAuth2 consent: accept the resource owner's session id as the csrf value#1083maximthomas wants to merge 2 commits into
Conversation
…m value minted while rendering the consent page. createCsrfToken() has a single caller, so a client that posts its consent decision directly never obtains a token and is rejected with HTTP 400 - the regression reported in OpenIdentityPlatform#1080. isCsrfAttack() now also accepts the resource owner's own session id, after the session-property and double-submit-cookie checks. The branch is additive, so nothing that passes today changes, and createCsrfToken() is untouched: the consent page still renders a random token and the SSO cookie need not be script-readable. Both consent gates share the helper, so /oauth2/device/user is repaired alongside /oauth2/authorize. This does not weaken the protection a browser gets. A foreign origin can read neither the victim's cookies nor a cross-origin response, so an attacking page cannot discover the session id, and anything that already knows it can act as the user outright without forging a consent decision. Also corrects the stale "Session id from consent request does not match users session" debug message, which described the pre-CVE comparison, and documents both accepted values in the administration and developer guides.
The three consent tests failed on their first CI run: the authorize request redirected with error=invalid_request, "Missing parameter, 'code_challenge'", so it never reached consent handling. Both clients are public and authenticate with "none", so OpenAM requires PKCE. The request validators run before the csrf check, so the two rejection tests would also have failed for this reason rather than the one they assert. Both the GET that renders the consent page and the POST that carries the decision now build their parameters from one helper, so they cannot drift apart again. The PKCE helpers move to module scope instead of being redefined inside the existing test.
Overview
Security assessmentI traced the threat model and the argument holds:
Verified plumbing: So this is not a reintroduction of the CVE. The concerns below are about hardening and blast radius, not a broken threat model. Findings1. No way to decline the legacy value (design — the one point I would push back on)The PR lists this as a known limitation and rejects a toggle in favour of minimality. I would reconsider, for one reason that isn't about the threat model: this relaxes something that shipped under a CVE number. An operator who read the advisory, set Two options, in order of preference:
Strategically, the cleaner long-term fix is to give headless clients a supported way to obtain the minted token — 2. The session id is accepted from the query string (hardening — cheap and worth doing)
The code can enforce what the doc asks for, using existing API — // The session id is a live credential; only honour it from the request body, never from the
// query string, where it would be recorded in access logs and Referer headers.
if (ssoToken != null && request.getParameterCount("csrf") == 0) {
SSOTokenID ssoTokenId = ssoToken.getTokenID();
...
}The minted-token and cookie branches are unaffected (a random per-request token in a URL is harmless), so this narrows only the new behaviour. Worth confirming 3. Copyright year not bumped on a modified file (project convention)
4. Dev guide and admin guide now disagree in emphasis
This repo just merged b4c030a ( 5.
|
tsujiguchitky
left a comment
There was a problem hiding this comment.
LGTM - The addition to isCsrfAttack() is purely additive and leaves the existing check order, the consent page, and the HttpOnly hardening untouched. Approving.
Fix #1080
Problem
CVE-2026-53660 replaced the OAuth2 consent
csrftoken with a dedicated random value bound to theauthorization request. That fix is correct and stays: it removed the need for the consent page to read the SSO
cookie from JavaScript, which is what forced
com.sun.identity.cookie.httponly=false.The side effect reported in #1080 is that
CsrfProtection.createCsrfToken()has exactly one caller —ConsentRequiredResource.getDataModel(), reached only while rendering the HTML consent page. A client thatposts its consent decision directly never executes it, so both consent gates reject it with HTTP 400 and there
is no supported way for a non-browser client to obtain a token. The documented
curlflow in the developerguide stopped working.
Change
CsrfProtection.isCsrfAttack()gains a third acceptance branch, after the protected-session-property anddouble-submit-cookie checks:
This restores the pre-16.1.1 contract. Three properties worth calling out:
trueintofalse, so nothing that passes today changes.createCsrfToken()is untouched. The consent page still renders a dedicated random token, so the SSOcookie still does not need to be script-readable and the CVE fix stands.
ssoToken.getTokenID().toString()unconditionally and would NPE onan unauthenticated POST. Both guards are covered by tests.
Comparison is constant-time (
MessageDigest.isEqual), as it was before.Why this does not weaken CSRF protection
A foreign origin can read neither the victim's cookies nor the response to a cross-origin request, so an
attacking page cannot discover the session id it would have to submit. Anything that already knows the session
id can act as the user outright — including driving the consent flow legitimately by fetching the page and
reading the minted token — so no new capability is granted.
This holds regardless of the
HttpOnlysetting, which ships disabled by default.Scope
Both consent gates call the shared helper, so this repairs
/oauth2/device/user(
DeviceCodeVerificationResource) alongside/oauth2/authorize(AuthorizationService). The developer guidedocuments a headless
curlfor the device endpoint too, so both were broken and both are fixed.Also in this PR
call sites; it described the pre-CVE comparison and no longer matched the code.
admin-guide/chap-oauth2.adoc(the normative description) anddev-guide/chap-client-dev.adoc(the per-endpoint reference), including a worked two-step headless example.HttpOnlycaveat: withcom.sun.identity.cookie.httponly=true,/json/authenticatedeliversthe session only via
Set-Cookie, so a client must parse the id out before posting it ascsrf, or thedeployment must set
org.openidentityplatform.openam.httponly.allowTokenInBody=true.Testing
CsrfProtectionTestmvn -pl openam-oauth2 -am testoauth2-test.spec.mjs)The new unit tests cover: session id accepted; a different session id rejected; no session rejected (no NPE);
session with no token id rejected; and that the minted token, the session id, and nothing else are accepted
when both exist.
The e2e tests add a client with
isConsentImplied: falseso consent is genuinely required, and each testfirst asserts that
GET /oauth2/authorizereturns the consent page. That precondition both reproduces thescenario from #1080 and validates the client's redirect URI, scope and response type — without it the two
rejection tests would pass on any 400, including one caused by a stale client from an earlier run.
Known limitations
PR's CI run is their first real execution.
is no property to disable it. This is a deliberate choice — a toggle was considered and rejected in favour
of the smallest change that restores the documented contract — but a deployment that hardened around the
per-request token cannot decline the legacy value.
consequence of fixing the shared helper rather than as a targeted benefit.