Commit graph

834 commits

Author SHA1 Message Date
Johannes Rauh
3c199b725b Use Generator to yield two-factor methods 2026-05-27 10:47:21 +02:00
Johannes Rauh
5cd6a2b90d Guard hook method call sites against implementation exceptions
The `TwoFactor` interface methods that contain logic are implemented by module
developers and may contain arbitrary third-party code. Without guards at the
call sites, an uncaught exception from a faulty implementation could crash the
whole application.

Each guard logs the exception with a confidential trace. `isEnrolled()` in
`loadEnrolled()` rethrows so the caller aborts rather than silently continuing.
`verify()` and `assembleEnrollmentFormElements()` show a user-facing message
and call `onError()` to keep the user on the form.

The existing `enroll()` and `unenroll()` catches are also updated: two separate
`Logger::error()` calls become one, and the user-visible messages gain
translation context.
2026-05-27 10:47:21 +02:00
Johannes Rauh
df4348081f Deduplicate remember-me revocation and redirect in onSuccess()
Both the enroll and unenroll paths ended with the same two statements.
Moving them after the switch eliminates the duplication.
2026-05-27 10:47:21 +02:00
Johannes Rauh
524cf5c286 Revoke remember-me cookies on unenrollment
Cookies issued while 2FA was active retain their validity after unenrollment
without this change. Revoking them forces a fresh login and prevents sessions
that were established under the enrolled requirement from persisting in a state
where that requirement no longer applies.
2026-05-27 10:47:21 +02:00
Johannes Rauh
dd1f570629 Catch and surface exceptions from enroll() and unenroll()
Previously both methods were declared to throw only `ConfigurationError`
and callers did not catch it. This removes the now-overly-narrow
`@throws ConfigurationError` declarations and wraps both call sites in
`try/catch(Throwable)`, letting the enrollment form log the full
confidential trace and show the exception message inline rather than
leaving the user with an unhandled error page.

The enroll false path is also updated: `Notification::error()` is
replaced with `$this->onError()` to keep error feedback consistent with
the exception path.
2026-05-27 10:47:21 +02:00
Johannes Rauh
240141a412 Remove success notifications 2026-05-27 10:47:21 +02:00
Johannes Rauh
efed65fb09 Disable the placeholder option in the 2FA method select
The empty placeholder `' - Please choose - '` could be re-selected after a real
method had been picked, resulting in `TwoFactorHook::fromName()` searching for
an empty string. Setting `disabledOptions` to `['']` prevents that by making the
placeholder unselectable once the user has moved past it.
2026-05-27 10:47:21 +02:00
Johannes Rauh
40a251d413 Require 2FA method selection and drop null fallback in onSuccess
With `required` set, the form will not pass validation when no method is
selected, so `getValue(static::METHOD)` in `onSuccess` can never return null.
The `?? ''` fallback passed to `TwoFactorHook::fromName` is dead code once
form validation runs.
2026-05-27 10:47:21 +02:00
Johannes Rauh
f7a38cb276 Apply fieldset module class after assembleEnrollmentFormElements
Setting the `icinga-module module-{name}` class on the fieldset before calling
`assembleEnrollmentFormElements` let hook implementations overwrite or clear it
by calling `setAttributes` on the fieldset themselves. Moving the assignment to
after assembly ensures the CSS scoping class is always present regardless of
what the hook does.

The guard on the fieldset name ensures a hook implementation cannot change the
fieldset name during assembly, which would break form submission handling.
2026-05-27 10:47:21 +02:00
Johannes Rauh
db36a68c4e Early return if no method is chosen
Instead of searching for a method named `''`.
2026-05-27 10:47:21 +02:00
Johannes Rauh
4d91eda501 Add module class to 2FA config fieldset in TwoFactorEnrollmentForm
The `icinga-module module-{name}` class pair lets the providing module scope
its CSS to its own fieldset without affecting other form elements.
2026-05-27 10:47:21 +02:00
Johannes Rauh
b99ec0df93 Add unique form ids to fix incorrect focus after login form submission
Without unique ids, `getCSSPath()` in `loader.js` produces the same path for the
submit buttons of both forms. When the login form is submitted and the container
transitions to the two-factor challenge form, `renderContentToContainer()`
restores focus to that path, incorrectly focusing the challenge form's submit
button instead of the token input.

