Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions Classes/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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'
);
}
}
7 changes: 3 additions & 4 deletions Classes/Domain/AuthenticationStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
6 changes: 0 additions & 6 deletions Classes/Domain/Model/Dto/SecondFactorDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 18 additions & 39 deletions Classes/Domain/Model/SecondFactor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
}
11 changes: 11 additions & 0 deletions Classes/Http/Middleware/SecondFactorMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down
19 changes: 15 additions & 4 deletions Classes/Service/SecondFactorSessionStorageService.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,24 @@ 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,
]
);
}

/**
* @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]);
}

/**
Expand All @@ -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.');
}
}
}
9 changes: 9 additions & 0 deletions Configuration/Routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Component.LoginSecondFactorStep)
}

renderer = afx`
<Neos.Fusion.Form:Form form.target.action="checkSecondFactor" form.target.controller="Login">
<Neos.Fusion.Form:Form
attributes.class="neos-two-factor__authentication-form"
form.target.action="checkSecondFactor"
form.target.controller="Login"
>
<fieldset>
<div class="neos-controls">
<Neos.Fusion.Form:Input
Expand All @@ -25,12 +29,32 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Component.LoginSecondFactorStep)
</div>

<div class="neos-actions">
<Neos.Fusion.Form:Button attributes.class="neos-span5 neos-pull-right neos-button neos-login-btn">
<Neos.Fusion.Form:Button
attributes.id="submitLogin"
attributes.class="neos-span5 neos-pull-right neos-button neos-login-btn"
>
{I18n.id('login').value('Login').package('Neos.Neos').source('Main').translate()}
</Neos.Fusion.Form:Button>
<Neos.Fusion.Form:Button attributes.class="neos-span5 neos-pull-right neos-button neos-login-btn neos-disabled neos-hidden">
<Neos.Fusion.Form:Button
attributes.id="submitLoginIndicator"
attributes.class="neos-span5 neos-pull-right neos-button neos-login-btn neos-disabled neos-hidden"
>
{I18n.id('authenticating').value('Authenticating').package('Neos.Neos').source('Main').translate()} <span class="neos-ellipsis"></span>
</Neos.Fusion.Form:Button>
<Neos.Fusion.Form:Button
attributes.id="cancelLogin"
attributes.formaction="/neos/second-factor-cancel-login"
attributes.formnovalidate="true"
attributes.class="neos-span5 neos-pull-right neos-button neos-login-btn neos-button-warning"
>
{I18n.id('cancel').value('Cancel').package('Neos.Neos').source('Main').translate()}
</Neos.Fusion.Form:Button>
<Neos.Fusion.Form:Button
attributes.id="cancelLoginIndicator"
attributes.class="neos-span5 neos-pull-right neos-button neos-login-btn neos-disabled neos-hidden neos-button-warning"
>
{I18n.id('cancel').value('Cancel').package('Neos.Neos').source('Main').translate()} <span class="neos-ellipsis"></span>
</Neos.Fusion.Form:Button>
</div>

<Neos.Fusion:Loop items={props.flashMessages} itemName="flashMessage">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,22 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Page.LoginSecondFactorPage) < pr
</div>

<script>{"
document.querySelector('form').addEventListener('submit', function() {
document.querySelector('.neos-login-btn').classList.toggle('neos-hidden');
document.querySelector('.neos-login-btn.neos-disabled').classList.toggle('neos-hidden');
document.querySelector('form').addEventListener('submit', function(event) {
if (event.submitter && event.submitter.id === 'cancelLogin') {
event.target.querySelector('#submitLogin').disabled = 'disabled';
event.target.querySelector('#cancelLogin').disabled = 'disabled';

event.target.querySelector('#submitLogin').classList.add('neos-disabled');
event.target.querySelector('#cancelLogin').classList.add('neos-hidden');
event.target.querySelector('#cancelLoginIndicator').classList.remove('neos-hidden');
} else {
event.target.querySelector('#submitLogin').disabled = 'disabled';
event.target.querySelector('#cancelLogin').disabled = 'disabled';

event.target.querySelector('#submitLogin').classList.add('neos-hidden');
event.target.querySelector('#cancelLogin').classList.add('neos-disabled');
event.target.querySelector('#submitLoginIndicator').classList.remove('neos-hidden');
}
});
try {
document.querySelector('form[name=\"login\"] input[name=\"lastVisitedNode\"]').value = sessionStorage.getItem('Neos.Neos.lastVisitedNode');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Page.SetupSecondFactorPage) < pr

<div class="neos-login-body neos">
<Sandstorm.NeosTwoFactorAuthentication:Component.LoginFlashMessages flashMessages={props.flashMessages} />
<Neos.Fusion.Form:Form form.target.action="setupSecondFactor">
<Neos.Fusion.Form:Form
attributes.class="neos-two-factor__authentication-form"
form.target.action="setupSecondFactor"
>
<div class="neos-control-group">
<img src={qrCode} style="width: 100%; max-width: 400px"/>
</div>
Expand Down Expand Up @@ -162,6 +165,13 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Page.SetupSecondFactorPage) < pr
<Neos.Fusion.Form:Button attributes.class="neos-span5 neos-pull-right neos-button neos-login-btn">
{I18n.id('module.new.submit-otp').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()}
</Neos.Fusion.Form:Button>
<Neos.Fusion.Form:Button
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This button might only be required if 2FA is required for the current user and the user does not yet have a TOTP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The user might still want to cancel the Login flow on this screen, right?
At least that is how I understood the feature request.

attributes.formaction="/neos/second-factor-cancel-login"
attributes.formnovalidate="true"
attributes.class="neos-span5 neos-pull-right neos-button neos-login-btn neos-button-warning"
>
{I18n.id('cancel').value('Cancel').package('Neos.Neos').source('Main').translate()}
</Neos.Fusion.Form:Button>
</div>
</Neos.Fusion.Form:Form>
</div>
Expand Down
15 changes: 15 additions & 0 deletions Resources/Public/Styles/Login.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,18 @@
.neos-two-factor__secret-wrapper ::-webkit-scrollbar-track {
background-color: initial;
}

.neos-two-factor__authentication-form .neos-actions {
display: flex;
flex-direction: column;
gap: 8px;
}

.neos-two-factor__authentication-form .neos-actions button {
margin-left: 0;
}

.neos-two-factor__authentication-form .neos-actions button.neos-login-btn.neos-button-warning {
background-color: #F89406;
color: #fff;
}