diff --git a/Events.php b/Events.php
index b321983..64fecba 100644
--- a/Events.php
+++ b/Events.php
@@ -8,12 +8,12 @@
namespace humhub\modules\twofa;
+use humhub\components\gates\GateInitEvent;
use humhub\helpers\ControllerHelper;
use humhub\modules\admin\controllers\UserController as AdminUserController;
use humhub\modules\admin\grid\UserActionColumn;
use humhub\modules\admin\permissions\ManageUsers;
-use humhub\modules\twofa\controllers\CheckController;
-use humhub\modules\twofa\events\BeforeCheck;
+use humhub\modules\twofa\components\TwofaGate;
use humhub\modules\twofa\helpers\TwofaHelper;
use humhub\modules\twofa\helpers\TwofaUrl;
use humhub\modules\ui\menu\MenuLink;
@@ -51,70 +51,30 @@ public static function registerAutoloader()
}
/**
- * Check if current User has been verified by 2fa if it is required
+ * Registers the user gates of this module (see core docs/develop/user-gates.md).
+ * The gate replaces the former request interception of this handler.
*
- * @param $event
- * @return false|\yii\console\Response|\yii\web\Response
+ * @since 1.4
*/
- public static function onBeforeAction($event)
+ public static function onGateInit(GateInitEvent $event): void
{
- if (Yii::$app->user->mustChangePassword()) {
- return;
- }
-
- /** @var Controller $controller */
- $controller = $event->sender;
-
- if (self::isImpersonateAction($controller)) {
- Yii::$app->session->set('twofa.switchedUserId', Yii::$app->user->id);
- }
-
- // Another event handler (e.g. from a module intercepting the same action) has
- // already canceled or redirected the current action; overriding its redirect
- // could produce a redirect loop between the two modules
- if (!$event->isValid || Yii::$app->response->getIsRedirection()) {
- return;
- }
-
- // Twofa-own allowlist — deliberately NOT the generic $doNotInterceptActionIds
- // flag: that flag is set by controllers for unrelated reasons (e.g. the REST
- // module, live polling, account deletion) and honoring it would exempt those
- // actions from the second factor. Every entry here is a security decision.
- if (self::isTwofaExemptRoute($controller, $event)) {
- return;
- }
-
- $beforeVerifying = new BeforeCheck();
- Yii::$app->trigger($beforeVerifying->name, $beforeVerifying);
-
- if (!$beforeVerifying->handled && TwofaHelper::isVerifyingRequired()) {
- $event->isValid = false;
- Yii::$app->response->redirect(TwofaUrl::toCheck());
- }
+ $event->manager->register(new TwofaGate());
}
/**
- * Routes that stay reachable while the two-factor verification is pending.
+ * Remembers the originating user of an admin "Impersonate" action, so the 2FA
+ * session state can be restored correctly.
*
- * @param $controller Controller
- * @return bool
+ * @param $event
*/
- protected static function isTwofaExemptRoute($controller, $event): bool
+ public static function onBeforeAction($event): void
{
- // The 2fa check page itself — redirecting it would loop onto itself
- if ($controller instanceof CheckController) {
- return true;
- }
+ /** @var Controller $controller */
+ $controller = $event->sender;
- // Login and logout must stay reachable
- if ($controller instanceof AuthController) {
- return true;
+ if (self::isImpersonateAction($controller)) {
+ Yii::$app->session->set('twofa.switchedUserId', Yii::$app->user->id);
}
-
- // The mobile app updates its push token in the background
- return $controller->module->id === 'fcm-push'
- && $controller->id === 'token'
- && $event->action->id === 'update';
}
/**
diff --git a/Module.php b/Module.php
index 658a21c..b170d6b 100644
--- a/Module.php
+++ b/Module.php
@@ -44,14 +44,6 @@ public function getConfigUrl()
return TwofaUrl::toConfig();
}
- /**
- * @return bool Check if current page is already URL of 2fa
- */
- public function isTwofaCheckUrl()
- {
- return Yii::$app->getRequest()->getUrl() === TwofaUrl::toCheck();
- }
-
/**
* Get available drivers options for the 2fa module settings
*
diff --git a/components/TwofaGate.php b/components/TwofaGate.php
new file mode 100644
index 0000000..85bbaf4
--- /dev/null
+++ b/components/TwofaGate.php
@@ -0,0 +1,106 @@
+user->isGuest && TwofaHelper::isVerificationPending();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getRoute(): array
+ {
+ return [TwofaUrl::ROUTE_CHECK];
+ }
+
+ /**
+ * Login/logout must stay reachable while verification is pending; the mobile app
+ * updates its push token in the background. Each entry is a deliberate,
+ * security-reviewed exemption from the second factor.
+ *
+ * @inheritdoc
+ */
+ public function getAllowedRoutes(): array
+ {
+ return ['user/auth', 'fcm-push/token/update'];
+ }
+
+ /**
+ * The verification is a session-based, interactive flow, so the gate does not apply
+ * to token-authenticated API requests: a REST token is issued through its own flow
+ * and stands on its own, and per-request gating a stateless request would only ever
+ * report "pending". API authentication is handled by the REST module.
+ *
+ * @inheritdoc
+ */
+ public function appliesTo(RequestClass $requestClass): bool
+ {
+ return $requestClass !== RequestClass::Api;
+ }
+
+ /**
+ * Whether 2FA is required follows group membership and driver settings, which can
+ * change at any time and must take effect instantly — so the gate is evaluated on
+ * every request.
+ *
+ * @inheritdoc
+ */
+ public function isCacheable(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Lazily delivers the verification code (e.g. by mail) when the user is
+ * intercepted and no valid code is pending yet.
+ *
+ * @inheritdoc
+ */
+ public function onIntercept(): void
+ {
+ TwofaHelper::sendCodeIfNeeded();
+ }
+}
diff --git a/config.php b/config.php
index 4045488..62f91ec 100644
--- a/config.php
+++ b/config.php
@@ -7,6 +7,7 @@
*/
use humhub\components\Application;
+use humhub\components\gates\GateManager;
use humhub\modules\admin\grid\UserActionColumn;
use humhub\modules\twofa\Events;
use humhub\modules\user\controllers\AuthController;
@@ -19,6 +20,7 @@
'namespace' => 'humhub\modules\twofa',
'events' => [
[Application::class, Application::EVENT_BEFORE_REQUEST, [Events::class, 'onBeforeRequest']],
+ [GateManager::class, GateManager::EVENT_INIT_GATES, [Events::class, 'onGateInit']],
[AuthController::class, AuthController::EVENT_AFTER_LOGIN, [Events::class, 'onAfterLogin']],
[Controller::class, Controller::EVENT_BEFORE_ACTION, [Events::class, 'onBeforeAction']],
[Controller::class, Controller::EVENT_AFTER_ACTION, [Events::class, 'onAfterAction']],
diff --git a/controllers/CheckController.php b/controllers/CheckController.php
index 38d4a98..120e0d1 100644
--- a/controllers/CheckController.php
+++ b/controllers/CheckController.php
@@ -19,11 +19,6 @@
*/
class CheckController extends Controller
{
- /**
- * @inheritdoc
- */
- protected $doNotInterceptActionIds = ['*'];
-
/**
* @inheritdoc
*/
@@ -38,10 +33,14 @@ public function actionIndex()
{
$redirectUrl = Yii::$app->user->getReturnUrl();
- if (!TwofaHelper::isVerifyingRequired()) {
- return $this->redirect($redirectUrl);
+ if (!TwofaHelper::isVerificationPending()) {
+ return $this->response->redirect($redirectUrl);
}
+ // Ensure a code is on its way even when the page is opened directly
+ // (interception delivers it via TwofaGate::onIntercept() already)
+ TwofaHelper::sendCodeIfNeeded();
+
if (isset(Yii::$app->getModule('live')->isActive)) {
Yii::$app->getModule('live')->isActive = false;
}
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index fdacae7..cc4de71 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,17 @@
Changelog
=========
+1.4.0 (July 16, 2026)
+---------------------
+- Enh: Migrated the request interception to the core user gate system (`TwofaGate`, requires humhub/humhub#8291) — deterministic ordering towards other intercepting modules (password change → 2FA → terms), no more redirect loops, and `user/auth` (login/logout) stays reachable while verification is pending
+- Enh: AJAX requests now receive `401` + JSON `{gate, url}` while verification is pending, instead of an HTML redirect; token-authenticated API requests are not intercepted (REST authentication is handled by the `rest` module)
+- Enh: `TwofaHelper::isVerifyingRequired()` was split into the side-effect free `isVerificationPending()` and `sendCodeIfNeeded()` — the verification code is delivered at interception time (`TwofaGate::onIntercept()`) instead of as a side effect of a check; a failed code delivery no longer skips the verification (fail-closed)
+- Chg: Removed the unused `BeforeCheck` event, `Module::isTwofaCheckUrl()` and the `doNotInterceptActionIds` usage
+
+1.3.0 (June 5, 2026)
+--------------------
+- Enh #111: Update for HumHub 1.19
+
1.2.3 (July 16, 2026)
---------------------
- Fix: Infinite redirect loop to the 2FA check page when another module intercepts the current action — the handler now yields when another interceptor already redirected the request, cancels the action via `$event->isValid` and uses a twofa-own, security-reviewed exemption list (check page, login/logout, push token update) instead of a generic opt-out flag
diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md
index 56c2a7b..27c656f 100644
--- a/docs/DEVELOPER.md
+++ b/docs/DEVELOPER.md
@@ -18,23 +18,18 @@ Default driver `humhub\modules\twofa\Module->defaultDriver` is used for Users fr
```php
public $defaultDriver = EmailDriver::class;
```
-## Events
+## Interception
-### `twofa.beforeCheck`
+Since 1.4 the verification is enforced through the core user gate system
+(`TwofaGate`, see the core `docs/develop/user-gates.md`) instead of a
+`Controller::EVENT_BEFORE_ACTION` handler. The former `twofa.beforeCheck` event
+has been removed.
-The `twofa.beforeCheck` event is triggered before a Two-Factor Authentication (2FA) check is performed.
+The gate applies to full page navigation and AJAX/PJAX requests, but not to
+token-authenticated API requests — REST, CalDAV and similar endpoints are
+therefore not intercepted and do not need to opt out. Login and logout
+(`user/auth`) as well as the mobile push token update stay reachable while
+verification is pending.
-Other modules can listen to this event and set `$handled = true` to skip the 2FA check.
-
-This mechanism allows disabling 2FA:
-
-- Globally for a module via its `beforeAction()` method
-- For specific controllers via their `beforeAction()` method
-- For specific actions within a controller via conditional logic in `beforeAction()`
-
-Example:
-```php
-Yii::$app->on('twofa.beforeCheck', function (Event $event) use ($action) {
- $event->handled = $action->controller->id === 'some-controller'; // Will disable 2FA for `some-controller`
-});
-```
\ No newline at end of file
+There is no longer a per-controller opt-out: the gate intercepts every full
+page request of a user with pending verification until the check is completed.
\ No newline at end of file
diff --git a/events/BeforeCheck.php b/events/BeforeCheck.php
deleted file mode 100644
index 040f29c..0000000
--- a/events/BeforeCheck.php
+++ /dev/null
@@ -1,15 +0,0 @@
-canSend()) {
- return false;
- }
+ return $driver && $driver->canSend() && !self::isSessionVerified();
+ }
- if (!self::isPendingVerification()) {
- return self::enableVerifying() || self::getCode() !== null;
+ /**
+ * Delivers a verification code unless a valid one is already pending.
+ *
+ * @return bool whether a valid code is available (already pending or newly sent)
+ * @since 1.4
+ */
+ public static function sendCodeIfNeeded(): bool
+ {
+ if (self::isPendingVerification() && self::getCode() !== null) {
+ return true;
}
- return self::getCode() !== null || self::enableVerifying();
+ return self::enableVerifying();
}
/**
diff --git a/messages/de/base.php b/messages/de/base.php
index ae4e94b..8dcc983 100644
--- a/messages/de/base.php
+++ b/messages/de/base.php
@@ -1,8 +1,8 @@
Confirm two-factor authentication reset' => '',
+ 'Confirm two-factor authentication reset' => 'Zwei-Faktor-Authentifizierung zurücksetzen bestätigen',
'Request new code' => 'Neuen Code anfordern',
- 'Two-Factor Authentication administration' => '',
+ 'Two-Factor Authentication administration' => 'Verwaltung der Zwei-Faktor-Authentifizierung',
'Two-Factor Authentication settings' => 'Zwei-Faktor-Authentifizierung Einstellungen',
'Two-factor authentication' => 'Zwei-Faktor-Authentifizierung',
'A confirmation code hast just been sent to your email address. Please enter the code from the email in order to proceed.' => 'Ein Bestätigungscode wurde soeben an Ihre E-Mail-Adresse gesendet. Bitte geben Sie den Code aus der E-Mail ein, um fortzufahren.',
@@ -17,41 +17,41 @@
'Default method for the mandatory groups' => 'Standardmethode für die obligatorischen Gruppen',
'Disable two-factor authentication (not recommended)' => 'Zwei-Faktor-Authentifizierung deaktivieren (nicht empfohlen)',
'Do you really want to request a new code?' => 'Möchtest Du wirklich einen neuen Code anfordern?',
- 'Download recovery codes' => '',
- 'Download them now and store them in a safe place. Each code can be used only once.' => '',
- 'Each code can be used only once.' => '',
+ 'Download recovery codes' => 'Wiederherstellungscodes herunterladen',
+ 'Download them now and store them in a safe place. Each code can be used only once.' => 'Lade sie jetzt herunter und bewahre sie an einem sicheren Ort auf. Jeder Code kann nur einmal verwendet werden.',
+ 'Each code can be used only once.' => 'Jeder Code kann nur einmal verwendet werden.',
'Email' => 'E-Mail',
'Enabled methods' => 'Aktivierte Methoden',
- 'Enabled users' => '',
+ 'Enabled users' => 'Aktivierte Benutzer',
'General' => 'Allgemein',
'Google Authenticator' => 'Google Authenticator',
'Hello {displayName}!' => 'Hallo {displayName}!',
- 'Help text' => '',
+ 'Help text' => 'Hilfetext',
'Install an application that implements a time-based one-time password (TOTP) algorithm, such as {googleAuthenticatorLink}, and use it to scan the QR code shown below.' => 'Installiere eine Anwendung/App, die einen zeitbasierten One-Time-Password-Algorithmus (TOTP) implementiert, z. B. {googleAuthenticatorLink}, und scanne damit den unten gezeigten QR-Code.',
'Leave empty to disable this feature.' => 'Leer lassen um die Funktion zu deaktivieren',
'Length of verifying code' => 'Länge des Verifizierungscodes',
'List of IPs or subnets to whitelist, currently yours is {0}. Use coma separator to create a list, example: "{0}, 127.0.0.1"' => 'Liste von IP Adressen oder Subnetzen für die Whitelist. Deine derzeitige {0}. Nutze Komma getrennte Liste. z.B.: "{0}, 127.0.0.1"',
'Log out' => 'Abmelden',
'Mandatory for the following groups' => 'Obligatorisch für die folgenden Gruppen',
- 'Method' => '',
- 'No users with configured two-factor authentication found.' => '',
+ 'Method' => 'Methode',
+ 'No users with configured two-factor authentication found.' => 'Keine Benutzer mit eingerichteter Zwei-Faktor-Authentifizierung gefunden.',
'Open the two-factor authentication app on your device to view your authentication code and verify your identity.' => 'Öffnen Sie die App zur Zwei-Faktor-Authentifizierung auf Ihrem Gerät, um Ihren Authentifizierungscode anzuzeigen und Ihre Identität zu überprüfen.',
- 'Optional text shown on the two-factor authentication screen.' => '',
+ 'Optional text shown on the two-factor authentication screen.' => 'Optionaler Text, der auf der Seite zur Zwei-Faktor-Authentifizierung angezeigt wird.',
'Pin code' => 'Pin-Code',
'Please do not forget to update the code in your authenticator app! If you do not do so, you will not be able to login.' => 'Bitte vergiss nicht, den Code in Ihrer Authenticator-App zu aktualisieren! Wenn Du dies nicht tust, kannst Du Dich nicht mehr anmelden.',
'Please enter your verifying code.' => 'Bitte geben Sie Ihren Verifizierungscode ein.',
- 'Recovery codes for {appName}' => '',
+ 'Recovery codes for {appName}' => 'Wiederherstellungscodes für {appName}',
'Remember browser option amount of days' => 'Browser Vertrauen Option Tage',
'Remember this browser for {0} days' => 'Diesem Gerät für {0} Tage vertrauen',
'Request new code' => 'Neuen Code anfordern',
'Reset' => 'Zurücksetzen',
- 'Reset two-factor authentication' => '',
+ 'Reset two-factor authentication' => 'Zwei-Faktor-Authentifizierung zurücksetzen',
'Secret:' => 'Secret:',
'TTL of verifying code in seconds' => 'Gültigkeitsdauer (TTL) des Bestätigungscodes in Sekunden',
- 'These recovery codes are shown only once.' => '',
- 'This list shows users who currently have two-factor authentication configured.' => '',
+ 'These recovery codes are shown only once.' => 'Diese Wiederherstellungscodes werden nur einmal angezeigt.',
+ 'This list shows users who currently have two-factor authentication configured.' => 'Diese Liste zeigt Benutzer, für die aktuell eine Zwei-Faktor-Authentifizierung eingerichtet ist.',
'This module is disabled because no drivers are selected, however users from the enforced groups always fallback to {defaultDriverName} driver by default.' => 'Dieses Modul ist deaktiviert, da keine Treiber ausgewählt sind, aber Benutzer aus den erzwungenen Gruppen greifen standardmäßig immer auf den Treiber {defaultDriverName} zurück.',
- 'This will remove the current two-factor authentication setup for this user. They will need to configure it again on the next login.' => '',
+ 'This will remove the current two-factor authentication setup for this user. They will need to configure it again on the next login.' => 'Dadurch wird die aktuelle Einrichtung der Zwei-Faktor-Authentifizierung für diesen Benutzer entfernt. Der Benutzer muss sie bei der nächsten Anmeldung erneut einrichten.',
'Time based: Yes' => 'Zeitbasiert: Ja',
'Time-based one-time passwords (e.g. Google Authenticator)' => 'Zeitbasierte Einmal-Passwörter (z. B. Google Authenticator)',
'To connect the app manually, provide the following details to the TOTP app (e.g. Google Authenticator).' => 'Um die App manuell zu verbinden, gib bitte in der TOTP-App (z. B. Google Authenticator) die folgenden Details an.',
@@ -59,14 +59,14 @@
'Two-Factor Authentication' => 'Zwei-Faktor-Authentifizierung',
'Two-factor authentication (2FA) provides an additional level of security for your account. Once enabled, you will be prompted to enter a code in addition to entering your username and password.' => 'Die Zwei-Faktor-Authentifizierung (2FA) bietet eine zusätzliche Sicherheitsstufe für Dein Konto. Sobald sie aktiviert ist, wirst Du aufgefordert, zusätzlich zur Eingabe Deines Benutzernamens und Passworts einen Code einzugeben.',
'Two-factor authentication code is expired. Please try again.' => 'Der Zwei-Faktor-Authentifizierungscode ist abgelaufen. Bitte versuche es erneut.',
- 'Two-factor authentication has been reset for this user.' => '',
+ 'Two-factor authentication has been reset for this user.' => 'Die Zwei-Faktor-Authentifizierung wurde für diesen Benutzer zurückgesetzt.',
'User' => 'Benutzer',
- 'User: {username}' => '',
+ 'User: {username}' => 'Benutzer: {username}',
'Users' => 'Benutzer',
'Verify' => 'Prüfen',
'Verifying code is not valid!' => 'Verifizierungscode ist nicht gültig!',
- 'You can also enter one of your recovery codes if you lost access to your authenticator app.' => '',
- 'You signed in with a recovery code. Please generate new recovery codes or reconfigure your authenticator app.' => '',
+ 'You can also enter one of your recovery codes if you lost access to your authenticator app.' => 'Du kannst auch einen deiner Wiederherstellungscodes eingeben, wenn du den Zugriff auf deine Authenticator-App verloren hast.',
+ 'You signed in with a recovery code. Please generate new recovery codes or reconfigure your authenticator app.' => 'Du hast dich mit einem Wiederherstellungscode angemeldet. Bitte erstelle neue Wiederherstellungscodes oder richte deine Authenticator-App erneut ein.',
'Your account is secured by a two-factor authentication system. Please use the following code to proceed.' => 'Ihr Konto ist durch ein Zwei-Faktor-Authentifizierungssystem gesichert. Bitte verwenden Sie den folgenden Code, um fortzufahren.',
'Your login verification code' => 'Ihr Login-Verifizierungscode',
];
diff --git a/messages/nl/base.php b/messages/nl/base.php
index 6bed335..464420c 100644
--- a/messages/nl/base.php
+++ b/messages/nl/base.php
@@ -1,8 +1,8 @@
Confirm two-factor authentication reset' => '',
+ 'Confirm two-factor authentication reset' => 'Bevestig het opnieuw instellen van de tweefactorauthenticatie',
'Request new code' => 'Vraageen nieuwe code aan',
- 'Two-Factor Authentication administration' => '',
+ 'Two-Factor Authentication administration' => 'Tweefactorauthenticatie beheer',
'Two-Factor Authentication settings' => 'Instellingen voor Twee-factorenauthenticatie',
'Two-factor authentication' => 'Tweefactorauthenticatie',
'A confirmation code hast just been sent to your email address. Please enter the code from the email in order to proceed.' => 'Een bevestigingscode is naar uw e-mailadres gestuurd. Voer de code uit deze e-mail in om verder te gaan.',
@@ -17,41 +17,41 @@
'Default method for the mandatory groups' => 'Standaardmethode voor de verplichte groepen',
'Disable two-factor authentication (not recommended)' => 'Schakel tweefactorauthenticatie uit (niet aanbevolen)',
'Do you really want to request a new code?' => 'Wilt u een nieuwe code aanvragen?',
- 'Download recovery codes' => '',
- 'Download them now and store them in a safe place. Each code can be used only once.' => '',
- 'Each code can be used only once.' => '',
+ 'Download recovery codes' => 'Herstelcodes downloaden',
+ 'Download them now and store them in a safe place. Each code can be used only once.' => 'Download ze nu en bewaar ze op een veilige plek. U kunt elke code slechts één keer gebruiken.',
+ 'Each code can be used only once.' => 'U kunt elke code slechts één keer gebruiken.',
'Email' => 'E-mail',
'Enabled methods' => 'Ingeschakelde methoden',
- 'Enabled users' => '',
+ 'Enabled users' => 'Ingeschakelde gebruikers',
'General' => 'Algemeen',
'Google Authenticator' => 'Google Authenticator',
'Hello {displayName}!' => 'Hallo {displayName}!',
- 'Help text' => '',
+ 'Help text' => 'Helptekst',
'Install an application that implements a time-based one-time password (TOTP) algorithm, such as {googleAuthenticatorLink}, and use it to scan the QR code shown below.' => 'Installeer een applicatie die een op tijd gebaseerd eenmalig wachtwoord (TOTP) -algoritme implementeert, zoals {googleAuthenticatorLink}, en gebruik dit om de onderstaande QR-code te scannen.',
'Leave empty to disable this feature.' => 'Laat leeg om deze functie uit te schakelen.',
'Length of verifying code' => 'Lengte van de verificatiecode',
'List of IPs or subnets to whitelist, currently yours is {0}. Use coma separator to create a list, example: "{0}, 127.0.0.1"' => 'Lijst met IP\'s of subnetten die op de vertrouwde lijst moeten worden gezet, momenteel is de jouwe {0}. Gebruik een kommascheidingsteken om een lijst te maken, bijvoorbeeld: "{0}, 127.0.0.1"',
'Log out' => 'Uitloggen',
'Mandatory for the following groups' => 'Verplicht voor de volgende groepen',
- 'Method' => '',
- 'No users with configured two-factor authentication found.' => '',
+ 'Method' => 'Methode',
+ 'No users with configured two-factor authentication found.' => 'Er zijn geen gebruikers gevonden met geconfigureerde tweefactorauthenticatie.',
'Open the two-factor authentication app on your device to view your authentication code and verify your identity.' => 'Open de tweefactorauthenticatie-app op uw apparaat om uw authenticatiecode te bekijken en uw identiteit te verifiëren.',
- 'Optional text shown on the two-factor authentication screen.' => '',
+ 'Optional text shown on the two-factor authentication screen.' => 'Optionele tekst die wordt weergegeven op het scherm voor tweefactorauthenticatie.',
'Pin code' => 'Pincode',
'Please do not forget to update the code in your authenticator app! If you do not do so, you will not be able to login.' => ' de code in uw authenticator-app bij te werken! Als u dit niet doet, kunt u niet inloggen.',
'Please enter your verifying code.' => 'Voer uw verificatiecode in.',
- 'Recovery codes for {appName}' => '',
+ 'Recovery codes for {appName}' => 'Herstelcodes voor {appName}',
'Remember browser option amount of days' => 'Onthoud browseroptie aantal dagen',
'Remember this browser for {0} days' => 'Onthoud deze browser voor {0} dagen',
'Request new code' => 'Vraag een nieuwe code aan',
- 'Reset' => 'Reset',
- 'Reset two-factor authentication' => '',
+ 'Reset' => 'Opnieuw instellen',
+ 'Reset two-factor authentication' => 'Tweefactorauthenticatie opnieuw instellen',
'Secret:' => 'Geheimcode:',
'TTL of verifying code in seconds' => 'Tijdsduur voor het verifiëren van de code in seconden',
- 'These recovery codes are shown only once.' => '',
- 'This list shows users who currently have two-factor authentication configured.' => '',
+ 'These recovery codes are shown only once.' => 'Deze herstelcodes worden slechts één keer weergegeven.',
+ 'This list shows users who currently have two-factor authentication configured.' => 'Deze lijst toont gebruikers die momenteel tweefactorauthenticatie hebben ingesteld.',
'This module is disabled because no drivers are selected, however users from the enforced groups always fallback to {defaultDriverName} driver by default.' => 'Deze module is uitgeschakeld omdat er geen stuurprogramma\'s zijn geselecteerd. Maar gebruikers uit de verplichte groepen vallen terug op het stuurprogramma {defaultDriverName}.',
- 'This will remove the current two-factor authentication setup for this user. They will need to configure it again on the next login.' => '',
+ 'This will remove the current two-factor authentication setup for this user. They will need to configure it again on the next login.' => 'Hierdoor wordt de huidige tweefactorauthenticatie voor deze gebruiker verwijderd. De gebruiker moet deze bij de volgende aanmelding opnieuw instellen.',
'Time based: Yes' => 'Op tijd gebaseerd: ja',
'Time-based one-time passwords (e.g. Google Authenticator)' => 'Op tijd gebaseerde eenmalige wachtwoorden (bijvoorbeeld Google Authenticator)',
'To connect the app manually, provide the following details to the TOTP app (e.g. Google Authenticator).' => 'Om de app handmatig te verbinden, geeft u de volgende details op aan de TOTP-app (bijvoorbeeld Google Authenticator).',
@@ -59,14 +59,14 @@
'Two-Factor Authentication' => 'Twee-factorenauthenticatie',
'Two-factor authentication (2FA) provides an additional level of security for your account. Once enabled, you will be prompted to enter a code in addition to entering your username and password.' => 'Tweefactorauthenticatie (2FA) biedt een extra beveiligingsniveau voor uw account. Eenmaal ingeschakeld, wordt u gevraagd om naast uw gebruikersnaam en wachtwoord ook een code in te voeren.',
'Two-factor authentication code is expired. Please try again.' => 'De tweefactorauthenticatiecode is verlopen. Probeer het opnieuw.',
- 'Two-factor authentication has been reset for this user.' => '',
+ 'Two-factor authentication has been reset for this user.' => 'De tweefactorauthenticatie is voor deze gebruiker opnieuw ingesteld.',
'User' => 'Gebruiker',
- 'User: {username}' => '',
+ 'User: {username}' => 'Gebruiker: {username}',
'Users' => 'Gebruikers',
'Verify' => 'Verifiëren',
'Verifying code is not valid!' => 'De verificatiecode is niet geldig!',
- 'You can also enter one of your recovery codes if you lost access to your authenticator app.' => '',
- 'You signed in with a recovery code. Please generate new recovery codes or reconfigure your authenticator app.' => '',
+ 'You can also enter one of your recovery codes if you lost access to your authenticator app.' => 'U kunt ook een van uw herstelcodes invoeren als u geen toegang meer hebt tot uw authenticatie-app.',
+ 'You signed in with a recovery code. Please generate new recovery codes or reconfigure your authenticator app.' => 'U hebt ingelogd met een herstelcode. Genereer nieuwe herstelcodes of configureer uw authenticatie-app opnieuw.',
'Your account is secured by a two-factor authentication system. Please use the following code to proceed.' => 'Uw account is beveiligd met een tweefactorauthenticatiesysteem. Gebruik de volgende code om door te gaan.',
'Your login verification code' => 'Uw inlogverificatiecode',
];
diff --git a/module.json b/module.json
index 865a3b7..c739873 100644
--- a/module.json
+++ b/module.json
@@ -14,9 +14,8 @@
"resources/screenshot3.png",
"resources/screenshot4.png"
],
- "version": "1.2.3",
+ "version": "1.4.0",
"humhub": {
- "minVersion": "1.18.0-beta.6",
- "maxVersion": "1.18"
+ "minVersion": "1.19"
}
}
diff --git a/tests/codeception/acceptance/TwofaCest.php b/tests/codeception/acceptance/TwofaCest.php
index 58be43f..6a45ac6 100644
--- a/tests/codeception/acceptance/TwofaCest.php
+++ b/tests/codeception/acceptance/TwofaCest.php
@@ -65,7 +65,7 @@ public function testManagerCanResetUserTwoFactorAuthentication(AcceptanceTester
$I->resetCookie('PHPSESSID');
$I->executeJS('window.localStorage.clear(); window.sessionStorage.clear();');
$I->amOnPage('/user/auth/login');
- $I->waitForText('Please sign in');
+ $I->waitForText('Sign In');
$I->amUser2();
$I->amOnPage('/admin/user/list?UserSearch%5BfreeText%5D=User1');
diff --git a/tests/codeception/functional/TwofaGateCest.php b/tests/codeception/functional/TwofaGateCest.php
new file mode 100644
index 0000000..b949e20
--- /dev/null
+++ b/tests/codeception/functional/TwofaGateCest.php
@@ -0,0 +1,78 @@
+installationState->setInstalled();
+ $loginPage = LoginPage::openBy($I);
+ $loginPage->login('Admin', 'admin&humhub@PASS%worD!');
+ $I->see('Two-factor authentication');
+ }
+
+ public function testAjaxRequestReceivesGateResponse(FunctionalTester $I)
+ {
+ $I->wantTo('ensure that AJAX requests receive a machine-readable gate response while 2FA is pending');
+
+ $this->loginPendingAdmin($I);
+
+ $I->sendAjaxGetRequest('/index-test.php?r=dashboard%2Fdashboard');
+
+ $I->seeResponseCodeIs(401);
+ $I->see('twofa');
+ }
+
+ public function testSessionRequestCannotEscapeViaAcceptHeader(FunctionalTester $I)
+ {
+ $I->wantTo('ensure a pending session cannot escape the 2FA check by faking a JSON Accept header');
+
+ $this->loginPendingAdmin($I);
+
+ // A cookie-authenticated (session) request that fakes a non-HTML Accept header must
+ // stay subject to the gate — the API exemption only covers stateless token requests,
+ // which are determined server-side (see core GateFilter::getRequestClass()).
+ $I->haveHttpHeader('Accept', 'application/json');
+ $I->amOnPage('/index-test.php?r=dashboard%2Fdashboard');
+
+ $I->seeResponseCodeIs(403);
+ $I->see('twofa');
+ }
+
+ public function testAccountDeleteStaysIntercepted(FunctionalTester $I)
+ {
+ $I->wantTo('ensure that account deletion is not reachable while 2FA is pending');
+
+ $this->loginPendingAdmin($I);
+
+ $I->amOnPage('/index-test.php?r=user%2Faccount%2Fdelete');
+
+ $I->see('Two-factor authentication');
+ }
+
+ public function testLogoutStaysReachableWhilePending(FunctionalTester $I)
+ {
+ $I->wantTo('ensure that logout works while 2FA is pending');
+
+ $this->loginPendingAdmin($I);
+
+ $I->sendAjaxPostRequest('/index-test.php?r=user%2Fauth%2Flogout');
+
+ $I->amOnRoute('/dashboard/dashboard');
+ $I->see('Sign in');
+ }
+}
diff --git a/tests/codeception/unit/TwofaTest.php b/tests/codeception/unit/TwofaTest.php
index 96e66f5..dea46c4 100644
--- a/tests/codeception/unit/TwofaTest.php
+++ b/tests/codeception/unit/TwofaTest.php
@@ -35,7 +35,7 @@ public function testVerifyCode()
{
$this->becomeUser('Admin');
$this->assertTrue(TwofaHelper::enableVerifying());
- $this->assertTrue(TwofaHelper::isVerifyingRequired());
+ $this->assertTrue(TwofaHelper::isVerificationPending());
$this->assertFalse(TwofaHelper::isValidCode('test'));
}
@@ -46,6 +46,23 @@ public function testDisableVerifying()
$this->assertTrue(TwofaHelper::disableVerifying(true));
$this->assertNull(TwofaHelper::getCode());
$this->assertNull(TwofaHelper::getSetting(TwofaHelper::CODE_EXPIRATION_SETTING));
- $this->assertFalse(TwofaHelper::isVerifyingRequired());
+ $this->assertFalse(TwofaHelper::isVerificationPending());
+ }
+
+ public function testVerificationPendingIsFreeOfSideEffects()
+ {
+ // 2FA is enforced for the Admin, but no code has been sent yet:
+ // the check must report pending without delivering a code itself
+ $this->becomeUser('Admin');
+ $this->assertTrue(TwofaHelper::isVerificationPending());
+ $this->assertNull(TwofaHelper::getCode());
+
+ // sendCodeIfNeeded() delivers once and keeps the pending code afterwards
+ $this->assertTrue(TwofaHelper::sendCodeIfNeeded());
+ $this->assertNotNull(TwofaHelper::getCode());
+
+ // users without a 2FA requirement are never pending
+ $this->becomeUser('User1');
+ $this->assertFalse(TwofaHelper::isVerificationPending());
}
}