Unique ids make the paths differ so no match is found and the autofocus class on
the token input takes effect as intended.
2026-05-27 10:47:21 +02:00
Johannes Rauh
215d2ec108 Avoid storing full remember-me cookie in session during 2FA challenge
Previously the `RememberMe` object (containing the AES-encrypted password and
the decryption key) was serialized directly into the PHP session while waiting
for the user to complete the 2FA challenge. Because PHP sessions are written
to disk in plaintext, this exposed the key and ciphertext in the same place,
sufficient to recover the user's password.

Fix by splitting the secret across the session and the database, mirroring the
design of the normal (non-2FA) remember-me flow:

- Call `persist()` at login time so the AES key goes to the database
  immediately, never touching the session.
- Store only the cookie value string (ciphertext + IV) in the session.
  The ciphertext is not exploitable without the key.
- After a successful 2FA challenge, reconstruct the `RememberMe` object via a
  new `RememberMe::fromCookieData()` factory that does a DB lookup by IV and
  restores the canonical expiry from the database row.
- Only then send the browser cookie, so the cookie never reaches the browser
  unless the second factor was verified.

Canceled challenges remove the created DB row, while abandoned challenges
leave an orphaned DB row which is cleaned up by the existing
`RememberMe::removeExpired()` mechanism.

`RememberMe::fromCookieData()` sets `$expiresAt` from the database row so
the browser cookie issued after 2FA inherits the expiry stored at login time
rather than receiving a fresh 30-day window computed at challenge-completion
time. The renewal path in `AuthenticationController::loginAction()` is
unaffected, because `renew()` constructs a new object via `fromCredentials()`.
2026-05-27 10:47:21 +02:00
Johannes Rauh
5284553a74 Handle unavailable 2FA method in TwoFactorChallengeForm
If the 2FA method a user was challenged for is no longer available when they
submit the token — e.g. the module providing it was disabled mid-session — show
an informative message instead of a fatal error. The message tells the user that
their method is gone, suggests contacting an administrator if unexpected, and
points them to 'Back to login' explaining that no second factor will be required
once the method is disabled.
2026-05-27 10:47:21 +02:00
Johannes Rauh
8f52c7639e Deduplicate redirect logic into LoginRedirect element
Add `LoginRedirect` that extends `HiddenElement` with a single `getUrl()` method
that encapsulates the three-step redirect resolution used in both login and
two-factor challenge form: fall back to `LoginForm::REDIRECT_URL` when the value
is empty or points to the logout action, then reject external URLs with a 400.

`LoginForm` and `TwoFactorChallengeForm` both replace their plain `'hidden'`
element with `LoginRedirect` and drop their identical `createRedirectUrl()`
methods in favor of `$this->getElement('redirect')->getUrl()`.

`AuthenticationController::loginAction()` had a pre-assembly call to
`$form->createRedirectUrl()` for the already-authenticated path. At that point
`handleRequest()` has not yet been called, so the form is not assembled and the
`redirect` element does not exist — calling `getElement()` would throw. That
path is also only reached when no `redirect` query param is present (the param
is handled explicitly on the line above), so the call always returned the
fallback URL anyway. It is replaced with a direct
`Url::fromPath(LoginForm::REDIRECT_URL)`.
2026-05-27 10:47:21 +02:00
Johannes Rauh
b1ced8769a Revoke remember-me cookies on 2FA enrollment
Add `RememberMe::removeAllByUsername()` which deletes all rows from
`icingaweb_rememberme` for a given user. Call it from
`TwoFactorEnrollmentForm::onSuccess()` after a successful enroll so that
pre-enrollment cookies on other devices cannot bypass the newly required
second factor.
2026-05-27 10:47:21 +02:00
Johannes Rauh
1b90b6c14f Add enrollment UI and two-factor tab to account navigation
`TwoFactorController::configAction()` renders `TwoFactorEnrollmentForm`, which
shows a method-selector dropdown (autosubmit), the method-specific fieldset
contributed by `assembleEnrollmentFormElements()`, and either an "Enroll" or
"Unenroll" submit button depending on current enrollment state.

