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.
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.
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.
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.
`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.
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.
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.
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.
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.
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.
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.
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.
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...'.
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.
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.
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)
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.
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.
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.