Skip to content

fix(sso): restore front-end auto-SSO via main-site magic-link redirect#1297

Closed
superdav42 wants to merge 10 commits into
mainfrom
fix/sso-frontend-magic-link-flow
Closed

fix(sso): restore front-end auto-SSO via main-site magic-link redirect#1297
superdav42 wants to merge 10 commits into
mainfrom
fix/sso-frontend-magic-link-flow

Conversation

@superdav42

Copy link
Copy Markdown
Collaborator

Summary

Visiting a subsite front-end page while logged in to the main site was
not re-authenticating the visitor on the subsite — even in browsers
that permit third-party cookies. Two interacting regressions blocked
the flow:

  1. fix(sso): guard against unpopulated $current_blog during early bootstrap #1185 turned the JSONP unattached branch in handle_broker() into
    an immediate {code: 0, message: "Broker not attached"} response,
    which calls sso_denied() in sso.js and sets wu_sso_denied for
    5 minutes on the very first subsite page-load. After that there is
    no path that ever surfaces the main-site session to the broker.

  2. The redirect branch still called $broker->getAttachUrl() which
    targets /sso-grant. After the cookie-less rework in fix: cookie-less cross-domain SSO login flow #1084 the
    dispatcher in handle_requests() unconditionally appends _grant
    on main, so /sso-grant produces wu_sso_handle_sso_grant_grant
    — an action with no listener — and the request silently 404s on
    main, leaving the user logged out on the subsite.

What this changes

Both branches now redirect to the main-site /sso endpoint that
handle_server() already supports under the cookie-less rework.
handle_server() checks the first-party main-site auth cookie (so
third-party-cookie posture is irrelevant), issues an HMAC-signed
wu_sso_token via add_cookie_less_sso_token(), and 302s back to the
subsite where handle_cookie_less_sso_token() consumes the token at
init priority 4 and sets the broker-side auth cookie.

Resulting request chain (verified in browser):

GET <broker>/sso?_jsonp=1                              -> 200 JSONP (must-redirect)
GET <broker>/sso?return_url=<original_subsite_url>     -> 302
GET <main>/sso?sso=login
     &return_url=<original>&redirect_to=<original>     -> 302
GET <broker>/?wu_sso_token=<hmac>&redirect_to=<orig>   -> 302
GET <broker>/                                          -> 200, logged in

JSONP branch

The JSONP <script> tag cannot follow a 302 to HTML, so instead of
silently failing the broker returns verify: 'must-redirect'. The
already-present branch in assets/js/sso.js (lines 93-95) handles this
by performing a top-level window.location.replace() to
o.server_url + '?return_url=<encoded current>', which re-enters
handle_broker() via the non-JSONP path on the same request and
redirects to $main_sso_url.

Return-URL handling

  • The forwarded return_url is taken from the explicit ?return_url=
    query param when present (set by sso.js on the must-redirect
    navigation) and falls back to get_current_url() only on the direct
    first hit.
  • The /sso path itself is rejected as a return target — without
    this guard the value /sso?return_url=… would feed back into
    return_url on each iteration and the URL would grow on every hop,
    producing an ERR_TOO_MANY_REDIRECTS.
  • redirect_to is passed explicitly so get_sso_redirect_to() does
    not append /wp/wp-admin/ (its fallback when only a cross-domain
    return_url is supplied) and therefore does not send front-end
    visitors to the subsite admin.

Tests / verification

  • php -l clean on the modified file.
  • Verified end-to-end in a real browser against a Bedrock multisite
    with mapped subsite domains: a main-site-logged-in visitor lands
    logged-in on the subsite front-end page they originally requested.
  • Re-running the same flow without clearing wu_sso_denied is
    unaffected by the previous broken behaviour because the new code
    path no longer sets that cookie on its own.

Out of scope

Two related issues observed while diagnosing this, left for follow-up:

  1. /sso-grant routing is dead. The dispatcher in
    handle_requests() rewrites the action to sso_grant and then
    appends _grant, so the only URL that reaches handle_server() is
    /sso (not /sso-grant). The SSO_Broker::getAttachUrl() method
    still points at /sso-grant. After this PR no caller uses that URL,
    but it should either be removed or the dispatcher should normalise
    so the two URLs do the same thing.
  2. should_skip_redirect_due_to_loop() from GH#1199: fix: cap SSO redirects on mapped domains #1203 increments
    wu_sso_redirect_attempts on every logged-out handle_auth_redirect
    call regardless of whether the previous request was itself part of
    an SSO loop. It can burn the counter on benign repeat visits to
    wp-admin from a logged-out user. Consider scoping it to requests
    whose Referer is the main-site /sso* endpoint.

