Skip to content

PPT-2536: fix SAML/ADFS login + OAuth callback lost-id (UAT-breaking)#4

Merged
camreeves merged 2 commits into
masterfrom
PPT-2536-saml-relaystate
Jul 17, 2026
Merged

PPT-2536: fix SAML/ADFS login + OAuth callback lost-id (UAT-breaking)#4
camreeves merged 2 commits into
masterfrom
PPT-2536-saml-relaystate

Conversation

@camreeves

Copy link
Copy Markdown
Contributor

Two callback-state fixes for the drop-in — both currently break a login path.

1. SAML/ADFS RelayState (every SAML login 401s)

The IdP posts the signed assertion cross-site (HTTP-POST binding) to the ACS URL, so the browser withholds our SameSite=Lax auth-flow cookie — the CSRF state stashed in #initiate is unavailable — and the IdP echoes the CSRF value back as RelayState, not state. The legacy Ruby service never did session-state CSRF for SAML; the callback was authenticated purely by the signed assertion. Fix: skip the session-state check for the adfs/saml providers and let the assertion signature authenticate. OAuth2 keeps full state validation.

2. OAuth callback lost-id (login 401s if IdP drops ?id=)

Some OAuth providers don't round-trip the ?id= query param on the callback redirect_uri, so id is nil and the CSRF check 401s "oauth state mismatch". Fix: recover the exact id from the session state stashed at #initiate when the callback omits it (a present-but-mismatched id is left alone, so CSRF is still enforced).

Testing

  • SAML: new specs verify the state check is skipped for saml/adfs callbacks but still enforced for OAuth2 (green locally).
  • id-recovery: new provider-flow spec drives the full Google round-trip with ?id= omitted and asserts it still completes (validated in CI — the WebMock spec OOMs my local machine alongside the running stack).

🤖 Generated with Claude Code

camreeves and others added 2 commits July 17, 2026 21:39
…eSite)

SAML/ADFS logins 401'd with "oauth state mismatch" on every real IdP
response. The IdP posts the signed assertion cross-site (HTTP-POST binding)
to the ACS URL, so the browser withholds our SameSite=Lax auth-flow cookie —
the CSRF `state` stashed in `#initiate` is unavailable — and the IdP echoes
the CSRF value back as `RelayState`, not `state`, anyway. The legacy Ruby
service (omniauth-saml) never did session-state CSRF for SAML; the callback
was authenticated purely by the signed assertion. Mirror that: skip the
session-state check for the `adfs`/`saml` providers and let the assertion
signature authenticate. The OAuth2 path keeps full state validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… ?id=

Some OAuth providers don't round-trip the `?id=` query param on the
callback redirect_uri, leaving `id` nil so the CSRF state check failed
(`expect_id != (id || "")`) → 401 "oauth state mismatch". The exact id was
stashed on the session in `#initiate`, so recover it when the callback omits
it — more precise than the legacy Ruby guess. A present-but-mismatched id is
left alone so the state check still rejects it as CSRF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@camreeves
camreeves merged commit 71b6bda into master Jul 17, 2026
7 checks passed
@camreeves
camreeves deleted the PPT-2536-saml-relaystate branch July 21, 2026 04:00
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.

1 participant