diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 33c7ebc..a1d2227 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -227,6 +227,29 @@ public function createSecondFactorAction(string $secret, string $secondFactorFro $this->redirect('index', 'Backend\Backend', 'Neos.Neos'); } + /** + * @throws StopActionException + */ + public function cancelLoginAction(): void + { + $this->secondFactorSessionStorageService->cancelLoginAttempt(); + + $this->redirect('index', 'Backend\Backend', 'Neos.Neos'); + } + + /** + * @return array + * @throws InvalidConfigurationTypeException + */ + protected function getNeosSettings(): array + { + $configurationManager = $this->objectManager->get(ConfigurationManager::class); + return $configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.Neos' + ); + } + /** * Check if the given token matches any registered second factor * @@ -247,17 +270,4 @@ private function enteredTokenMatchesAnySecondFactor(string $enteredSecondFactor, return false; } - - /** - * @return array - * @throws InvalidConfigurationTypeException - */ - protected function getNeosSettings(): array - { - $configurationManager = $this->objectManager->get(ConfigurationManager::class); - return $configurationManager->getConfiguration( - ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, - 'Neos.Neos' - ); - } } diff --git a/Classes/Domain/AuthenticationStatus.php b/Classes/Domain/AuthenticationStatus.php index b0c007d..c21da67 100644 --- a/Classes/Domain/AuthenticationStatus.php +++ b/Classes/Domain/AuthenticationStatus.php @@ -2,9 +2,8 @@ namespace Sandstorm\NeosTwoFactorAuthentication\Domain; -// FIXME: Refactor to enum once we only support PHP >= 8.1 -class AuthenticationStatus +enum AuthenticationStatus: string { - const AUTHENTICATION_NEEDED = 'AUTHENTICATION_NEEDED'; - const AUTHENTICATED = 'AUTHENTICATED'; + case AUTHENTICATION_NEEDED = 'AUTHENTICATION_NEEDED'; + case AUTHENTICATED = 'AUTHENTICATED'; } diff --git a/Classes/Domain/Model/Dto/SecondFactorDto.php b/Classes/Domain/Model/Dto/SecondFactorDto.php index d87d5e0..3016c06 100644 --- a/Classes/Domain/Model/Dto/SecondFactorDto.php +++ b/Classes/Domain/Model/Dto/SecondFactorDto.php @@ -17,17 +17,11 @@ public function __construct(SecondFactor $secondFactor, User $user = null) $this->secondFactor = $secondFactor; } - /** - * @return SecondFactor|string - */ public function getSecondFactor(): SecondFactor { return $this->secondFactor; } - /** - * @return User - */ public function getUser(): User { return $this->user; diff --git a/Classes/Domain/Model/SecondFactor.php b/Classes/Domain/Model/SecondFactor.php index 569d9d4..1260fae 100644 --- a/Classes/Domain/Model/SecondFactor.php +++ b/Classes/Domain/Model/SecondFactor.php @@ -3,10 +3,10 @@ namespace Sandstorm\NeosTwoFactorAuthentication\Domain\Model; use DateTime; -use Neos\Flow\Http\InvalidArgumentException; -use Neos\Flow\Security\Account; use Doctrine\ORM\Mapping as ORM; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Http\InvalidArgumentException; +use Neos\Flow\Security\Account; /** * Store the secrets needed for two factor authentication @@ -48,57 +48,48 @@ class SecondFactor */ protected DateTime|null $creationDate; - /** - * @return Account - */ + public static function typeToString(int $type): string + { + switch ($type) { + case self::TYPE_TOTP: + return 'OTP'; + case self::TYPE_PUBLIC_KEY: + return 'Public Key'; + default: + throw new InvalidArgumentException('Unsupported second factor type with index ' . $type); + } + } + public function getAccount(): Account { return $this->account; } - /** - * @param Account $account - */ public function setAccount(Account $account): void { $this->account = $account; } - /** - * @return int - */ public function getType(): int { return $this->type; } - /** - * @return string - */ - public function getTypeAsName(): string + public function setType(int $type): void { - return self::typeToString($this->getType()); + $this->type = $type; } - /** - * @param int $type - */ - public function setType(int $type): void + public function getTypeAsName(): string { - $this->type = $type; + return self::typeToString($this->getType()); } - /** - * @return string - */ public function getSecret(): string { return $this->secret; } - /** - * @param string $secret - */ public function setSecret(string $secret): void { $this->secret = $secret; @@ -118,16 +109,4 @@ public function __toString(): string { return $this->account->getAccountIdentifier() . " with " . self::typeToString($this->type); } - - public static function typeToString(int $type): string - { - switch ($type) { - case self::TYPE_TOTP: - return 'OTP'; - case self::TYPE_PUBLIC_KEY: - return 'Public Key'; - default: - throw new InvalidArgumentException('Unsupported second factor type with index ' . $type); - } - } } diff --git a/Classes/Http/Middleware/SecondFactorMiddleware.php b/Classes/Http/Middleware/SecondFactorMiddleware.php index ad1c922..3cb2755 100644 --- a/Classes/Http/Middleware/SecondFactorMiddleware.php +++ b/Classes/Http/Middleware/SecondFactorMiddleware.php @@ -20,7 +20,9 @@ class SecondFactorMiddleware implements MiddlewareInterface { + // TODO: duplication of information - actually there is quite some duplicated information here (like Controller Action names, Route config etc) 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'; @@ -146,6 +148,15 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $handler->handle($request); } + $routingMatchResults = $request->getAttribute(ServerRequestAttributes::ROUTING_RESULTS) ?? []; + $requestAction = $routingMatchResults['@action'] ?? ''; + $requestPackage = $routingMatchResults['@package'] ?? ''; + + // If login cancellation is requested - do it + if ($requestPackage === $this::SECOND_FACTOR_PACKAGE_KEY && $requestAction === 'cancelLogin') { + 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 edb75f4..2f1ef20 100644 --- a/Classes/Service/SecondFactorSessionStorageService.php +++ b/Classes/Service/SecondFactorSessionStorageService.php @@ -21,12 +21,12 @@ class SecondFactorSessionStorageService /** * @throws SessionNotStartedException */ - public function setAuthenticationStatus(string $authenticationStatus): void + public function setAuthenticationStatus(AuthenticationStatus $authenticationStatus): void { $this->sessionManager->getCurrentSession()->putData( self::SESSION_OBJECT_ID, [ - self::SESSION_OBJECT_AUTH_STATUS => $authenticationStatus, + self::SESSION_OBJECT_AUTH_STATUS => $authenticationStatus->value, ] ); } @@ -34,11 +34,11 @@ public function setAuthenticationStatus(string $authenticationStatus): void /** * @throws SessionNotStartedException */ - public function getAuthenticationStatus(): string + public function getAuthenticationStatus(): AuthenticationStatus { $storageObject = $this->sessionManager->getCurrentSession()->getData(self::SESSION_OBJECT_ID); - return $storageObject[self::SESSION_OBJECT_AUTH_STATUS]; + return AuthenticationStatus::from($storageObject[self::SESSION_OBJECT_AUTH_STATUS]); } /** @@ -50,4 +50,15 @@ public function initializeTwoFactorSessionObject(): void self::setAuthenticationStatus(AuthenticationStatus::AUTHENTICATION_NEEDED); } } + + /** + * When the user requests to cancel the login attempt, we destroy the current session to get out + * of the state between "logged in with username/password" and "fully authenticated with second factor". + */ + public function cancelLoginAttempt(): void + { + if ($this->sessionManager->getCurrentSession()->hasKey(self::SESSION_OBJECT_ID)) { + $this->sessionManager->getCurrentSession()->destroy('Termination requested by user.'); + } + } } diff --git a/Configuration/Routes.yaml b/Configuration/Routes.yaml index c7682a7..24cb3e9 100644 --- a/Configuration/Routes.yaml +++ b/Configuration/Routes.yaml @@ -36,3 +36,12 @@ '@action': 'createSecondFactor' '@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'] diff --git a/Resources/Private/Fusion/Presentation/Components/LoginSecondFactorStep.fusion b/Resources/Private/Fusion/Presentation/Components/LoginSecondFactorStep.fusion index 4506033..d4776ae 100644 --- a/Resources/Private/Fusion/Presentation/Components/LoginSecondFactorStep.fusion +++ b/Resources/Private/Fusion/Presentation/Components/LoginSecondFactorStep.fusion @@ -9,7 +9,11 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Component.LoginSecondFactorStep) } renderer = afx` - +
- + {I18n.id('login').value('Login').package('Neos.Neos').source('Main').translate()} - + {I18n.id('authenticating').value('Authenticating').package('Neos.Neos').source('Main').translate()} + + {I18n.id('cancel').value('Cancel').package('Neos.Neos').source('Main').translate()} + + + {I18n.id('cancel').value('Cancel').package('Neos.Neos').source('Main').translate()} +
diff --git a/Resources/Private/Fusion/Presentation/Pages/LoginSecondFactorPage.fusion b/Resources/Private/Fusion/Presentation/Pages/LoginSecondFactorPage.fusion index c6d5bfc..da098c4 100644 --- a/Resources/Private/Fusion/Presentation/Pages/LoginSecondFactorPage.fusion +++ b/Resources/Private/Fusion/Presentation/Pages/LoginSecondFactorPage.fusion @@ -90,9 +90,22 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Page.LoginSecondFactorPage) < pr