aidevops.sh v3.20.0 plugin for OpenCode v1.15.11 with claude-opus-4-7 spent 1h 2m and 81,144 tokens on this with the user in an interactive session.

Visiting a subsite front-end page while logged in to the main site was
not re-authenticating the visitor on the subsite even in browsers that
permit third-party cookies. Two interacting regressions blocked the
flow:

1. #1185 turned the JSONP unattached branch in handle_broker() into an
   immediate '{code:0, message:"Broker not attached"}' response, which
   triggered sso_denied() in sso.js and set wu_sso_denied for 5 minutes
   on the very first subsite page-load — there was no longer any path
   that ever surfaced the main-site session to the broker.

2. The redirect branch still called $broker->getAttachUrl() which
   targets /sso-grant. After the cookie-less rework in #1084 the
   dispatcher in handle_requests() unconditionally appends '_grant' on
   main, so /sso-grant produces 'wu_sso_handle_sso_grant_grant' — an
   action with no listener — and the request silently 404'd on the main
   site, leaving the user logged out on the subsite.

This change replaces both branches with a redirect to the main-site
/sso endpoint that handle_server() already supports. handle_server()
checks the first-party main-site auth cookie (so third-party-cookie
posture is irrelevant), issues an HMAC-signed wu_sso_token via
add_cookie_less_sso_token(), and 302s back to the subsite where
handle_cookie_less_sso_token() consumes the token at init priority 4
and sets the broker-side auth cookie:

  GET <broker>/sso?_jsonp=1                       -> 200 must-redirect JSONP
  GET <broker>/sso?return_url=<original>          -> 302
  GET <main>/sso?sso=login&return_url=<original>
       &redirect_to=<original>                    -> 302
  GET <broker>/?wu_sso_token=<hmac>
       &redirect_to=<original>                    -> 302
  GET <broker>/                                   -> 200, logged in