`AccountController`, `MyDevicesController`, and `NavigationController` each gain
a "Two-Factor Auth" tab.
2026-05-27 10:47:21 +02:00
Johannes Rauh
ad268ab0fd Add 2FA challenge flow to LoginForm
After successful password authentication, check whether the user is enrolled in
any 2FA method via `TwoFactorHook::loadEnrolled()`. If enrolled, stores the user
in `TwoFactorState`, stashes the `RememberMe` cookie for later (if the checkbox
was checked), and redirects to `authentication/twofactor`. Users without a
registered 2FA credential continue through the existing authentication flow
unchanged.
2026-05-26 15:27:51 +02:00
Johannes Rauh
c225fc0b8a Add TwoFactorChallengeForm and login CSS
`TwoFactorChallengeForm` renders the second login step: a token input, a
"Verify" submit button, and a "Back to login" link button. `onSuccess()` calls
`TwoFactorHook::loadEnrolled()` and `TwoFactor::verify()` to verify the token,
calls `Auth::setAuthenticated()`, persists the `RememberMe` cookie if one was
stashed, completes the challenge, fires `AuthenticationHook::triggerLogin()`,
and redirects.

Add `.btn-back-to-login-link` to `login.less`. A link-styled submit button with
an arrow to the left with an underline on hover.
2026-05-26 15:27:51 +02:00
Johannes Rauh
fe282a304e Rewrite authentication login form to ipl CompatForm
Replace the legacy Zend-based `LoginForm` with an ipl `CompatForm`-based form:

