1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-06 16:07:22 +01:00

Add TOTP two-factor authentication to admin panel login (#1814)

* add user interface for managing 2fa

* update user schema with 2fa columns

* implement two factor check during login

* Use pyotp for validating TOTP codes

* also implements resynchronisation support via `pyotp`'s `valid_window option

* Update API route naming, update setup page

* Rename /two-factor-auth/ => /2fa/
* Nest totp routes under /2fa/totp/
* Update ids and methods in panel to allow for different setup types

* Autofocus otp input when logging in, update layout

* Extract TOTPStrategy class to totp.py

* this decouples `TOTP` validation and storage logic from `auth` and moves it to `totp`
* reduce `pyotp.validate#valid_window` from `2` to `1`

* Update OpenApi docs, rename /2fa/ => /mfa/

* Decouple totp from users table by moving to totp_credentials table

* this allows implementation of other mfa schemes in the future (webauthn)
* also makes key management easier and enforces one totp credentials per user on db-level

* Add sqlite migration

* Rename internal validate_two_factor_secret => validate_two_factor_secret

* conn.close() if mru_token update can't .commit()

* Address review feedback, thanks @hija

* Use hmac.compare_digest() to compare mru_token

* Safeguard against empty mru_token column

* hmac.compare_digest() expects arguments of type string, make sure we don't pass None
 * Currently, this cannot happen but we might not want to store `mru_token` during setup

* Do not log failed login attempts for MissingToken errors

* Due to the way that the /login UI works, this persists at least one failed login each time a user logs into the admin panel. This in turn triggers fail2ban at some point.

* Add TOTP secret to user_key hash

thanks @downtownallday
* this invalidates all user_keys after TOTP status is changed for user
* after changing TOTP state, a login is required
* due to the forced login, we can't and don't need to store the code used for setup in `mru_code`

* Typo

* Reorganize the MFA backend methods

* Reorganize MFA front-end and add label column

* Fix handling of bad input when enabling mfa

* Update openAPI docs

* Remove unique key constraint on foreign key user_id in mfa table

* Don't expose mru_token and secret for enabled mfas over HTTP

* Only update mru_token for matched mfa row

* Exclude mru_token in user key hash

* Rename tools/mail.py to management/cli.py

* Add MFA list/disable to the management CLI so admins can restore access if MFA device is lost

Co-authored-by: Joshua Tauberer <jt@occams.info>
This commit is contained in:
Joshua Tauberer
2020-10-31 10:27:38 -04:00
committed by GitHub
15 changed files with 912 additions and 200 deletions

View File

@@ -1,6 +1,6 @@
# If there aren't any mail users yet, create one.
if [ -z "`tools/mail.py user`" ]; then
# The outut of "tools/mail.py user" is a list of mail users. If there
if [ -z "`management/cli.py user`" ]; then
# The outut of "management/cli.py user" is a list of mail users. If there
# aren't any yet, it'll be empty.
# If we didn't ask for an email address at the start, do so now.
@@ -47,11 +47,11 @@ if [ -z "`tools/mail.py user`" ]; then
fi
# Create the user's mail account. This will ask for a password if none was given above.
tools/mail.py user add $EMAIL_ADDR ${EMAIL_PW:-}
management/cli.py user add $EMAIL_ADDR ${EMAIL_PW:-}
# Make it an admin.
hide_output tools/mail.py user make-admin $EMAIL_ADDR
hide_output management/cli.py user make-admin $EMAIL_ADDR
# Create an alias to which we'll direct all automatically-created administrative aliases.
tools/mail.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null
management/cli.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null
fi

View File

@@ -22,6 +22,7 @@ if [ ! -f $db_path ]; then
echo Creating new user database: $db_path;
echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path;
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path;
echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 $db_path;
fi
# ### User Authentication

View File

@@ -50,6 +50,7 @@ hide_output $venv/bin/pip install --upgrade pip
hide_output $venv/bin/pip install --upgrade \
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
flask dnspython python-dateutil \
qrcode[pil] pyotp \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver
# CONFIGURATION

View File

@@ -181,6 +181,12 @@ def migration_12(env):
conn.commit()
conn.close()
def migration_13(env):
# Add the "mfa" table for configuring MFA for login to the control panel.
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);"])
###########################################################
def get_current_migration():
ver = 0

View File

@@ -352,7 +352,7 @@ rm -f /etc/cron.hourly/mailinabox-owncloud
# and there's a lot they could mess up, so we don't make any users admins of Nextcloud.
# But if we wanted to, we would do this:
# ```
# for user in $(tools/mail.py user admins); do
# for user in $(management/cli.py user admins); do
# sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "INSERT OR IGNORE INTO oc_group_user VALUES ('admin', '$user')"
# done
# ```