For the JSONP branch (which cannot follow a 302 to HTML in a <script>
tag), the response now returns 'verify: must-redirect' which the
already-present sso.js branch (assets/js/sso.js#L93) handles by
performing a top-level navigation. The non-JSONP path of handle_broker
is then re-entered as a normal page load and performs the redirect to
$main_sso_url.

The return_url passed forward is the explicit ?return_url= query param
when present (set by sso.js on the must-redirect navigation) and falls
back to get_current_url() only on the direct first hit. The /sso path
is rejected as a return target so a stray legacy URL cannot restart
the same nested-/sso ERR_TOO_MANY_REDIRECTS loop. redirect_to is also
passed explicitly so get_sso_redirect_to() does not append
/wp/wp-admin/ for front-end visitors.

Verified end-to-end in a real browser against a multisite with mapped
subsite domains: logged-in main-site visitor lands logged-in on the
subsite front-end page they originally requested.
@superdav42 superdav42 added the origin:interactive Created by interactive user session label May 27, 2026
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88c086ae-4167-4eeb-95ef-4889ae896dac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sso-frontend-magic-link-flow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@superdav42

Copy link
Copy Markdown
Collaborator Author

Follow-up issues filed for the two items in the "Out of scope" section above:


aidevops.sh v3.20.0 plugin for OpenCode v1.15.11 with claude-opus-4-7 spent 1h 11m and 87,936 tokens on this with the user in an interactive session.

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions

Copy link
Copy Markdown

Performance Test Results

Performance test results for 5c4e3ec are in 🛎️!

Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown.

URL: /

Run DB Queries Memory Before Template Template WP Total LCP TTFB LCP - TTFB
0 41 37.78 MB 833.50 ms (-50.50 ms / -6% ) 148.50 ms (-8.00 ms / -5% ) 1030.50 ms 1902.00 ms (-132.00 ms / -7% ) 1808.50 ms (-147.50 ms / -8% ) 80.30 ms (+2.70 ms / +3% )
1 56 49.13 MB 948.00 ms (+54.50 ms / +6% ) 141.50 ms (+7.50 ms / +5% ) 1093.50 ms (+64.50 ms / +6% ) 2086.00 ms (+54.00 ms / +3% ) 2014.10 ms (+53.50 ms / +3% ) 70.75 ms

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@superdav42

Copy link
Copy Markdown
Collaborator Author

CLAIM_RELEASED reason=worker_failed runner=superdav42 ts=2026-05-28T04:39:04Z aidevops_version=3.20.4 opencode_version=1.15.11

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@superdav42

Copy link
Copy Markdown
Collaborator Author

MERGE_SUMMARY

Status: BLOCKED — Cypress still fails on the target SSO spec after pushed fixes.

Changes pushed to fix/sso-frontend-magic-link-flow:

  • tests/e2e/cypress/fixtures/setup-sso-test.php: attempts to make the wp-env SSO subsite resolve on 127.0.0.1:8889 by seeding a native test host and mapped-domain rows.
  • tests/e2e/cypress/integration/060-sso-cross-domain.spec.js: aligns assertions with the mapped-host root-domain setup.
  • sunrise.php: adds a dynamic fallback to locate inc/class-sunrise.php when wp-env maps the plugin under a non-standard directory.

Verification:

  • php -l tests/e2e/cypress/fixtures/setup-sso-test.php — pass.
  • vendor/bin/phpcs tests/e2e/cypress/fixtures/setup-sso-test.php — pass.
  • php -l sunrise.php — pass.
  • vendor/bin/phpcs sunrise.php — pass.
  • GitHub checks on latest pushed commit bbe87789: PHP Lint, PHP 8.2/8.3/8.4/8.5, Code Quality Checks, Build Plugin for Testing — pass.
  • GitHub E2E run 26584302903: cypress (8.2, chrome) still fails in tests/e2e/cypress/integration/060-sso-cross-domain.spec.js; the response for http://127.0.0.1:8889/ remains 302 to http://localhost:8889/ instead of resolving to the test subsite.

Current blocker:

  • The CI wp-env request to 127.0.0.1:8889 is still handled as an unknown host and redirected to the main site before the SSO assertions can execute, so the target Cypress check is not green yet.

@superdav42

Copy link
Copy Markdown
Collaborator Author

CLAIM_RELEASED reason=worker_complete runner=superdav42 ts=2026-05-28T15:38:53Z aidevops_version=3.20.4 opencode_version=1.15.11

@superdav42

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #1309, which carries the minimal subset of this PR that the post-#1301 / post-#1302 codebase still needs.

When this PR was opened, two regressions interacted:

  1. The JSONP unattached branch returned {code: 0, message: "Broker not attached"}, which tripped sso.js into setting wu_sso_denied for 5 minutes on the very first subsite page-load.
  2. $broker->getAttachUrl() produced /sso-grant, which the dispatcher then routed to the unlistened action wu_sso_handle_sso_grant_grant.

(2) has since been fixed independently:

With those landed, the only remaining defect is the JSONP denial payload (1). #1309 fixes exactly that one line and adds source-code regression tests asserting verify: must-redirect is returned and that sso.js still handles it. The broader rewrite proposed here (redirecting through /sso instead of /sso-grant, plus the sso.js checkout-form skip, sunrise.php glob fallback, and wp-env Cypress fixture changes) is no longer required for this bug and was also blocking on a pre-existing wp-env environment issue in Cypress.

If the checkout-form skip and wp-env Cypress fixture rework are still wanted, they should each be opened as separate PRs scoped to their own bug.

@superdav42 superdav42 closed this May 28, 2026
superdav42 added a commit that referenced this pull request May 29, 2026
…1309)

The unattached-broker JSONP branch in handle_broker() returned
'{code: 0, message: "Broker not attached"}', which sso.js treats
as a denial (assets/js/sso.js lines 101-117). That set the
wu_sso_denied cookie for 5 minutes on the very first subsite
front-end page-load, silently disabling auto-SSO before it could
complete a single round-trip — even when the user was in fact
logged in on the main site.

The denial payload is the wrong signal here: the broker not being
attached on the JSONP probe is the expected state on the first
hit, not a failure. Return 'verify: must-redirect' instead so the
already-existing sso.js branch (assets/js/sso.js lines 93-95)
performs a top-level navigation to '<broker>/sso?return_url=...'.
That request re-enters handle_broker() as a regular page load and
falls through to the non-JSONP redirect, which after #1301 reaches
the cookie-less server handler via getAttachUrl() and completes the
SSO round-trip.

This is the minimal subset of #1297 that the post-#1301/#1302
codebase still needs. The /sso-grant routing fix (#1301) and the
scoped redirect-loop counter (#1302) already shipped, so the rest
of #1297's PHP changes are no longer required.

Regression tests in tests/WP_Ultimo/SSO/SSO_Test.php:
- test_handle_broker_source_jsonp_unattached_returns_must_redirect:
  locks in the new payload and forbids the legacy 'Broker not
  attached' denial.
- test_sso_js_handles_must_redirect_verify: confirms sso.js still
  recognises the must-redirect verify value.
@superdav42

Copy link
Copy Markdown
Collaborator Author

CLAIM_RELEASED reason=process_exit runner=superdav42 ts=2026-06-02T22:43:21Z aidevops_version=3.20.10 opencode_version=1.15.13 exit=0 session_count=1

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

Labels

origin:interactive Created by interactive user session

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant