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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ```
|
||||
|
||||
Reference in New Issue
Block a user