- Add `CsrfCounterMeasure` and `FormUid` traits
- Replace `init()`/`createElements()` with `__construct()´/`assemble()` using
  inline ipl decorator objects instead of Zend decorator arrays
- Replace `getRedirectUrl()` with `createRedirectUrl()` because
  `getRedirectUrl()` would override `ipl\Html\Form::getRedirectUrl()`
    - use `hasBeenAssembled()` instead of `$this->created`
    - use `str_contains()` instead of `strpos()`
- Rewrite `onSuccess()` for `CompatForm`:
    - change visibility from `public` to `protected`
    - void return type
    - use `Icinga::app()->getResponse()` instead of `$this->getResponse()`
    - use `addMessage()` instead of `addError()`
    - call `setRedirectUrl()` explicitly
2026-05-18 12:14:07 +02:00
Johannes Meyer
0488bebd8c ConfirmRemovalForm: Drop autofocus entirely
Besides that this is a destructive operation which shouldn't
get automatic focus… our native way to adhere to WCAG standards
is by focusing the newly rendered container which suffices.
2026-05-08 11:45:22 +02:00
Johannes Meyer
acc0259464 login/setup: Use native autofocus on form inputs 2026-05-08 11:45:22 +02:00
Eric Lippmann
662de28f85 License source files as GPL-3.0-or-later
Add SPDX license headers and mark source files as GPL-3.0-or-later to
preserve the option to relicense under later GPL versions.
2026-03-26 17:49:26 +01:00
Markus Opolka
9774f11c2f
Normalize confirm remove (#5429)
Some checks are pending
L10n Update / update (push) Waiting to run
CI / PHP (push) Waiting to run
This PR normalized the use of the phrase "Confirm Removal" in forms that
confirm removal.

It also normalizes the use of the "cancel" icon.
2026-03-26 16:08:34 +01:00
Eric Lippmann
298f52456f Use Pdo\Mysql driver-specific constants
Replace deprecated `PDO::MYSQL_*` constant usage with the driver-specific
`Pdo\Mysql::ATTR_*` constants introduced in PHP 8.4.

This prepares the code for PHP 8.5, where accessing MySQL driver constants
through the generic `PDO` class is deprecated.

This change requires a compatibility shim on older PHP versions to provide
`Pdo\Mysql` for runtimes that do not expose the driver-specific class yet.  The
shim is provided in `ipl-sql`. As a consequence, the required version of the
Icinga PHP library has been raised.
2026-03-19 22:27:45 +01:00
Bastian Lederer
ea0ce32bc9 Change implicit nullable type declaration to explicit
Since PHP 8.4 implicitly nullable parameter types are deprecated.

Normalize scoped PHPDoc for nullable-parameter updates: use `?Type` instead of
`Type|null`, remove column alignment, and indent continuation lines with 2
spaces.

Co-authored-by: "Eric Lippmann <eric.lippmann@icinga.com>"
2026-03-10 16:40:27 +01:00
Johannes Meyer
451ec34775 migrations: Show a hint about wildcards in database names 2025-07-14 11:31:51 +02:00
Alexander Aleksandrovič Klimov
11251481dc ApplicationConfigForm: fix typo
Fix a consecutive "to" in a form element description.
2024-11-04 14:20:24 +01:00
Johannes Meyer
4a8d171aec migrations/index: Let the migrate all button submit the migration form 2023-09-19 14:37:55 +02:00
Yonas Habteab
2657f032dc Allow to automatically fix missing grants & elevalte database users
Co-authored-by: Johannes Meyer
2023-09-19 14:37:55 +02:00
Yonas Habteab
fdadba59ca Fix form with mulitple buttons doesn't recognize whether it's been submitted 2023-09-19 14:37:55 +02:00
Yonas Habteab
a9db85ed71 Introduce application/migrations permission 2023-09-19 14:37:55 +02:00
Yonas Habteab
2daa1447b7 Introduce MigrationForm class 2023-09-19 14:37:55 +02:00
Sukhwinder Dhillon
8739a9da73
Fix setup wizard (#5094) 2023-09-07 13:37:10 +02:00
Eric Lippmann
1b91a93e34 Make host optional for oci database resources
`oci` uses Zend's `Oracle` adapter, which does not use this setting at
all.
2023-09-07 08:51:22 +02:00
raviks789
4e8107c231 RoleForm: Replace static inline styles with css class
The static inline style for resetriction text element is replaced with css class `role-restriction-text`
to avoid Content-Security-Policy violations.
2023-08-28 16:40:37 +02:00
Eric Lippmann
0bac6cfe07 Config: Add setting to enable strict CSP 2023-08-24 16:02:06 +02:00
Sukhwinder Dhillon
ee9d139a3a PhpDoc: Fix incorrect @param and @return type hints 2023-08-23 10:53:15 +02:00
Sukhwinder Dhillon
4d3765b22f PreferenceForm: Fix Variable '$disabled' is probably undefined
- Remove obsolete `value` attribute
2023-08-23 10:53:14 +02:00
Sukhwinder Dhillon
143347634f NavigationConfigForm: Fix Variable '$classPath' is probably undefined 2023-08-23 10:53:14 +02:00
Sukhwinder Dhillon
c501e3e2e1 AddMemberForm: Fix Variable '$userName' is probably undefined 2023-08-23 10:53:14 +02:00
Sukhwinder Dhillon
0198054943 CreateMembershipForm: Fix Variable '$groupName' is probably undefined 2023-08-23 10:53:14 +02:00
raviks789
89630bf0de ResourceConfigForm: Import class Zend_Form_Element
Class `Zend_Form_Element` is the return type of the method `getForceCreationCheckBox()`. Hence the class should be imported.
2023-08-23 10:53:14 +02:00
raviks789
2eb3b6c405 ResourceConfigForm: Fix return type for edit() and remove() methods 2023-08-23 10:53:14 +02:00
raviks789
eff262cafd PreferenceForm: Use correct number of arguments for method Preferences::get() 2023-08-23 10:53:14 +02:00
Johannes Meyer
c40cfb41a8 RoleForm: Force a suffix for all element names
fixes #4973
2023-06-22 15:04:19 +02:00
Alexander A. Klimov
3784fe80b7 LoggingConfigForm: on Docker default to webserver log 2023-01-24 11:23:34 +01:00
Johannes Meyer
ec7fb82a94 login: Don't redirect to external resources
fixes #4945
2022-12-07 11:54:45 +01:00
Alexander A. Klimov
a3100d378b SshResourceForm: fix XSS by escaping user-defined resource name
in the tooltip of the message shown instead of the private key.
2022-12-01 11:35:11 +01:00
Johannes Meyer
4d0e42787a ConfigForm: Remove empty sections
fixes #4939
2022-11-02 16:07:53 +01:00