Skip to content
Merged
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
21 changes: 21 additions & 0 deletions Classes/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// ------------------------------------------------------------------
Expand Down
14 changes: 14 additions & 0 deletions Classes/Http/Middleware/SecondFactorMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions Classes/Service/SecondFactorSessionStorageService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
}
}
9 changes: 9 additions & 0 deletions Configuration/Routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ 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'
action = 'newTotp'
}
}
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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ Sandstorm.NeosTwoFactorAuthentication.BackendController.newTotp = Sandstorm.Neos
</div>
<div class="neos-control-group">
<Neos.Fusion.Form:Button
attributes.data-test-id="create-second-factor-submit-button"
attributes.class="neos-button neos-two-factor__register-button">
{I18n.id('module.new.submit-otp').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()}
</Neos.Fusion.Form:Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ Sandstorm.NeosTwoFactorAuthentication.LoginController.setupSecondFactor = Sandst
body = 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 = 'Login'
action = 'setupTotp'
}
}
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 = 'Login'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
prototype(Sandstorm.NeosTwoFactorAuthentication:Component.CancelLoginButton) < prototype(Neos.Fusion:Component) {
# Secondary "escape hatch" shown on every 2FA screen (challenge + enforced setup).
# It is a self-contained form, so it works whether or not the surrounding step is itself
# a form (the WebAuthn step and method picker are not), and avoids invalid nested forms.
renderer = afx`
<Neos.Fusion.Form:Form
attributes.class="neos-two-factor__cancel-login"
form.target.package="Sandstorm.NeosTwoFactorAuthentication"
form.target.controller="Login"
form.target.action="cancelLogin"
>
<Neos.Fusion.Form:Button
attributes.type="submit"
attributes.formnovalidate="true"
attributes.class="neos-two-factor__cancel-login__button"
attributes.aria-label={I18n.id('login.cancel').value('Cancel and return to login').package('Sandstorm.NeosTwoFactorAuthentication').translate()}
>
{I18n.id('login.cancel').value('Cancel and return to login').package('Sandstorm.NeosTwoFactorAuthentication').translate()}
</Neos.Fusion.Form:Button>
</Neos.Fusion.Form:Form>
`
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
prototype(Sandstorm.NeosTwoFactorAuthentication:Component.MethodPicker) < prototype(Neos.Fusion:Component) {
# array of { label, description, href, testId }
# array of { id, label, description, href }
# Each method is a link whose visible label is its accessible name, so it can
# be addressed in tests by role+name (e.g. "Authenticator app") — no test id needed.
items = ${[]}

renderer = afx`
<br />
<div class="neos-two-factor__method-picker">
<div class="neos-two-factor__method-picker neos-actions">
<Neos.Fusion:Loop items={props.items} itemName="item">
<div class="neos-two-factor__method-picker__row">
<a href={item.href} class="neos-button neos-login-btn neos-two-factor__method-picker__button" data-test-id={item.testId}>
<a aria-describedby={item.id} href={item.href} class="btn neos-button neos-login-btn neos-two-factor__method-picker__button">
{item.label}
</a>
<span class="neos-two-factor__method-picker__description">{item.description}</span>
<label id={item.id} class="neos-two-factor__method-picker__description">{item.description}</label>
</div>
</Neos.Fusion:Loop>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Component.SecondFactorList.Entry
<td>{props.factorAndPerson.secondFactor.name == null ? '-' : props.factorAndPerson.secondFactor.name}</td>
<td>{props.factorAndPerson.secondFactor.creationDate == null ? '-' : Date.format(props.factorAndPerson.secondFactor.creationDate, 'Y-m-d H:i')}</td>
<td>
<button class="neos-button neos-button-danger" data-toggle="modal" data-test-id="delete-second-factor-button"
href={'#user-' + props.iterator.index} title={I18n.id('module.index.list.action.delete').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()} data-neos-toggle="tooltip">
<button class="neos-button neos-button-danger" data-toggle="modal"
href={'#user-' + props.iterator.index} title={I18n.id('module.index.list.action.delete').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()} aria-label={I18n.id('module.index.list.action.delete').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()} data-neos-toggle="tooltip">
<i class="fas fa-trash-alt icon-white"></i>
</button>

<div class="neos-hide" id={'user-' + props.iterator.index}>
<div class="neos-modal-centered">
<div class="neos-modal-content">
<div class="neos-modal-header">
<button type="button" class="neos-close neos-button" data-dismiss="modal"></button>
<button type="button" class="neos-close neos-button" data-dismiss="modal" aria-label={I18n.id('module.index.delete.close').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()}></button>
<div class="neos-header">
{I18n.id('module.index.delete.header').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()}
</div>
Expand Down
Loading