Two-factor verification
This RoundCube plugin adds the 2-step verification (OTP) to the login process., (*1)
It works with all TOTP applications RFC 6238., (*2)
Some code by:, (*3)
, (*4)
Screenshots
, (*5)
, (*6)
Table of Contents
, (*7)
Installation
If you are using Roundcube 1.3.x, please refer to section For version 1.3.x., (*8)
Get the plugin
Method 1: Clone from GitHub inside the plugins directory of Roundcube:, (*9)
1. `cd plugins`
2. `git clone https://github.com/alexandregz/twofactor_gauthenticator.git`
Method 2: Use composer from the Roundcube root directory:, (*10)
composer require alexandregz/twofactor_gauthenticator:dev-master
NOTE: Answer N when the composer ask you about plugin activation., (*11)
Activate the plugin
Activate the plugin by editing the HOME_RC/config/config.inc.php
file:, (*12)
$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., (*13)
Configuration
Copy HOME_RC/plugins/twofactor_gauthenticator/config.inc.php.dist
to
HOME_RC/plugins/twofactor_gauthenticator/config.inc.php
., (*14)
Variables
Variables inside config.inc.php
:, (*15)
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 log_errors_2FA.txt under HOME_RC/logs/log_errors_2FA.txt.
Suggested by @pngd issue 131. |
The tickbox allows users to skip 2FA for 30 days:, (*16)
, (*17)
Variables that only existed in Samefield branch:, (*18)
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) |
, (*19)
Usage
Go to Roundcube Settings > 2-Factor Authentication:, (*20)
, (*21)
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:, (*22)
, (*23)
You can store/create TOTP codes with any authenticator app by either scanning the QR code or entering the secret
manually., (*24)
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., (*25)
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., (*26)
, (*27)
, (*28)
Docker Compose
You can use docker-compose
file to modify and test plugin:, (*29)
- Replace
mail.EXAMPLE.com
for your IMAP and SMTP server.
docker-compose up
- You can use
adminer
to check DB and reset secrets, for example.
, (*30)
Development
Install PHP-CS-Fixer (requires composer
to be installed):, (*31)
composer install --working-dir=./tools/php-cs-fixer
Run the coding standards fixer (in current working directory):, (*32)
./tools/php-cs-fixer/vendor/bin/php-cs-fixer fix .
, (*33)
Author
Alexandre Espinosa Menor aemenor@gmail.com, (*34)
Issues
Just open issues using GitHub issues instead of sending me emails, please.
Gmail usually marks messages like this as SPAMs., (*35)
TOTP Codes
TOTP codes have a 2*30 seconds clock tolerance. (May be editable in future versions), (*36)
License
MIT, see License, (*37)
Notes
Tested with RoundCube 0.9.5 and Google app. Also with Roundcube 1.0.4 and 1.6.9 with OpenAuthenticator., (*38)
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)., (*39)
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, (*40)
Show explained
by Martin Stone, (*41)
Client implementations
You can use various OTP clients
, by helmo., (*42)
, (*43)
Uninstall
To deactivate the plugin, there are two methods:, (*44)
-
For one only: Restore the user prefs from DB to null (rouncubeDB.users.preferences) which the user plugin options
stored., (*45)
-
To all: Remove the plugin from config.inc.php thus remove the plugin itself., (*46)
, (*47)
For version 1.3.x
Use 1.3.9-version branch, (*48)
$ git checkout 1.3.9-version
, (*49)
If you are using other versions other than 1.3.9, use master version normally (thanks
to tborgans), (*50)
, (*51)
, (*52)
Security incidents
2022-04-02
Reported by kototilt@haiiro.dev (thx for the report and the PoC script), (*53)
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., (*54)
NOTE: I also checked if the user has 2FA activated because with only first condition -check SESSION- app kick me out
before activating 2FA., (*55)
, (*56)
Function twofactor_gauthenticator_save()
, (*57)
On function twofactor_gauthenticator_save()
I added this code:, (*58)
// 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
), (*59)
, (*60)
Tests with PoC Python Script, (*61)
Previously, with security compromised:, (*62)
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):, (*63)
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