Commit graph

860 commits

Author SHA1 Message Date
Johannes Rauh
93bf838f0b Replace cancel button with a link-styled back-to-login button
Replaces the cancel submit button on the 2FA challenge form with a minimal
link-styled button showing an arrow-left icon and "Back to login" label. The new
`.btn-back-to-login-link` style strips all button chrome (background, padding,
fixed height) and only underlines the label text on hover, making it visually
unobtrusive next to the primary verify button.
2026-05-05 14:09:16 +02:00
Johannes Rauh
0931ffaceb Reformat LoginForm::assemble() to inline addElement() call style 2026-04-30 10:59:24 +02:00
Johannes Rauh
b6374d49e8 Normalise naming conventions across 2FA form constants and element names
Rename constants and HTML element names to a consistent `twofactor_*/submit_*`
scheme, drop the now unused `SUBMIT_LOGIN` constant from `LoginForm`, and update
all references in the controllers.
2026-04-30 10:59:24 +02:00
Johannes Rauh
299e3fdae3 Reuse LoginForm::REDIRECT_URL in TwoFactorChallengeForm
Remove the duplicate constant and reference the one defined in LoginForm.
2026-04-30 10:59:24 +02:00
Johannes Rauh
ecec2bb99e fixup! Strip 2FA logic out of LoginForm 2026-04-30 10:59:24 +02:00
Johannes Rauh
aa5962a467 Log 2FA challenge and verification outcome
Emit an info line when a user is challenged and when they pass verification, and
a warning when the submitted token is rejected. All three log lines include the
method name so the 2FA backend in use is always visible in the logs.
2026-04-30 10:59:24 +02:00
Johannes Rauh
e31d5885cd Add TwoFactorState to centralise 2FA session handling
Introduce `TwoFactorState` as a dedicated wrapper around the session namespace
used during the two-factor authentication flow, replacing the scattered raw
session key accesses in `LoginForm`, `TwoFactorChallengeForm`, and
`AuthenticationController`.

`challenge()` opens the flow by storing the temporary user,
`completeChallenge()` closes it by cleaning up both the user and the pending
remember-me cookie, and `isChallenged()` provides a single readable guard for
the controller actions.
2026-04-30 10:59:24 +02:00
Johannes Rauh
b8ec333ac2 Strip 2FA logic out of LoginForm
`LoginForm` is now responsible only for the first authentication factor. After a
successful credential check, users with 2FA enrolled are redirected to
`authentication/twofactor` where `TwoFactorChallengeForm` takes over.

Remove `assembleTwoFactorElements()`, the `SUBMIT_VERIFY_2FA` /
`SUBMIT_CANCEL_2FA` / `TOKEN_INPUT` constants, the `getResponse()` helper, and
the `2fa_must_challenge` session branch from `assemble()`. The `onSuccess()`
switch-on-button is replaced with a simple login-only handler.

Also removes the `ON_SENT` handler from `AuthenticationController` that handled
the cancel button. It referenced `SUBMIT_CANCEL_2FA` which is removed here, and
the responsibility moves to `TwoFactorChallengeForm` in the next commit.
2026-04-30 10:59:24 +02:00
Johannes Rauh
5cb979cf33 Add TwoFactorChallengeForm for the 2FA verification step
Split the two-factor challenge out of `LoginForm` into its own standalone form
which handles token verification, remember-me cookie persistence, and the cancel
flow that purges the temporary session and returns to login.
2026-04-30 10:59:24 +02:00
Johannes Rauh
eeb1c14777 fixup! Rewrite authentication forms to ipl forms 2026-04-30 10:59:24 +02:00
Johannes Rauh
860bf995ab Move 2FA challenge form assembly back to LoginForm
Replace `assembleVerificationForm()` on the `TwoFactor` interface with
a generic `assembleTwoFactorElements()` method on `LoginForm` itself.
The token input, verify button, and cancel button are now owned by the
login form rather than each hook implementation.

Remove the `TOKEN_INPUT`, `SUBMIT_VERIFY_2FA`, and `SUBMIT_CANCEL_2FA`
constants from the `TwoFactor` interface and define them directly on
`LoginForm`. Update `AuthenticationController` to reference the new
location.
2026-04-30 10:59:24 +02:00
Johannes Rauh
7f0ea5b6c1 Scope enrollment elements in a per-method FieldsetElement
Wrap each hook's enrollment elements in a `FieldsetElement` named after the
method (via `getName()`). This scopes their POST keys to the method name, so
switching between methods in the type selector no longer cross-populates
elements that share the same name across implementations. This also ensures that
the implementation has only access to the elements added by itself.

