config = $config; $this->appSettings = $appSettings; $this->request = $request; $this->plugins = $plugins; } /** * Return the public discovery payload: enabled auth methods, the OIDC login * URL (when enabled), instance name, core version, and min app version — the * safe unauthenticated tier only. Never plugin inventory / versions / db * version (see the class-level security note). */ public function get(array $params): Response { $oidcEnabled = (bool) $this->config->oidcEnable; $ldapEnabled = $this->config->useLdap === true && extension_loaded('ldap'); // Mobile auth is an AdvancedAuth capability — the mobile connection points // (getToken, and the OIDC mint bridge) require the plugin. Only advertise // mobile OIDC when AdvancedAuth is installed, so a core-only instance never // offers a login the mint endpoint (Oidc\Controllers\Mobile) would refuse. // NOTE: Cloud is assumed to ship AdvancedAuth, so this predicate covers it; // confirm before release. $mobileGate = $this->plugins->isEnabled('AdvancedAuth'); // password is always available; ldap/oidc only when configured. $authMethods = ['password']; if ($ldapEnabled) { $authMethods[] = 'ldap'; } if ($oidcEnabled && $mobileGate) { $authMethods[] = 'oidc'; } $payload = [ 'mobileAuthEnabled' => $mobileGate, 'instanceName' => (string) ($this->config->sitename ?: 'Leantime'), 'version' => $this->appSettings->appVersion, 'minAppVersion' => null, 'authMethods' => $authMethods, 'ssoProviders' => [], ]; if ($oidcEnabled && $mobileGate) { // Generic-OIDC login initiation URL — advertised for mobile only when // AdvancedAuth is installed. The app opens this in the system auth // browser; the mobile branch is triggered by its own query params // (see Oidc\Controllers\Login). $payload['oidcLoginUrl'] = $this->request->getSchemeAndHttpHost().'/oidc/login'; } // AdvancedAuth (or other plugins) can append named SSO providers to the // PUBLIC-safe payload (labels + login URLs only) via this filter, without // core knowing about them. Filter handlers MUST preserve the public-safe // contract — never add secrets, plugin inventory, or versions here. $payload = self::dispatchFilter('publicStatus', $payload, ['request' => $this->request]); // Defense in depth: this endpoint is unauthenticated, so strip any // known-sensitive keys a misbehaving filter (or a future edit) might have // added. The recon-risk inventory must NEVER reach an unauthenticated // caller, even if a plugin gets the contract wrong. foreach (self::SENSITIVE_KEYS as $sensitive) { unset($payload[$sensitive]); } return new JsonResponse($payload); } }