Skip to content

[bug] sMultisite SSO login fails when site_url differs from the multisite domain record #7

Description

When Evolution CMS is installed or updated via the CLI (php core/artisan make:site update, or any php artisan migrate --force), the seiger/smultisite migration seeds the default multisite record with domain = "localhost". After this, logging into /manager/ from the real domain (e.g. local-test.evo) triggers the sMultisite SSO flow, which redirects the browser to http://localhost/_ms-sso/?c=<jwt>.... Because localhost does not serve the CMS in a typical local-dev setup, the SSO handshake never completes and the manager becomes inaccessible.

Steps to Reproduce

  1. Install Evolution CMS 3.5.7
  2. Configure a vhost for a non-localhost domain, e.g. local-test.evo, pointing to the project root.
  3. Open http://local-test.evo/manager/ in a browser.
  4. Enter valid admin credentials and submit.

Observed: the browser is redirected to
http://localhost/_ms-sso/?c=eyJ...&return=http%3A%2F%2Flocal-test.evo%2F_ms-run%2F...
and the request fails (404 / connection refused), because Apache on localhost serves the OSPanel default page, not Evolution CMS.

Expected: login completes on local-test.evo and the manager welcome page is shown.

Root Cause

In the package migration
core/vendor/seiger/smultisite/database/migrations/2023_08_31_150405_create_s_multisites_table.php (line 36):

'domain' => get_by_key($_SERVER, 'HTTP_HOST', 'localhost'),

get_by_key (defined in core/functions/helper.php:72) returns the fallback 'localhost' whenever $_SERVER['HTTP_HOST'] is missing. Migrations executed through Artisan run in the CLI SAPI, where HTTP_HOST is not populated — confirmed: under CLI getenv('HTTP_HOST') returns false. As a result, the seeded multisite record is always domain = "localhost" when migrations are run from the console.

The web installer (which runs through install/index.php under a real HTTP request) populates HTTP_HOST correctly, so the issue is specific to the CLI update / migrate --force path.

How the SSO flow then breaks

The sMultisite plugin
(core/vendor/seiger/smultisite/plugins/sMultisitePlugin.php)
listens to the evolution.OnManagerLogin event (line 379). After a successful credential check:

  1. $home is derived from the current request host ($_SERVER['HTTP_HOST'], line 392), i.e. local-test.evo.
  2. $targets is computed as every domain from evo_s_multisites that is not equal to $home and not empty (lines 402–409). With the only seeded row being localhost, this yields ['localhost'].
  3. A signed SSO token is built for each target via ms_sso_token_make()
    (core/vendor/seiger/smultisite/functions/sso.php:75),
    embedding mode, sid, sid_native, uid, and host (= localhost) into a JWT.
  4. The run plan is persisted with ms_run_put() and the runId is stored in $_SESSION['ms_run_login'] (lines 427–431).
  5. On the next manager render, the OnManagerPageInit handler (line 533) calls ms_sso_resolve_start_url() (lines 77–161), which reads the plan and issues a Location: http://localhost/_ms-sso/?c=<jwt>&return=... redirect (line 547).
  6. localhost does not serve the CMS, so the receiver at SsoController::receiveLogin() (src/Controllers/SsoController.php:85) never runs. The user is left on a 404 / error page.

Suggested Fixes

  1. Migration default (preferred): In 2023_08_31_150405_create_s_multisites_table.php, detect the CLI context and either skip seeding (letting the admin configure the domain later) or read the host from core/custom/.env (e.g. a new APP_URL/SITE_URL key) instead of falling back to localhost. For example:

    $host = get_by_key($_SERVER, 'HTTP_HOST', '');
    if ($host === '' && function_exists('env')) {
        $envUrl = (string) env('APP_URL', '');
        $host = $envUrl !== '' ? (string) parse_url($envUrl, PHP_URL_HOST) : '';
    }
    'domain' => $host !== '' ? $host : 'localhost',
  2. Skip SSO when no real cross-domain targets exist: In OnManagerLogin, bail out when targets is empty OR matches the default/placeholder value. With only localhost in the table and $home already being localhost (or any other domain), the user is redirected to a host that is not actually a separate site, which is never useful.

  3. Guard against unreachable targets: Fall back to the standard (non-SSO) manager login when the configured targets are not part of allowedHosts() or when the receiver is unreachable.

Files Involved

File Role
core/vendor/seiger/smultisite/database/migrations/2023_08_31_150405_create_s_multisites_table.php:36 Seeds domain with HTTP_HOST/localhost fallback
core/functions/helper.php:72 get_by_key returns fallback when key is missing
core/src/Console/SiteUpdateCommand.php:248-252 Runs php artisan migrate --force under CLI SAPI
core/vendor/seiger/smultisite/plugins/sMultisitePlugin.php:379-439 OnManagerLogin — builds SSO run plan over all domains != current
core/vendor/seiger/smultisite/plugins/sMultisitePlugin.php:77-161 ms_sso_resolve_start_url() — issues the first redirect to _ms-sso on the target host
core/vendor/seiger/smultisite/plugins/sMultisitePlugin.php:533-549 OnManagerPageInit — fires the HTTP 303 redirect
core/vendor/seiger/smultisite/functions/sso.php:74-83 ms_sso_token_make() — embeds target host claim in JWT
core/vendor/seiger/smultisite/src/Controllers/SsoController.php:85-136 receiveLogin() — never reached because localhost does not serve the CMS
core/vendor/seiger/smultisite/src/Http/routes.php Routes for _ms-run / _ms-sso endpoints

Workaround (until fixed)

After a CLI install/upgrade, update the seeded multisite row to the real domain:

php core/artisan tinker
>>> \Seiger\sMultisite\Models\sMultisite::where('key','default')->update(['domain' => 'local-test.evo']);

or via SQL:

UPDATE evo_s_multisites SET domain = 'local-test.evo' WHERE `key` = 'default';

Then clear cookies for both localhost and the real domain, and re-open /manager/.


Environment: Evolution CMS 3.5.7 · seiger/smultisite (bundled) · PHP 8.4 · SQLite (also affects MySQL) · Local dev (OSPanel / Apache, analogous to Laragon/XAMPP).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions