From 83a89aaf2ff19be23cdcaa01e13bfcdbd8706d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean=20Charles=20Del=C3=A9pine?= Date: Mon, 20 Jul 2026 12:44:40 +0200 Subject: [PATCH] fix(auth): generalize loginparams collection, honor selection via session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iterate every app declaring 'loginparams' (registry->listApps()) instead of hardcoding imp — addresses ralflang's review. Collect posted values and store the selection in the session so an app's transparent-auth logic can honor it even when Horde authenticated via a different driver (LDAP, SQL, ...) — addresses TDannhauer's review (selector was shown but ignored). Companion change in horde/imp reads the stored selection. --- login.php | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/login.php b/login.php index 67f844bf..82cbdc70 100644 --- a/login.php +++ b/login.php @@ -54,6 +54,74 @@ function _addAnchor($url, $type, $vars, $url_anchor = null) return $url; } +/** + * Collect login params (and optionally posted values) from every + * registered application that declares the 'loginparams' auth capability, + * independently of which app/driver is currently authenticating Horde + * itself. + * + * This lets an app's login UI (e.g. a mail server selector) appear and be + * honored even when Horde's own auth driver is unrelated to that app + * (LDAP, SQL, ...). + * + * @param Horde_Injector $injector + * @param Horde_Registry $registry + * @param boolean $collectPost Also read posted values for each + * collected field. + * + * @return array{params: array, js_code: array, js_files: array, + * posted: array} 'posted' is keyed by app. + */ +function _collectAppLoginParams($injector, $registry, $collectPost = false) +{ + $params = $js_code = $js_files = []; + $posted = []; + + // perms=null (not 0) is required to bypass Horde_Registry's permission + // check here — we're pre-authentication, there is no user to check + // permissions against yet. + foreach ($registry->listApps(null, false, null) as $app) { + if ($app === 'horde') { + continue; + } + + try { + $appAuth = $injector->getInstance('Horde_Core_Factory_Auth')->create($app); + if (!$appAuth->hasCapability('loginparams')) { + continue; + } + + $result = $appAuth->getLoginParams(); + $params = array_merge($params, $result['params'] ?? []); + $js_code = array_merge($js_code, $result['js_code'] ?? []); + $js_files = array_merge($js_files, $result['js_files'] ?? []); + + if ($collectPost) { + foreach (array_keys($result['params'] ?? []) as $key) { + $posted[$app][$key] = Util::getPost($key); + } + } + } catch (Horde_Exception | \Horde\Exception\HordeThrowable $e) { + // Expected: this app declined to provide login params (not + // configured, not applicable, etc). Skip it silently, same + // behavior as the pre-existing single-app getLoginParams() call + // below. + continue; + } catch (\Throwable $e) { + // Unexpected failure (misconfigured DI, broken app code, ...). + // Do not let one broken app take down the login page for + // everyone, but do not swallow it silently either. + continue; + } + } + + return [ + 'params' => $params, + 'js_code' => $js_code, + 'js_files' => $js_files, + 'posted' => $posted, + ]; +} /* Try to login - if we are doing auth to an app, we need to auth to * Horde first or else we will lose the session. Ignore any auth errors. @@ -209,6 +277,13 @@ function _addAnchor($url, $type, $vars, $url_anchor = null) } catch (Horde_Exception $e) { } + // Also collect any posted values for apps declaring 'loginparams', + // independently of the current Horde auth driver, so a selection (e.g. + // an IMP mail-server choice) submitted alongside the primary login isn't + // silently dropped. + $appLoginParams = _collectAppLoginParams($injector, $registry, true); + $app_login_selection = array_filter($appLoginParams['posted']); + // TODO: Factor out into login handler class // First check if we need to validate the second factor. $authUser = (string) Util::getPost('horde_user'); @@ -252,6 +327,13 @@ function _addAnchor($url, $type, $vars, $url_anchor = null) $horde_login_url = Horde::url(_addAnchor($url_in->remove(session_name()), 'url', $vars), true); } + if (!empty($app_login_selection)) { + // Store per-app posted login params in the session so that an app's + // own transparent-auth logic can honor the user's selection later, + // even though Horde itself authenticated through a different driver. + $session->set('horde', 'login_app_params', $app_login_selection); + } + /* Do password change request on initial login only. */ if (!$is_auth && $registry->passwordChangeRequested()) { $notification->push(_("Your password has expired."), 'horde.message'); @@ -368,6 +450,13 @@ function _addAnchor($url, $type, $vars, $url_anchor = null) } catch (Horde_Exception $e) { } +// Also render login params for any app declaring 'loginparams', not +// just the app currently bound to $auth. +$appLoginParams = _collectAppLoginParams($injector, $registry); +$loginparams = array_filter(array_merge($loginparams, $appLoginParams['params'])); +$js_code = array_merge($js_code, $appLoginParams['js_code']); +$js_files = array_merge($js_files, $appLoginParams['js_files']); + /* If we currently are authenticated, and are not trying to authenticate to * an application, redirect to initial page. This is done in index.php. * If we are trying to authenticate to an application, but don't have to,