hi1 / contao-2fa-email-bundle
Adds email-based two-factor authentication for Contao backend users
Package info
github.com/hi1-yildiz/contao-2fa-email-bundle
Type:contao-bundle
pkg:composer/hi1/contao-2fa-email-bundle
Requires
- php: ^8.1
- contao/core-bundle: ^5.3
- scheb/2fa-bundle: ^7.0 || ^8.0
- scheb/2fa-email: ^7.0 || ^8.0
- symfony/config: ^6.4 || ^7.0 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.5
- contao/manager-plugin: ^2.0
- phpunit/phpunit: ^10.0
- symfony/phpunit-bridge: ^6.4 || ^7.0 || ^8.0
Conflicts
- contao/manager-plugin: <2.0 || >=3.0
README
Email-based two-factor authentication for Contao backend users.
Contao ships with two-factor authentication based on an authenticator app (TOTP). That works well, but it requires every editor to install and configure an app — which is a real hurdle for occasional users, and a support problem when someone loses their phone.
This bundle adds a second option: a six-digit code sent by email at every login. Users choose the method that suits them, administrators can require that some method is active, and both methods live side by side on Contao's existing security page.
Features
- One-time codes by email — a six-digit code is sent to the user's registered address at every login
- Free choice of method — every user picks either an authenticator app or email, and can switch at any time
- Optional enforcement — a single global setting requires all backend users to set up two-factor authentication before they can work
- Brute-force protection — codes expire after five minutes, and the account is locked after five wrong attempts
- Password confirmation — disabling or switching a method requires the account password
- Integrated, not bolted on — extends Contao's own security page instead of adding a separate module
- German and English included
Screenshots
Choosing a method
Users open their profile menu and select "Security". Both methods are offered side by side, with an explanation of the trade-offs.
Activating email authentication
A code is sent to the registered address and confirmed once to activate the method.
Enforcing two-factor authentication
Administrators enable the requirement globally under System settings → Two-factor authentication.
Requirements
| PHP | 8.1 or higher |
| Contao | 5.3 or higher |
| Dependencies | scheb/2fa-bundle ^7.0 || ^8.0, scheb/2fa-email ^7.0 || ^8.0 |
A working mailer configuration is required — the bundle sends codes through Contao's
mailer, so MAILER_DSN and the system sender address must be set up correctly.
Installation
Contao Manager
Search for hi1/contao-2fa-email-bundle and install it.
Composer
composer require hi1/contao-2fa-email-bundle
Then update the database, either through the Contao install tool or on the command line:
vendor/bin/contao-console contao:migrate
This adds four columns to tl_user; no existing data is modified.
Usage
For backend users
- Open the profile menu in the top right corner and select Security.
- Choose Email or Authenticator app.
- For email: click Send verification code, then enter the six-digit code from your inbox.
- From the next login onwards, a code is sent automatically after the password step.
The active method can be changed at any time on the same page. Switching or disabling a method requires the account password. Note that when the authenticator app method is disabled, its configuration key changes — the app entry has to be set up again.
For administrators
To require two-factor authentication for everyone, open System settings and enable Enforce two-factor authentication under the Two-factor authentication legend.
Backend users without an active method are then redirected to the security page on every request until they set one up. The redirect deliberately steps aside for a few cases:
- while a two-factor challenge is already running
- while an administrator is impersonating another user
- while a forced password change is pending
- on the security page itself
If a user is locked out after five failed code attempts, their account is disabled. An administrator has to re-enable it in the user management.
How it works
Registration as a two-factor provider
The bundle registers EmailTwoFactorProvider as a scheb_two_factor.provider under the
alias contao_email. Contao's own TOTP provider stays untouched, so both methods can be
offered next to each other.
TotpProviderDecorator decorates Contao's contao.security.two_factor.provider and
suppresses it when the user has no TOTP secret stored. Without that, Contao's provider
would claim every login and the email method would never be reached.
Code lifecycle
| Step | Behaviour |
|---|---|
| Generation | Six digits from random_int(), zero-padded, stored in tl_user |
| Delivery | Contao\Email, subject and body in German or English depending on the user's language |
| Validity | 300 seconds (5 minutes) |
| Comparison | hash_equals(), so the check is not timing-dependent |
| On success | Code, timestamp and attempt counter are cleared |
| On failure | Attempt counter is incremented |
| After 5 failures | Code cleared and the user account is set to disabled |
Code entry during login is handled by Contao's own two-factor screen; the provider intentionally does not register a scheb form renderer.
Enforcement
EnforceTwoFactorListener hooks into kernel.request at priority -1 and only acts on
backend requests. It reads the enforceTwoFactor system setting and redirects to
contao_backend?do=security when neither useTwoFactor nor useEmailTwoFactor is set
for the current user.
Backend integration
The bundle replaces the callback of Contao's existing security module
($GLOBALS['BE_MOD']['accounts']['security']['callback']) with ModuleTwoFactorEmail,
which renders both methods, the switch between them and the password confirmation.
Database
Four columns are added to tl_user, all hidden from the DCA listing and excluded from
record copying:
| Column | Type | Purpose |
|---|---|---|
useEmailTwoFactor |
boolean | Email method active for this user |
emailTwoFactorCode |
varchar(6) | Currently valid code |
emailTwoFactorCodeCreatedAt |
int unsigned | Issue timestamp, used for expiry |
emailTwoFactorFailedAttempts |
int unsigned | Failed attempts since the last valid code |
One setting is added to tl_settings:
| Setting | Purpose |
|---|---|
enforceTwoFactor |
Require every backend user to set up two-factor authentication |
Security notes
Codes sent by email are less secure than TOTP: they travel through the mail system and are only as safe as the recipient's mailbox. Where the threat model calls for it, prefer the authenticator app — this bundle exists to raise the floor for users who would otherwise run without any second factor at all, not to replace TOTP.
Codes are stored in plain text in the database for their five-minute lifetime and are cleared immediately on success. Anyone with database read access during that window could read a pending code.
Translations
German and English are included. Additional languages can be contributed as Contao
language files under contao/languages/<locale>/.
Contributing
Issues and merge requests are welcome. Please keep the coding style consistent with the
existing code (declare(strict_types=1), constructor property promotion, readonly
properties).
License
Licensed under the GNU Lesser General Public License v3.0 or later (LGPL-3.0-or-later), the same license Contao itself uses.
You may use this bundle freely, including in commercial projects and on client sites, and you may distribute it as part of a larger work without that work having to adopt the same license. If you modify the bundle itself and distribute your modified version, those changes have to be made available under the LGPL as well.
The full text is in LICENSE. The LGPL incorporates the terms of the GNU General Public License v3 by reference.
Developed by HI¹ · support@hi1.at