Update `assembleEnrollmentFormElements()` and `enroll()` on the `TwoFactor`
interface to accept a `FieldsetElement` instead of a `CompatForm`. Also guard
the no-method-selected case with an early return so the enroll button is only
rendered once a method is chosen.
2026-04-30 10:59:24 +02:00
Johannes Rauh
f97f750313 Move enroll/unenroll responsibility to TwoFactorEnrollmentForm
Replace `assembleEnrollmentForm()` and `onSuccessEnrollmentForm()` on the
`TwoFactor` interface with three narrower contracts:

- `assembleEnrollmentFormElements()` — add only the method-specific credential
  fields. Submit buttons are no longer the implementation's concern.
- `enroll(CompatForm $form): bool` — read the submitted credential, verify it,
  and persist it. Return false on verification failure.
- `unenroll(): void` — remove the stored credential.

`TwoFactorEnrollmentForm` now owns the enroll and unenroll submit buttons, calls
the appropriate method, and handles notifications and redirects centrally. This
keeps all implementations free of button naming, redirect logic, and user-facing
messages.
2026-04-30 10:59:24 +02:00
Johannes Rauh
7ddf2e9893 Delegate 2FA verification form assembly to hook implementations
The form element name constants (`TOKEN_INPUT`, `SUBMIT_VERIFY_2FA`,
`SUBMIT_CANCEL_2FA`) are moved out of LoginForm onto TwoFactor so all call sites
share a single source of truth.

Replace the `LoginForm`-owned `assembleTwoFactorElements()` method and the
`getChallengeFormValidators()` interface stub with a proper
`assembleVerificationForm(CompatForm $form)` contract on `TwoFactor`. Each hook
implementation now builds its own verification UI. For the element names the
constants from `TwoFactor` have to be used, so the login form's success handler
can retrieve the values.
2026-04-30 10:59:24 +02:00
Johannes Rauh
d94e0f28a6 Redirect directly to the target URL after successful 2FA verification
Previously, a successful 2FA login redirected back to the login action via
`Url::fromRequest()`, which would then detect the authenticated session and
issue a second redirect to the actual destination. This was an unnecessary extra
round-trip.

The `SUBMIT_VERIFY_2FA` path now calls createRedirectUrl() directly, matching
the behaviour of the `SUBMIT_LOGIN` path for users without 2FA enrolled.
2026-04-30 10:59:24 +02:00
Johannes Rauh
8c5a11fe65 wip: hookable auth 2026-04-30 10:59:24 +02:00
Johannes Rauh
3e96d71445 wip: hookable config form 2026-04-30 10:59:24 +02:00
Johannes Rauh
53b01f2915 wip 2026-04-30 10:59:24 +02:00
Johannes Rauh
7348e8569a Add hint to store the secret for recovery
If the device running the authenticator app is lost, the secret can
be used to set up an authenticator app on another device. But for
that the secret has to be stored on another device when setting up
2FA.
2026-04-30 10:59:24 +02:00
Johannes Rauh
0194c75ded Remove permission user/two-factor-authentication
The user should not need to ask for more security.
2026-04-30 10:59:24 +02:00
Johannes Rauh
d443c4c633 Show only secret instead of whole otpauth url
This is more user friendly, because most authenticator apps just
want the secret for manual creation.
2026-04-30 10:59:24 +02:00
Johannes Rauh
e643dcc413 Add 'Download QR Code' action link
So the user can download the QR code to backup if they lose access
to their device.

The download currently does only work in chrome and firefox, but
not in safari. The reason is, that the action link uses
`ipl\Web\Url` which adds '///' between the schema and the base
path. The safari browser can't handle this.

'data:image/png;base64,...' is converted to
'data:///image/png;base64...'.
2026-04-30 10:59:24 +02:00
Johannes Rauh
268c6ddeee Add CsrfCounterMeasure to TwoFactorConfigForm 2026-04-30 10:59:24 +02:00
Johannes Rauh
5556cef0b0 Improve TwoFactorConfigForm
A new class `FakeFormElement` is introduced which integrates
`ValidHtml` into IPL Web `CompatForm`s. The following parts of the
`TwoFactorConfigForm` are now added to the Form via the new class:

- The QR code image
- The manual auth URL for TOTP

