diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 4947eab..e703991 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -278,6 +278,27 @@ public function createSecondFactorAction(string $secret, string $secondFactorFro $this->redirectToInterceptedRequestOrBackend(); } + /** + * Abort the login process from the 2FA challenge or enforced-setup screen: tear down + * the half-authenticated session and bounce the user back to the regular login screen. + * + * @throws StopActionException + */ + public function cancelLoginAction(): void + { + // Resolve the redirect target *before* destroying the session: the intercepted request + // lives in the (session-backed) security context and would be gone afterwards. Sending the + // user back to that same URI means the security entry point re-intercepts it, so the next + // login attempt resumes at the page they originally wanted instead of the backend default. + $redirectUri = $this->interceptedRequestOrBackendUri(); + + $this->secondFactorSessionStorageService->cancelLoginAttempt(); + + // The session (including the Neos backend authentication) is gone now, so this secured + // target bounces straight to the login screen via the security entry point. + $this->redirectToUri($redirectUri); + } + // ------------------------------------------------------------------ // WebAuthn XHR endpoints (registration ceremony) // ------------------------------------------------------------------ diff --git a/Classes/Http/Middleware/SecondFactorMiddleware.php b/Classes/Http/Middleware/SecondFactorMiddleware.php index d583ac7..7810827 100644 --- a/Classes/Http/Middleware/SecondFactorMiddleware.php +++ b/Classes/Http/Middleware/SecondFactorMiddleware.php @@ -21,6 +21,7 @@ class SecondFactorMiddleware implements MiddlewareInterface { const LOGGING_PREFIX = 'Sandstorm/NeosTwoFactorAuthentication: '; + const SECOND_FACTOR_PACKAGE_KEY = 'Sandstorm.NeosTwoFactorAuthentication'; const SECOND_FACTOR_LOGIN_URI = '/neos/second-factor-login'; const SECOND_FACTOR_SETUP_URI = '/neos/second-factor-setup'; @@ -167,6 +168,19 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $handler->handle($request); } + // Let a cancellation request through regardless of the 2FA state (challenge or enforced setup), + // so the controller can tear down the half-authenticated session instead of being redirected + // back to the 2FA screen by the checks below. + $routingMatchResults = $request->getAttribute(ServerRequestAttributes::ROUTING_RESULTS) ?? []; + if ( + ($routingMatchResults['@package'] ?? '') === self::SECOND_FACTOR_PACKAGE_KEY + && ($routingMatchResults['@action'] ?? '') === 'cancelLogin' + ) { + $this->log('Second factor login cancellation requested, skipping second factor.'); + + return $handler->handle($request); + } + $account = $this->securityContext->getAccount(); $isEnabledForAccount = $this->secondFactorService->isSecondFactorEnabledForAccount($account); diff --git a/Classes/Service/SecondFactorSessionStorageService.php b/Classes/Service/SecondFactorSessionStorageService.php index 84e6e77..17f1806 100644 --- a/Classes/Service/SecondFactorSessionStorageService.php +++ b/Classes/Service/SecondFactorSessionStorageService.php @@ -85,4 +85,18 @@ public function removeValue(string $key): void unset($data[$key]); $session->putData(self::SESSION_OBJECT_ID, $data); } + + /** + * Abort the login attempt: destroy the whole session to leave the in-between state + * of "authenticated with username/password, but not yet with the second factor". + * This also drops the Neos backend authentication, so the next request lands on the + * regular login screen again. + */ + public function cancelLoginAttempt(): void + { + $session = $this->sessionManager->getCurrentSession(); + if ($session->isStarted()) { + $session->destroy('Second factor login attempt cancelled by user.'); + } + } } diff --git a/Configuration/Routes.yaml b/Configuration/Routes.yaml index 92180b2..09433cc 100644 --- a/Configuration/Routes.yaml +++ b/Configuration/Routes.yaml @@ -37,6 +37,15 @@ '@format': 'html' httpMethods: ['POST'] +- name: 'Sandstorm Two Factor Authentication - Cancel Login' + uriPattern: 'neos/second-factor-cancel-login' + defaults: + '@package': 'Sandstorm.NeosTwoFactorAuthentication' + '@controller': 'Login' + '@action': 'cancelLogin' + '@format': 'html' + httpMethods: ['POST'] + - name: 'Sandstorm Two Factor Authentication - Setup TOTP' uriPattern: 'neos/second-factor-setup/totp' defaults: diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 8b7e951..4b6761f 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -13,7 +13,7 @@ Neos: icon: 'fas fa-qrcode' additionalResources: styleSheets: - - 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/Styles/Login.css' + - 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/Styles/Shared.css' - 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/Styles/Backend.css' javaScripts: - 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/index.js' @@ -27,6 +27,7 @@ Neos: backendLoginForm: stylesheets: + 'Sandstorm.NeosTwoFactorAuthentication:SharedStyles': 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/Styles/Shared.css' 'Sandstorm.NeosTwoFactorAuthentication:AdditionalStyles': 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/Styles/Login.css' scripts: 'Sandstorm.NeosTwoFactorAuthentication:AdditionalScripts': 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/index.js' diff --git a/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion b/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion index 6816c25..a4bfec3 100644 --- a/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion +++ b/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion @@ -8,9 +8,9 @@ Sandstorm.NeosTwoFactorAuthentication.BackendController.new = Sandstorm.NeosTwoF content = Sandstorm.NeosTwoFactorAuthentication:Component.MethodPicker { items = Neos.Fusion:DataStructure { totp = Neos.Fusion:DataStructure { + id = 'method.totp' label = ${I18n.id('method.totp.label').package('Sandstorm.NeosTwoFactorAuthentication').translate()} description = ${I18n.id('method.totp.description').package('Sandstorm.NeosTwoFactorAuthentication').translate()} - testId = 'select-method-totp' href = Neos.Fusion:UriBuilder { package = 'Sandstorm.NeosTwoFactorAuthentication' controller = 'Backend' @@ -18,9 +18,9 @@ Sandstorm.NeosTwoFactorAuthentication.BackendController.new = Sandstorm.NeosTwoF } } webauthn = Neos.Fusion:DataStructure { + id = 'method.webauthn' label = ${I18n.id('method.webauthn.label').package('Sandstorm.NeosTwoFactorAuthentication').translate()} description = ${I18n.id('method.webauthn.description').package('Sandstorm.NeosTwoFactorAuthentication').translate()} - testId = 'select-method-webauthn' href = Neos.Fusion:UriBuilder { package = 'Sandstorm.NeosTwoFactorAuthentication' controller = 'Backend' diff --git a/Resources/Private/Fusion/Integration/Controller/Backend/NewTotp.fusion b/Resources/Private/Fusion/Integration/Controller/Backend/NewTotp.fusion index 3e17bbf..739a9f9 100644 --- a/Resources/Private/Fusion/Integration/Controller/Backend/NewTotp.fusion +++ b/Resources/Private/Fusion/Integration/Controller/Backend/NewTotp.fusion @@ -126,7 +126,6 @@ Sandstorm.NeosTwoFactorAuthentication.BackendController.newTotp = Sandstorm.Neos