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