The manual auth URL is now wrapped by a copy-to-clipboard element,
so the user doesn’t have to select it manually. Additionally the
QR code image and the manual auth url have now descriptions.
2026-04-30 10:59:24 +02:00
Johannes Rauh
b1a3ec8770 Use one combined LoginForm
The form displays either the login inputs or the inputs to verify
the totp token depending on whether `'2fa_must_challenge_token'`
is set `true` in the session.
2026-04-30 10:59:24 +02:00
Johannes Rauh
e7dbc7afe3 Add TotpTokenValidator
It validates two things:
1. The token must only contain numbers
2. The token must have a specific length (this is configurable via
   the constructor, the default is 6)
2026-04-30 10:59:24 +02:00
Johannes Rauh
e6b97eeb90 Rewrite authentication forms to ipl forms
Removed description for `rememberme` input, because it wasn't
displayed anywhere.
2026-04-30 10:59:24 +02:00
Johannes Rauh
c164f2d90d Rewrite TwoFactorConfigForm to ipl form 2026-04-30 10:59:24 +02:00
Johannes Rauh
697b53af38 Correct array indentation 2026-04-30 10:59:24 +02:00
Johannes Rauh
599cc2349a Use more appropriate names 2026-04-30 10:59:24 +02:00
Johannes Rauh
3baa18fe87 Rename label to disable 2FA
Change the label to disable the 2FA from 'Remove TOTP Secret' to
'Disable 2FA'.
2026-04-30 10:59:24 +02:00
Johannes Rauh
9db39aa5c7 Don't show the stored secret anymore 2026-04-30 10:59:24 +02:00
Johannes Rauh
9ad7ec0525 Remove redundant object creation 2026-04-30 10:59:24 +02:00
Johannes Rauh
97b4f547e9 Remove unused TotpConfigForm::onRequest() 2026-04-30 10:59:24 +02:00
Johannes Rauh
598e038d2b Add function default values
...for `User::setTwoFactorEnabled()` and
`User::setTwoFactorSuccessful()`.
2026-04-30 10:59:24 +02:00
Johannes Rauh
c7bded30e6 Add function return types 2026-04-30 10:59:24 +02:00
Johannes Rauh
b0b9d3af4e Remove @inheritDoc 2026-04-30 10:59:24 +02:00
Johannes Rauh
ebf1b11f74 Add license headers for new files 2026-04-30 10:59:24 +02:00
Johannes Rauh
5e223322df Adjust remember me functionality to work with 2fa
If 2fa is enable the remember me cookie only gets set if the 2fa
authentication was successful. To log the user back in from the
cookie the 2fa will be skipped.
2026-04-30 10:59:24 +02:00
Johannes Rauh
3cc2bdc229 Verify 2fa token properly 2026-04-30 10:59:24 +02:00
Johannes Rauh
2471231d01 Remove session cookie for successful 2fa
Remove the cookie for a successful 2fa, because there is no need for it.
Instead set the 2fa directly to successful on the user.
2026-04-30 10:59:24 +02:00
Johannes Rauh
b34978533c Rename session key for 2fa challenge
Rename from 'must_challenge_2fa_token' to '2fa_must_challenge_token'
for a uniform naming schema in the future always starting with '2fa_'.
2026-04-30 10:59:24 +02:00
Johannes Rauh
1bcbff9c56 Rename text input to 'token' 2026-04-30 10:59:24 +02:00
Johannes Rauh
3cb93d7e27 Adjust 'Cancel' button
Add cancel submit button explicitly instead of setting it implicitly
by calling `setSubmitLabel('...')` to add `.btn-cancel` class so the
cancel button does not look like a primary button.
2026-04-30 10:59:24 +02:00
Johannes Rauh
50901edeae Refactor 2FA secret handling
Previously, a new 2FA secret was generated and stored temporarily in the
session. It was only persisted to the database when the user clicked the
"Save Changes" button. This behavior has been removed.

Now, the secret is written directly to the database once it has been
initially verified with a token. To generate a new secret if one
already exists, just remove the old one. There is no longer a separate
setting to toggle whether 2FA is enabled for a user — 2FA is considered
enabled if a secret exists for that user in the database.
2026-04-30 10:59:24 +02:00
Johannes Rauh
00438e54b5 Rename stuff
- 'TotpForm' to 'TotpConfigForm'
- 'Authentication\Totp' to 'IcingaTotp'
- 'Model\Totp' to 'TotpModel'
2026-04-30 10:59:24 +02:00
Jan Schuppik
121fe23c25 Initial implementation
From https://github.com/Icinga/icingaweb2/pull/5397
2026-04-30 10:59:24 +02:00
Eric Lippmann
97d3106b73 wip 2026-04-30 10:59:24 +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