diff --git a/management/daemon.py b/management/daemon.py index 55c0a3ec..9f56df8c 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -9,7 +9,7 @@ import auth, utils, totp from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias -from mailconfig import get_two_factor_info, set_two_factor_secret, remove_two_factor_secret +from mailconfig import get_mfa_state, create_totp_credential, delete_totp_credential env = utils.load_environment() @@ -410,18 +410,17 @@ def ssl_provision_certs(): requests = provision_certificates(env, limit_domains=None) return json_response({ "requests": requests }) -# Two Factor Auth +# multi-factor auth @app.route('/mfa/status', methods=['GET']) @authorized_personnel_only def two_factor_auth_get_status(): email, _ = auth_service.authenticate(request, env) - two_factor_secret, _ = get_two_factor_info(email, env) - if two_factor_secret != None: - return json_response({ - "type": 'totp' - }) + mfa_state = get_mfa_state(email, env) + + if mfa_state['type'] == 'totp': + return json_response({ "type": 'totp' }) secret = totp.get_secret() secret_url = totp.get_otp_uri(secret, email) @@ -446,7 +445,7 @@ def totp_post_enable(): return json_response({ "error": 'bad_input' }, 400) if (totp.validate(secret, token)): - set_two_factor_secret(email, secret, token, env) + create_totp_credential(email, secret, token, env) return json_response({}) return json_response({ "error": 'token_mismatch' }, 400) @@ -455,7 +454,7 @@ def totp_post_enable(): @authorized_personnel_only def totp_post_disable(): email, _ = auth_service.authenticate(request, env) - remove_two_factor_secret(email, env) + delete_totp_credential(email, env) return json_response({}) # WEB diff --git a/management/mailconfig.py b/management/mailconfig.py index 3bc48897..227c7cdf 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -547,38 +547,42 @@ def get_required_aliases(env): return aliases -def get_two_factor_info(email, env): +# multi-factor auth + +def get_mfa_state(email, env): c = open_database(env) + c.execute('SELECT secret, mru_token FROM totp_credentials WHERE user_email=?', (email,)) - c.execute('SELECT two_factor_secret, two_factor_last_used_token FROM users WHERE email=?', (email,)) - rows = c.fetchall() - if len(rows) != 1: - raise ValueError("That's not a user (%s)." % email) - return (rows[0][0], rows[0][1]) + credential_row = c.fetchone() + if (credential_row == None): + return { 'type': None } -def set_two_factor_secret(email, secret, token, env): + return { + 'type': 'totp', + 'secret': credential_row[0], + 'mru_token': credential_row[1] + } + +def create_totp_credential(email, secret, token, env): validate_two_factor_secret(secret) conn, c = open_database(env, with_connection=True) - c.execute("UPDATE users SET two_factor_secret=?, two_factor_last_used_token=? WHERE email=?", (secret, token, email)) + c.execute('INSERT INTO totp_credentials (user_email, secret, mru_token) VALUES (?, ?, ?)', (email, secret, token)) + conn.commit() + return "OK" + +def set_mru_totp_code(email, token, env): + conn, c = open_database(env, with_connection=True) + c.execute('UPDATE totp_credentials SET mru_token=? WHERE user_email=?', (token, email)) + if c.rowcount != 1: raise ValueError("That's not a user (%s)." % email) conn.commit() return "OK" -def set_two_factor_last_used_token(email, token, env): +def delete_totp_credential(email, env): conn, c = open_database(env, with_connection=True) - c.execute("UPDATE users SET two_factor_last_used_token=? WHERE email=?", (token, email)) - if c.rowcount != 1: - raise ValueError("That's not a user (%s)." % email) - conn.commit() - return "OK" - -def remove_two_factor_secret(email, env): - conn, c = open_database(env, with_connection=True) - c.execute("UPDATE users SET two_factor_secret=null, two_factor_last_used_token=null WHERE email=?", (email,)) - if c.rowcount != 1: - raise ValueError("That's not a user (%s)." % email) + c.execute('DELETE FROM totp_credentials WHERE user_email=?', (email,)) conn.commit() return "OK" diff --git a/management/templates/index.html b/management/templates/index.html index 7f8a1e32..7c346e37 100644 --- a/management/templates/index.html +++ b/management/templates/index.html @@ -93,7 +93,7 @@
  • Custom DNS
  • External DNS
  • -
  • Two Factor Authentication
  • +
  • Two-Factor Authentication
  • Munin Monitoring
  • diff --git a/management/templates/login.html b/management/templates/login.html index 8d8bd394..4d6155f6 100644 --- a/management/templates/login.html +++ b/management/templates/login.html @@ -72,7 +72,7 @@ sudo tools/mail.py user make-admin me@{{hostname}}
    - +
    diff --git a/management/templates/two-factor-auth.html b/management/templates/two-factor-auth.html index 6a6b6c31..bb519e46 100644 --- a/management/templates/two-factor-auth.html +++ b/management/templates/two-factor-auth.html @@ -31,7 +31,7 @@ } -

    Two Factor Authentication

    +

    Two-Factor Authentication

    Loading...
    diff --git a/management/totp.py b/management/totp.py index 2cb1b148..2cea61b8 100644 --- a/management/totp.py +++ b/management/totp.py @@ -6,7 +6,7 @@ import struct import time import pyotp import qrcode -from mailconfig import get_two_factor_info, set_two_factor_last_used_token +from mailconfig import get_mfa_state, set_mru_totp_code def get_secret(): return base64.b32encode(os.urandom(20)).decode('utf-8') @@ -45,13 +45,13 @@ class TOTPStrategy(): self.email = email def store_successful_login(self, token, env): - return set_two_factor_last_used_token(self.email, token, env) + return set_mru_totp_code(self.email, token, env) def validate_request(self, request, env): - secret, mru_token = get_two_factor_info(self.email, env) + mfa_state = get_mfa_state(self.email, env) # 2FA is not enabled, we can skip further checks - if secret == "" or secret == None: + if mfa_state['type'] != 'totp': return True # If 2FA is enabled, raise if: @@ -65,7 +65,7 @@ class TOTPStrategy(): raise MissingTokenError("Two factor code missing (no x-auth-token supplied)") # TODO: Should a token replay be handled as its own error? - if token_header == mru_token or validate(secret, token_header) != True: + if token_header == mfa_state['mru_token'] or validate(mfa_state['secret'], token_header) != True: raise BadTokenError("Two factor code incorrect") self.store_successful_login(token_header, env) diff --git a/setup/mail-users.sh b/setup/mail-users.sh index 3047489b..14e1d9df 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -20,9 +20,10 @@ db_path=$STORAGE_ROOT/mail/users.sqlite # Create an empty database if it doesn't yet exist. if [ ! -f $db_path ]; then echo Creating new user database: $db_path; - # TODO: Add migration - echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '', two_factor_secret TEXT, two_factor_last_used_token TEXT);" | sqlite3 $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; + # TODO: Add migration + echo "CREATE TABLE totp_credentials (id INTEGER PRIMARY KEY AUTOINCREMENT, user_email TEXT NOT NULL UNIQUE, secret TEXT NOT NULL, mru_token TEXT, FOREIGN KEY (user_email) REFERENCES users(email) ON DELETE CASCADE);" | sqlite3 $db_path; fi # ### User Authentication