Skip to content

feat(status): public /status discovery endpoint — advertise auth methods for mobile SSO#3664

Merged
marcelfolaron merged 2 commits into
masterfrom
feat/mobile-status-endpoint
Jul 19, 2026
Merged

feat(status): public /status discovery endpoint — advertise auth methods for mobile SSO#3664
marcelfolaron merged 2 commits into
masterfrom
feat/mobile-status-endpoint

Conversation

@gloriafolaron

Copy link
Copy Markdown
Contributor

The discovery half of the mobile OIDC bridge (#3637). Answers Q5 in docs/backend-mobile-auth-bridge-plan.md ("build the general status endpoint in core with auth-tiered disclosure").

Why

The mobile app calls GET /status (unauthenticated) at connect time to discover which login methods an instance offers, so it shows the right affordances — most importantly the OIDC "Sign in with SSO" flow. That flow ships gated: the button stays dormant until a backend advertises OIDC here. Without this endpoint, SSO never appears even when the bridge (#3637) is deployed. (Mobile side: connectionService::fetchPublicStatus.)

What

New app/Domain/Status/Controllers/Index.phpGET /status returns the safe public tier:

{
  "mobileAuthEnabled": true,
  "instanceName": "",            // config sitename
  "version": "3.9.8",             // AppSettings::appVersion
  "minAppVersion": null,
  "authMethods": ["password", "oidc"],   // password always; ldap/oidc when configured
  "oidcLoginUrl": "https://host/oidc/login",  // only when OIDC enabled
  "ssoProviders": []              // AdvancedAuth appends via the 'publicStatus' filter
}
  • AuthCheck: status / status.index allow-listed public.
  • Security — the whole point of the auth tiering: this is unauthenticated, so it deliberately omits plugin inventory / plugin versions / dbVersion. An unauthenticated inventory of those is a recon gift (CVE matching) — the plan doc flags this as "probably why a full public status endpoint wasn't built." Those stay behind auth (the existing authenticated mobileStatus). Plugins extend the public payload only via the publicStatus filter, which must preserve the safe contract.

Tests

IndexTest (3): password-only when no SSO configured, OIDC advertised + oidcLoginUrl when enabled, and never leaks a plugin/version inventory. AuthCheckTest: /status is a public route. Pint clean.

Note for review

  • authMethods reads config->oidcEnable / config->useLdap (mirrors Auth\Controllers\Login + Auth\Services\Auth).
  • ssoProviders is empty in core by design; AdvancedAuth's provider labels + login URLs come through the publicStatus filter (keeps core provider-agnostic — Q1 in the plan doc).

Follow-up to #3637 / #3662 / #3663.

🤖 Generated with Claude Code

…ods for mobile SSO

The mobile app calls GET /status (unauthenticated) at connect time to discover
which login methods an instance offers, so it can show the right affordances —
notably the OIDC 'Sign in with SSO' flow, which stays DORMANT in the app until a
backend advertises it here. This is the discovery half of the mobile OIDC bridge
(#3637); without it the SSO button never appears even when the bridge is deployed.

- app/Domain/Status/Controllers/Index.php — GET /status returns the SAFE public
  tier: authMethods (password / ldap / oidc, from config), oidcLoginUrl when OIDC
  is enabled, instanceName, core version, minAppVersion, and an empty ssoProviders
  that AdvancedAuth can populate via the 'publicStatus' filter. Deliberately OMITS
  plugin inventory / versions / dbVersion — unauthenticated that is a recon gift
  (CVE matching); those stay behind auth (the authenticated mobileStatus).
- AuthCheck: allow-list 'status' / 'status.index' as public.
- Tests: IndexTest (password-only, oidc-advertised + login URL, no-inventory-leak)
  + AuthCheck public-route test. Pint clean.

Answers Q5 in docs/backend-mobile-auth-bridge-plan.md. Follow-up to #3637/#3662/#3663.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtfhjPwUEEHJb4Jf7714eR
@gloriafolaron
gloriafolaron requested a review from a team as a code owner July 19, 2026 01:32
@gloriafolaron
gloriafolaron requested review from Copilot and marcelfolaron and removed request for a team July 19, 2026 01:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new unauthenticated /status discovery endpoint in the core backend to support the mobile app’s connect-time capability detection (notably advertising OIDC SSO availability), while keeping sensitive “inventory” details behind authenticated status endpoints.

Changes:

  • Introduces Status\Controllers\Index with GET /status returning a minimal, public-safe JSON payload (auth methods, instance name, version, optional oidcLoginUrl, and ssoProviders).
  • Allow-lists status / status.index as public routes in AuthCheck.
  • Adds unit tests to pin the public contract and validate the route is public.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
app/Domain/Status/Controllers/Index.php Implements the public /status discovery JSON response and plugin filter hook.
app/Core/Middleware/AuthCheck.php Marks status and status.index as public actions so /status is reachable unauthenticated.
tests/Unit/app/Domain/Status/Controllers/IndexTest.php Verifies payload contract (auth methods, oidcLoginUrl) and absence of sensitive inventory keys.
tests/Unit/app/Core/Middleware/AuthCheckTest.php Ensures status routes are treated as public by AuthCheck.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +80 to +82
$payload = self::dispatchFilter('publicStatus', $payload, ['request' => $this->request]);

return new JsonResponse($payload);

private IncomingRequest $request;

public function init(Environment $config, AppSettings $appSettings, IncomingRequest $request): void
$this->request = $request;
}

public function get(array $params): Response
…-filter + docblocks

- Defense in depth: after the publicStatus filter runs, strip a denylist of
  known-sensitive keys (plugins / plugin versions / dbVersion) so a misbehaving
  filter (or a future edit) can't leak the recon-risk inventory into this
  unauthenticated response, even if a plugin gets the public-safe contract wrong.
- Add init()/get() method docblocks to match the controller doc style.

IndexTest green (3 tests / 10 assertions); Pint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtfhjPwUEEHJb4Jf7714eR
@marcelfolaron

Copy link
Copy Markdown
Collaborator

Status: triaged · Priority: P2 · Next action: human review · Owner: @marcelfolaron

Picked up in the daily sweep — new since the last pass. This is the discovery half of the mobile OIDC bridge (follow-up to #3637 / #3662 / #3663), adding the unauthenticated GET /status endpoint.

One thing for the reviewer to keep front-of-mind, since it's the security crux of the PR: the endpoint is public, so the "safe tier" contract is load-bearing — verify it never leaks plugin inventory / plugin versions / dbVersion (the IndexTest "never leaks inventory" case is the one to trust here), and that any plugin extending the payload via the publicStatus filter can't widen that surface. No CI blockers observed. Advisory only — merge gate stays with you and @broskees.

@marcelfolaron
marcelfolaron merged commit 5eee264 into master Jul 19, 2026
11 of 12 checks passed
@marcelfolaron
marcelfolaron deleted the feat/mobile-status-endpoint branch July 19, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants