diff --git a/.gitignore b/.gitignore index e6abe13..2ec0263 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,8 @@ vendor/ # composer composer.lock +# node +node_modules/ + # IDEs .idea/ diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php index 5e99390..6f4c75f 100644 --- a/Classes/Controller/BackendController.php +++ b/Classes/Controller/BackendController.php @@ -30,59 +30,51 @@ class BackendController extends AbstractModuleController { /** - * @var SecondFactorRepository * @Flow\Inject */ - protected $secondFactorRepository; + protected SecondFactorRepository $secondFactorRepository; /** - * @var Context * @Flow\Inject */ - protected $securityContext; + protected Context $securityContext; /** - * @var PartyService * @Flow\Inject */ - protected $partyService; + protected PartyService $partyService; /** * @Flow\Inject - * @var FlashMessageService */ - protected $flashMessageService; + protected FlashMessageService $flashMessageService; /** * @Flow\Inject - * @var SecondFactorSessionStorageService */ - protected $secondFactorSessionStorageService; + protected SecondFactorSessionStorageService $secondFactorSessionStorageService; /** * @Flow\Inject - * @var TOTPService */ - protected $tOTPService; + protected TOTPService $tOTPService; /** * @Flow\Inject - * @var Translator */ - protected $translator; + protected Translator $translator; protected $defaultViewObjectName = FusionView::class; /** * @Flow\Inject - * @var SecondFactorService */ - protected $secondFactorService; + protected SecondFactorService $secondFactorService; /** * used to list all second factors of the current user */ - public function indexAction() + public function indexAction(): void { $account = $this->securityContext->getAccount(); @@ -174,10 +166,6 @@ public function createAction(string $secret, string $secondFactorFromApp): void $this->redirect('index'); } - /** - * @param SecondFactor $secondFactor - * @return void - */ public function deleteAction(SecondFactor $secondFactor): void { $account = $this->securityContext->getAccount(); @@ -227,7 +215,6 @@ public function deleteAction(SecondFactor $secondFactor): void } /** - * @return array * @throws InvalidConfigurationTypeException */ protected function getNeosSettings(): array diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 33c7ebc..411f5c8 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -2,10 +2,6 @@ namespace Sandstorm\NeosTwoFactorAuthentication\Controller; -/* - * This file is part of the Sandstorm.NeosTwoFactorAuthentication package. - */ - use Neos\Error\Messages\Message; use Neos\Flow\Annotations as Flow; use Neos\Flow\Configuration\ConfigurationManager; @@ -15,15 +11,14 @@ use Neos\Flow\Mvc\Exception\StopActionException; use Neos\Flow\Mvc\FlashMessage\FlashMessageService; use Neos\Flow\Persistence\Exception\IllegalObjectTypeException; -use Neos\Flow\Security\Account; use Neos\Flow\Security\Context as SecurityContext; use Neos\Flow\Session\Exception\SessionNotStartedException; use Neos\Fusion\View\FusionView; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; use Sandstorm\NeosTwoFactorAuthentication\Domain\AuthenticationStatus; -use Sandstorm\NeosTwoFactorAuthentication\Domain\Model\SecondFactor; use Sandstorm\NeosTwoFactorAuthentication\Domain\Repository\SecondFactorRepository; +use Sandstorm\NeosTwoFactorAuthentication\Service\SecondFactorService; use Sandstorm\NeosTwoFactorAuthentication\Service\SecondFactorSessionStorageService; use Sandstorm\NeosTwoFactorAuthentication\Service\TOTPService; @@ -35,52 +30,49 @@ class LoginController extends ActionController protected $defaultViewObjectName = FusionView::class; /** - * @var SecurityContext * @Flow\Inject */ - protected $securityContext; + protected SecurityContext $securityContext; + + /** + * @Flow\Inject + */ + protected DomainRepository $domainRepository; /** - * @var DomainRepository * @Flow\Inject */ - protected $domainRepository; + protected SiteRepository $siteRepository; /** * @Flow\Inject - * @var SiteRepository */ - protected $siteRepository; + protected FlashMessageService $flashMessageService; /** * @Flow\Inject - * @var FlashMessageService */ - protected $flashMessageService; + protected SecondFactorRepository $secondFactorRepository; /** - * @var SecondFactorRepository * @Flow\Inject */ - protected $secondFactorRepository; + protected SecondFactorSessionStorageService $secondFactorSessionStorageService; /** * @Flow\Inject - * @var SecondFactorSessionStorageService */ - protected $secondFactorSessionStorageService; + protected TOTPService $tOTPService; /** * @Flow\Inject - * @var TOTPService */ - protected $tOTPService; + protected SecondFactorService $secondFactorService; /** * @Flow\Inject - * @var Translator */ - protected $translator; + protected Translator $translator; /** * This action decides which tokens are already authenticated @@ -112,7 +104,7 @@ public function checkSecondFactorAction(string $otp): void { $account = $this->securityContext->getAccount(); - $isValidOtp = $this->enteredTokenMatchesAnySecondFactor($otp, $account); + $isValidOtp = $this->secondFactorService->validateOtpForAccount($otp, $account); if ($isValidOtp) { $this->secondFactorSessionStorageService->setAuthenticationStatus(AuthenticationStatus::AUTHENTICATED); @@ -175,9 +167,6 @@ public function setupSecondFactorAction(?string $username = null): void } /** - * @param string $secret - * @param string $secondFactorFromApp - * @return void * @throws IllegalObjectTypeException * @throws SessionNotStartedException * @throws StopActionException @@ -228,28 +217,16 @@ public function createSecondFactorAction(string $secret, string $secondFactorFro } /** - * Check if the given token matches any registered second factor - * - * @param string $enteredSecondFactor - * @param Account $account - * @return bool + * @throws StopActionException */ - private function enteredTokenMatchesAnySecondFactor(string $enteredSecondFactor, Account $account): bool + public function cancelLoginAction(): void { - /** @var SecondFactor[] $secondFactors */ - $secondFactors = $this->secondFactorRepository->findByAccount($account); - foreach ($secondFactors as $secondFactor) { - $isValid = TOTPService::checkIfOtpIsValid($secondFactor->getSecret(), $enteredSecondFactor); - if ($isValid) { - return true; - } - } + $this->secondFactorSessionStorageService->cancelLoginAttempt(); - return false; + $this->redirect('index', 'Backend\Backend', 'Neos.Neos'); } /** - * @return array * @throws InvalidConfigurationTypeException */ protected function getNeosSettings(): array @@ -260,4 +237,5 @@ protected function getNeosSettings(): array 'Neos.Neos' ); } + } diff --git a/Classes/Controller/ReloginApiController.php b/Classes/Controller/ReloginApiController.php new file mode 100644 index 0000000..a86e3c2 --- /dev/null +++ b/Classes/Controller/ReloginApiController.php @@ -0,0 +1,98 @@ + + */ + protected $supportedMediaTypes = ['application/json']; + + /** + * @Flow\Inject + */ + protected SecurityContext $securityContext; + + /** + * @Flow\Inject + */ + protected SecondFactorService $secondFactorService; + + /** + * @Flow\Inject + */ + protected SecondFactorSessionStorageService $secondFactorSessionStorageService; + + /** + * Returns whether the currently authenticated account requires a second factor. + */ + public function secondFactorStatusAction(): void + { + $account = $this->securityContext->getAccount(); + if ($account === null) { + $this->response->setStatusCode(401); + $this->view->assign('value', ['error' => 'Not authenticated']); + return; + } + + $required = $this->secondFactorService->isSecondFactorEnabledForAccount($account); + $this->view->assign('value', ['secondFactorRequired' => $required]); + } + + /** + * Validates the submitted OTP and sets the session to AUTHENTICATED on success. + * + * CSRF protection is skipped because after session timeout + re-login, the CSRF token + * context may not match. The endpoint is still protected by session authentication + * and policy authorization (Policy.yaml). + * + * @Flow\SkipCsrfProtection + */ + public function verifySecondFactorAction(): void + { + $account = $this->securityContext->getAccount(); + if ($account === null) { + $this->response->setStatusCode(401); + $this->view->assign('value', ['success' => false, 'error' => 'Not authenticated']); + return; + } + + // Read the raw body — rewind the stream first since Flow may have already read it + $httpRequest = $this->request->getHttpRequest(); + $bodyStream = $httpRequest->getBody(); + $bodyStream->rewind(); + $body = json_decode($bodyStream->getContents(), true); + $otp = $body['otp'] ?? ''; + + if ($otp === '') { + $this->response->setStatusCode(400); + $this->view->assign('value', ['success' => false, 'error' => 'Missing OTP']); + return; + } + + $isValid = $this->secondFactorService->validateOtpForAccount($otp, $account); + + if ($isValid) { + $this->secondFactorSessionStorageService->setAuthenticationStatus(AuthenticationStatus::AUTHENTICATED); + $this->view->assign('value', ['success' => true]); + return; + } + + $this->response->setStatusCode(401); + $this->view->assign('value', ['success' => false, 'error' => 'Invalid OTP']); + } +} 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..223bb63 100644 --- a/Classes/Domain/Model/Dto/SecondFactorDto.php +++ b/Classes/Domain/Model/Dto/SecondFactorDto.php @@ -5,31 +5,11 @@ use Neos\Neos\Domain\Model\User; use Sandstorm\NeosTwoFactorAuthentication\Domain\Model\SecondFactor; -class SecondFactorDto +readonly class SecondFactorDto { - protected SecondFactor $secondFactor; - - protected User $user; - - public function __construct(SecondFactor $secondFactor, User $user = null) - { - $this->user = $user; - $this->secondFactor = $secondFactor; - } - - /** - * @return SecondFactor|string - */ - public function getSecondFactor(): SecondFactor - { - return $this->secondFactor; - } - - /** - * @return User - */ - public function getUser(): User + public function __construct( + public SecondFactor $secondFactor, + public User $user) { - return $this->user; } } diff --git a/Classes/Domain/Model/SecondFactor.php b/Classes/Domain/Model/SecondFactor.php index 569d9d4..25fcafa 100644 --- a/Classes/Domain/Model/SecondFactor.php +++ b/Classes/Domain/Model/SecondFactor.php @@ -3,10 +3,11 @@ 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; +use Sandstorm\NeosTwoFactorAuthentication\Domain\SecondFactorType; /** * Store the secrets needed for two factor authentication @@ -18,9 +19,6 @@ class SecondFactor // For apps like the google authenticator const TYPE_TOTP = 1; - // using the webauthn standard supported by most modern browsers - const TYPE_PUBLIC_KEY = 2; - /** * @var Account * @ORM\ManyToOne @@ -48,57 +46,44 @@ class SecondFactor */ protected DateTime|null $creationDate; - /** - * @return Account - */ + public static function typeToString(int $type): string + { + return match ($type) { + self::TYPE_TOTP => 'OTP', + 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 +103,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/Domain/Repository/SecondFactorRepository.php b/Classes/Domain/Repository/SecondFactorRepository.php index 08aa1fe..120c107 100644 --- a/Classes/Domain/Repository/SecondFactorRepository.php +++ b/Classes/Domain/Repository/SecondFactorRepository.php @@ -29,6 +29,7 @@ public function createSecondFactorForAccount(string $secret, Account $account): $secondFactor = new SecondFactor(); $secondFactor->setAccount($account); $secondFactor->setSecret($secret); + // Hard set for now, as we only support TOTP right now $secondFactor->setType(SecondFactor::TYPE_TOTP); $secondFactor->setCreationDate(new \DateTime()); $this->add($secondFactor); diff --git a/Classes/Http/Middleware/SecondFactorMiddleware.php b/Classes/Http/Middleware/SecondFactorMiddleware.php index ad1c922..33a903c 100644 --- a/Classes/Http/Middleware/SecondFactorMiddleware.php +++ b/Classes/Http/Middleware/SecondFactorMiddleware.php @@ -20,37 +20,35 @@ 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'; + const SECOND_FACTOR_API_PREFIX = '/neos/api/second-factor-'; /** * @Flow\Inject - * @var SecurityContext */ - protected $securityContext; + protected SecurityContext $securityContext; /** * @Flow\Inject - * @var ActionRequestFactory */ - protected $actionRequestFactory; + protected ActionRequestFactory $actionRequestFactory; /** - * @Flow\Inject(name="Neos.Flow:SecurityLogger") - * @var LoggerInterface + * @Flow\Inject(name="Neos.Flow:SecurityLogger", lazy=false) */ - protected $securityLogger; + protected LoggerInterface $securityLogger; /** * @Flow\Inject - * @var SecondFactorSessionStorageService */ - protected $secondFactorSessionStorageService; + protected SecondFactorSessionStorageService $secondFactorSessionStorageService; /** * @Flow\Inject - * @var SecondFactorService */ protected SecondFactorService $secondFactorService; @@ -118,7 +116,6 @@ class SecondFactorMiddleware implements MiddlewareInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $authenticationTokens = $this->securityContext->getAuthenticationTokens(); // 1. Skip, if no authentication tokens are present, because we're not on a secured route. @@ -146,6 +143,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); @@ -162,6 +168,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $authenticationStatus = $this->secondFactorSessionStorageService->getAuthenticationStatus(); + // Allow 2FA API routes through (used by the Neos UI relogin plugin) + $requestPath = $request->getUri()->getPath(); + if (str_starts_with($requestPath, self::SECOND_FACTOR_API_PREFIX)) { + // Request will be handled by ReloginApiController + return $handler->handle($request); + } + // 5. Skip, if second factor is already authenticated. if ($authenticationStatus === AuthenticationStatus::AUTHENTICATED) { $this->log('Second factor already authenticated.'); diff --git a/Classes/Service/SecondFactorService.php b/Classes/Service/SecondFactorService.php index 57fd988..60bc2fc 100644 --- a/Classes/Service/SecondFactorService.php +++ b/Classes/Service/SecondFactorService.php @@ -4,33 +4,33 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Account; +use Sandstorm\NeosTwoFactorAuthentication\Domain\Model\SecondFactor; use Sandstorm\NeosTwoFactorAuthentication\Domain\Repository\SecondFactorRepository; +/** + * @Flow\Scope("singleton") + */ class SecondFactorService { /** * @Flow\InjectConfiguration(path="enforceTwoFactorAuthentication") - * @var bool */ - protected $enforceTwoFactorAuthentication; + protected bool $enforceTwoFactorAuthentication; /** * @Flow\InjectConfiguration(path="enforce2FAForAuthenticationProviders") - * @var array */ - protected $enforce2FAForAuthenticationProviders; + protected array $enforce2FAForAuthenticationProviders; /** * @Flow\InjectConfiguration(path="enforce2FAForRoles") - * @var array */ - protected $enforce2FAForRoles; + protected array $enforce2FAForRoles; /** * @Flow\Inject - * @var SecondFactorRepository */ - protected $secondFactorRepository; + protected SecondFactorRepository $secondFactorRepository; /** * Check if the second factor is enforced for the given account. @@ -64,6 +64,22 @@ public function isSecondFactorEnabledForAccount(Account $account): bool return count($factors) > 0; } + /** + * Check if the given OTP matches any registered second factor for the account. + */ + public function validateOtpForAccount(string $otp, Account $account): bool + { + /** @var SecondFactor[] $secondFactors */ + $secondFactors = $this->secondFactorRepository->findByAccount($account); + foreach ($secondFactors as $secondFactor) { + if (TOTPService::checkIfOtpIsValid($secondFactor->getSecret(), $otp)) { + return true; + } + } + + return false; + } + /** * Check if the account can delete 1 second factor. * diff --git a/Classes/Service/SecondFactorSessionStorageService.php b/Classes/Service/SecondFactorSessionStorageService.php index edb75f4..b2567b7 100644 --- a/Classes/Service/SecondFactorSessionStorageService.php +++ b/Classes/Service/SecondFactorSessionStorageService.php @@ -7,26 +7,28 @@ use Neos\Flow\Session\SessionManagerInterface; use Sandstorm\NeosTwoFactorAuthentication\Domain\AuthenticationStatus; +/** + * @Flow\Scope("singleton") + */ class SecondFactorSessionStorageService { - const SESSION_OBJECT_ID = 'Sandstorm/NeosTwoFactorAuthentication'; - const SESSION_OBJECT_AUTH_STATUS = 'authenticationStatus'; + const string SESSION_OBJECT_ID = 'Sandstorm/NeosTwoFactorAuthentication'; + const string SESSION_OBJECT_AUTH_STATUS = 'authenticationStatus'; /** * @Flow\Inject - * @var SessionManagerInterface */ - protected $sessionManager; + protected SessionManagerInterface $sessionManager; /** * @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 +36,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 +52,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/Classes/Service/TOTPService.php b/Classes/Service/TOTPService.php index bab5e20..e7ea643 100644 --- a/Classes/Service/TOTPService.php +++ b/Classes/Service/TOTPService.php @@ -4,31 +4,31 @@ use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; -use Neos\Flow\Security\Account; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Security\Account; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; use OTPHP\TOTP; +/** + * @Flow\Scope("singleton") + */ class TOTPService { /** * @Flow\Inject - * @var DomainRepository */ - protected $domainRepository; + protected DomainRepository $domainRepository; /** * @Flow\Inject - * @var SiteRepository */ - protected $siteRepository; + protected SiteRepository $siteRepository; /** * @Flow\InjectConfiguration(path="issuerName") - * @var string | null */ - protected $issuerName; + protected ?string $issuerName; public static function generateNewTotp(): TOTP { diff --git a/Configuration/Policy.yaml b/Configuration/Policy.yaml index 674084e..fd11f88 100644 --- a/Configuration/Policy.yaml +++ b/Configuration/Policy.yaml @@ -8,6 +8,9 @@ privilegeTargets: 'Sandstorm.NeosTwoFactorAuthentication:BackendModule': matcher: 'method(Sandstorm\NeosTwoFactorAuthentication\Controller\BackendController->(.*)Action())' + 'Sandstorm.NeosTwoFactorAuthentication:ReloginApi': + matcher: 'method(Sandstorm\NeosTwoFactorAuthentication\Controller\ReloginApiController->(.*)Action())' + roles: 'Neos.Neos:AbstractEditor': privileges: @@ -17,6 +20,9 @@ roles: - privilegeTarget: 'Sandstorm.NeosTwoFactorAuthentication:BackendModule' permission: GRANT + - + privilegeTarget: 'Sandstorm.NeosTwoFactorAuthentication:ReloginApi' + permission: GRANT 'Neos.Neos:Administrator': privileges: diff --git a/Configuration/Routes.yaml b/Configuration/Routes.yaml index c7682a7..6f2b5ef 100644 --- a/Configuration/Routes.yaml +++ b/Configuration/Routes.yaml @@ -1,3 +1,21 @@ +- name: 'Sandstorm Two Factor Authentication - API Status' + uriPattern: 'neos/api/second-factor-status' + httpMethods: ['GET'] + defaults: + '@package': 'Sandstorm.NeosTwoFactorAuthentication' + '@controller': 'ReloginApi' + '@action': 'secondFactorStatus' + '@format': 'json' + +- name: 'Sandstorm Two Factor Authentication - API Verify' + uriPattern: 'neos/api/second-factor-verify' + httpMethods: ['POST'] + defaults: + '@package': 'Sandstorm.NeosTwoFactorAuthentication' + '@controller': 'ReloginApi' + '@action': 'verifySecondFactor' + '@format': 'json' + - name: 'Sandstorm Two Factor Authentication' uriPattern: 'neos/second-factor-login' httpMethods: ['GET'] @@ -36,3 +54,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/Configuration/Settings.yaml b/Configuration/Settings.yaml index 559bc32..0e706fb 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -17,6 +17,12 @@ Neos: javaScripts: - 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/index.js' + Ui: + resources: + javascript: + 'Sandstorm.NeosTwoFactorAuthentication:ReloginPlugin': + resource: 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/ReloginPlugin/Plugin.js' + userInterface: translation: autoInclude: @@ -47,7 +53,7 @@ Neos: 'Sandstorm.NeosTwoFactorAuthentication:SecondFactor': pattern: 'ControllerObjectName' patternOptions: - controllerObjectNamePattern: 'Sandstorm\NeosTwoFactorAuthentication\Controller\(LoginController|BackendController)' + controllerObjectNamePattern: 'Sandstorm\NeosTwoFactorAuthentication\Controller\(LoginController|BackendController|ReloginApiController)' Sandstorm: NeosTwoFactorAuthentication: 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