alexandregz / twofactor_gauthenticator
This RoundCube plugin adds the Google 2-step verification to the login proccess (OTP)
Package info
github.com/alexandregz/twofactor_gauthenticator
Type:roundcube-plugin
pkg:composer/alexandregz/twofactor_gauthenticator
Requires
- php: >=5.3.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 00:04:06 UTC
README
This RoundCube plugin adds the 2-step verification (OTP) to the login process.
It works with all TOTP applications RFC 6238.
Some code by:
- Ricardo Signes
- Justin Buchanan
- Ricardo Iván Vieitez Parra
- GoogleAuthenticator class by Michael Kliewe (to see secrets)
- qrcode.js by ShimSangmin
- Also thanks to Victor R. Rodriguez Dominguez for some ideas and support
- Stephen K. Gielda
- Manuel Delgado
- kototilt@haiiro.dev
Screenshots
Table of Contents
- Installation
- Usage
- Docker Compose
- Development
- Additional Information
- Uninstall
- For version 1.3.x
- Security incidents
Installation
If you are using Roundcube 1.3.x, please refer to section For version 1.3.x.
Get the plugin
Method 1: Clone from GitHub inside the plugins directory of Roundcube:
1. `cd plugins`
2. `git clone https://github.com/alexandregz/twofactor_gauthenticator.git`
Method 2: Use composer from the Roundcube root directory:
composer require alexandregz/twofactor_gauthenticator:dev-master
NOTE: Answer N when the composer ask you about plugin activation.
Activate the plugin
Activate the plugin by editing the HOME_RC/config/config.inc.php file:
$config['plugins'] = [ // Other plugins... 'twofactor_gauthenticator', ];
NOTE: For docker user, add env ROUNDCUBE_PLUGINS=twofactor_gauthenticator into docker-compose file. For detailed
information, see Roundcube Docker Hub.
Configuration
Copy HOME_RC/plugins/twofactor_gauthenticator/config.inc.php.dist to
HOME_RC/plugins/twofactor_gauthenticator/config.inc.php.
Variables
Variables inside config.inc.php:
| Variable | Variable Type | Default Value | Description |
|---|---|---|---|
force_enrollment_users |
boolean | false | If true, all users must log in with 2-step verification. They will receive an alert message and cannot skip the configuration. |
whitelist |
array | N/A | A Whitelist of IPs which are allowed to bypass the 2FA, CIDR format available. NOTE: We need to use .0 IP to define LAN because the class CIDR have a issue about that (we can't use 129.168.1.2/24, for example). NOTE2: To create a empty whitelist, make sure it looks like this: $rcmail_config['whitelist'] = array(); <- There are NO QUOTES inside the parentheses. |
allow_save_device_30days |
boolean | false | If true, there will be a checkbox in the the TOTP code prompting page. By ticking it, there will be no 2FA prompt for 30 days. |
twofactor_formfield_as_password |
boolean | false | If true, the entered TOTP code will appear as password in the webpage when prompting it. Otherwise, it'll shown as text. |
users_allowed_2FA |
array | N/A | Users allowed to use plugin (IMPORTANT: other users DON'T have plugin activated). Regex is supported. NOTE: plugin must be base32 valid characters ([A-Z][2-7]), see PHPGansta Library, from Issues 139. |
enable_fail_logs |
boolean | false | If true, 2FA failure will be logged in file twofactor_gauthenticator.log under HOME_RC/logs/. Suggested by @pngd issue 131. Code by @valarauco #227 |
twofactor_pref_encrypt |
boolean | false | If true, twofactor user preferences (secret and codes) will be encrypted with Roundcube's DES key Code by @valarauco #225 NOTE: when prefs are encrypted and you set this variable to false, you enter without twofactor authentication. Encryption it's not reversible. |
| `twofactor_lockout_delays` | int[] / array | [1,2,5,20,60,300,600] | Incremental wait (seconds) applied after the N-th accumulated failed 2FA code; each value clamped to ≥ 1, levels beyond the list use the LAST value. |
| `twofactor_lockout` | int | 900 | Counter-expiry window (seconds; resets the failure level). If the last failure is older than this window, the level resets to 0 before the next attempt is counted. |
| `twofactor_trusted_proxies` | array | empty | List of trusted reverse-proxies (IPs / CIDR) whose X-Forwarded-For header is honored for the IP whitelist decision and the rate-limit key. Empty default disables header trust (REMOTE_ADDR only); HTTP_CLIENT_IP is never used anywhere in the decision path. |
The tickbox allows users to skip 2FA for 30 days:
Variables that only existed in Samefield branch:
| Variable | Variable Type | Default Value | Description |
|---|---|---|---|
2step_codes_on_login_form |
boolean | false | If config value 2step_codes_on_login_form is true, 2-step codes (and recovery) must be sended with password value, append to this, from the login screen: "Normal" codes just following password (passswordCODE), recovery codes after two pipes (passsword||RECOVERYCODE) |
Usage
Go to Roundcube Settings > 2-Factor Authentication:
The most easy way to configure it is by clicking "Fill all fields". The plugin automatically creates the secret as well as the recovery codes for you:
You can store/create TOTP codes with any authenticator app by either scanning the QR code or entering the secret manually.
Manually entering the secret as well as recovery codes is also possible. Note that the recovery codes are optional, so you can leave them blank.
After setting up the authenticator, enter the code and press "Check code". If the code is correct, you can press "Save" to save the configuration and enable 2-step verification.
Docker Compose
You can use docker-compose file to modify and test plugin:
- Replace
mail.EXAMPLE.comfor your IMAP and SMTP server. docker-compose up- You can use
adminerto check DB and reset secrets, for example.
Development
Code formatting
Install PHP-CS-Fixer (requires composer to be installed):
composer install --working-dir=./tools/php-cs-fixer
Run the coding standards fixer (in current working directory):
./tools/php-cs-fixer/vendor/bin/php-cs-fixer fix .
Additional Information
Author
Alexandre Espinosa Menor aemenor@gmail.com
Issues
Just open issues using GitHub issues instead of sending me emails, please. Gmail usually marks messages like this as SPAMs.
TOTP Codes
TOTP codes have a 2*30 seconds clock tolerance. (May be editable in future versions)
License
MIT, see License
Notes
Tested with RoundCube 0.9.5 and Google app. Also with Roundcube 1.0.4 and 1.6.9 with OpenAuthenticator, Authy, etc.
Remember, time synchronization it's essential to TOTP: "For this to work, the clocks of the user's device and the server need to be roughly synchronized (the server will typically accept one-time passwords generated from timestamps that differ by ±1 from the client's timestamp)" ( from Wikipedia: Time-based one-time password).
Testing
- Vagrant: https://github.com/alexandregz/vagrant-twofactor_gauthenticator
- Docker: https://hub.docker.com/r/alexandregz/twofactor_gauthenticator/
Using with Kolab
Add a symlink into the public_html/assets directory
Show explained by Martin Stone
Client implementations
You can use various OTP clients , by helmo.
Uninstall
To deactivate the plugin, there are two methods:
-
For one only: Restore the user prefs from DB to null (rouncubeDB.users.preferences) which the user plugin options stored.
-
To all: Remove the plugin from config.inc.php thus remove the plugin itself.
For version 1.3.x
Use 1.3.9-version branch
$ git checkout 1.3.9-version
If you are using other versions other than 1.3.9, use master version normally (thanks to tborgans)
Security incidents
Security
Hardening measures added for the recommended remediation of the findings below (plugin 2.x):
- V-1 · Rate limiting. Failed 2FA code attempts on both verification paths (the login POST in
check_2FAlogin()and the AJAX self-test endpointplugin.twofactor_gauthenticator-checkcode) are recorded in a persistent per-key JSON state file in the Roundcubelog_dir(one file per (IP, username) key at{log_dir}/twofactor_gauth_fail_{md5key}.json, written withfile_put_contents(..., LOCK_EX)), keyed by the resolved client IP + authenticated username, that survives session teardown and accumulates across consecutive logins. Every failed code at the login path immediately performs the full server-side logout (original plugin behavior restored) after recording the failure in the counter; attempts submitted while blocked are rejected without verification or incrementing. The blocked wait grows incrementally viatwofactor_lockout_delays(default[1, 2, 5, 20, 60, 300, 600]seconds; the last value is used beyond the list; each ≥ 1s). The AJAX self-test counts a failure but never logs out (the session stays alive; the lockout is enforced at the login path only). The counter resets on a successful 2FA and after thetwofactor_lockoutcounter-expiry window (default 900s). Note:twofactor_max_attemptsis replaced bytwofactor_lockout_delays.enable_fail_logsdoes not gate counting or eviction. - V-2 · Server-side session teardown.
__exitSession()now destroys the session server-side viarcmail::kill_session()(stored session destroyed, session ID invalidated so it cannot be resumed, Roundcube session cookie cleared,$_SESSIONreset), clears the plugin 30-day remember-me cookie, drops the plugin session markers, and redirects to login only as a UI courtesy. The redirect is no longer the destruction mechanism and no request token is required. This uniform teardown applies to every caller: the failed-attempt threshold, the V-4 partial-auth branch, and thesave()anti-bypass gate. - V-3 · IP-whitelist trust. The whitelist decision now resolves the client IP via a single
__getClientIP()helper that defaults toREMOTE_ADDR(the real TCP peer).HTTP_CLIENT_IPis removed entirely andX-Forwarded-Foris ignored unless a newtwofactor_trusted_proxiesoption (default empty) explicitly lists the connecting peer; then the forwarded chain is parsed right-to-left to the first non-trusted hop, defeating prepending of a whitelisted value. Migration note: deployments that whitelist clients behind a reverse proxy MUST settwofactor_trusted_proxiesto include that proxy, otherwise their whitelist stops matching (the secure-by-default tradeoff). - V-4 · Freshness precedence fix. The dead
elseifincheck_2FAlogin()(operator-precedence bug) is re-enabled and parses to the intended semantics, and the invariant "2FA complete only whentwofactor_gauthenticator_2FA_loginis set and>=twofactor_gauthenticator_login" is centralized in a single__is2FAFresh()helper reused byinit(),check_2FAlogin(), andtwofactor_gauthenticator_save(). Whitelisted and remember-me/cookie users who legitimately skip the 2FA marker are not falsely evicted (the whitelist and cookie early-return paths precede the freshness check).
2022-04-02
Reported by kototilt@haiiro.dev (thx for the report and the PoC script)
I made a little modification to the script to disallow user to save config without param session generated from a rendered page to force user to introduce previously 2FA code and navigate across site.
NOTE: I also checked if the user has 2FA activated because with only first condition -check SESSION- app kick me out before activating 2FA.
Function twofactor_gauthenticator_save()
On function twofactor_gauthenticator_save() I added this code:
// save config function twofactor_gauthenticator_save() { $rcmail = rcmail::get_instance(); // 2022-04-03: Corrected security incidente reported by kototilt@haiiro.dev // "2FA in twofactor_gauthenticator can be bypassed allowing an attacker to disable 2FA or change the TOTP secret." // // Solution: if user don't have session created by any rendered page, we kick out $config_2FA = self::__get2FAconfig(); if(!$_SESSION['twofactor_gauthenticator_2FA_login'] && $config_2FA['activate']) { $this->__exitSession(); }
The idea is to create a session variable from a rendered page, redirected from __goingRoundcubeTask function (
redirector to roundcube tasks)
Tests with PoC Python Script
Previously, with security compromised:
alex@vosjod:~/Desktop/report$ ./poc.py Password:xxxxxxxx 1. Fetching login page (http://localhost:8888/roundcubemail-1.4.8) 2. Logging in POST http://localhost:8888/roundcubemail-1.4.8/?_task=login 3. Disabling 2FA POST http://localhost:8888/roundcubemail-1.4.8/?_task=settings&_action=plugin.twofactor_gauthenticator-save POST returned task "settings" 2FA disabled!
Modified code and tested again, not allowed to deactivated/modified without going to a RC task (with 2FA authentication):
alex@vosjod:~/Desktop/report$ ./poc.py Password:xxxxxxxxx 1. Fetching login page (http://localhost:8888/roundcubemail-1.4.8) 2. Logging in POST http://localhost:8888/roundcubemail-1.4.8/?_task=login 3. Disabling 2FA POST http://localhost:8888/roundcubemail-1.4.8/?_task=settings&_action=plugin.twofactor_gauthenticator-save POST returned task "login" Expected "settings" task, something went wrong







