mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2024-11-23 02:27:05 +00:00
TOTP two-factor authentication
This commit is contained in:
parent
1f0345fe0e
commit
6c843fc92e
@ -1,4 +1,4 @@
|
||||
import base64, os, os.path, hmac
|
||||
import base64, os, os.path, hmac, json
|
||||
|
||||
from flask import make_response
|
||||
|
||||
@ -97,6 +97,17 @@ class KeyAuthService:
|
||||
# email address does not correspond to a user.
|
||||
pw_hash = get_mail_password(email, env)
|
||||
|
||||
# If 2FA is set up, get the first factor and authenticate against
|
||||
# that first.
|
||||
twofa = None
|
||||
if pw_hash.startswith("{TOTP}"):
|
||||
twofa = json.loads(pw_hash[6:])
|
||||
pw_hash = twofa["first_factor"]
|
||||
try:
|
||||
pw, twofa_code = pw.split(" ", 1)
|
||||
except:
|
||||
twofa_code = ""
|
||||
|
||||
# Authenticate.
|
||||
try:
|
||||
# Use 'doveadm pw' to check credentials. doveadm will return
|
||||
@ -111,6 +122,14 @@ class KeyAuthService:
|
||||
# Login failed.
|
||||
raise ValueError("Invalid password.")
|
||||
|
||||
# Check second factor.
|
||||
if twofa:
|
||||
import oath
|
||||
ok, drift = oath.accept_totp(twofa["secret"], twofa_code, drift=twofa["drift"])
|
||||
if not ok:
|
||||
raise ValueError("Invalid 2FA code.")
|
||||
|
||||
|
||||
# Get privileges for authorization.
|
||||
|
||||
# (This call should never fail on a valid user. But if it did fail, it would
|
||||
|
@ -7,7 +7,7 @@ from functools import wraps
|
||||
from flask import Flask, request, render_template, abort, Response
|
||||
|
||||
import auth, utils
|
||||
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_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user, get_mail_password
|
||||
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
|
||||
|
||||
@ -40,6 +40,7 @@ def authorized_personnel_only(viewfunc):
|
||||
# Authorized to access an API view?
|
||||
if "admin" in privs:
|
||||
# Call view func.
|
||||
request.user_email = email
|
||||
return viewfunc(*args, **kwargs)
|
||||
elif not error:
|
||||
error = "You are not an administrator."
|
||||
@ -115,6 +116,81 @@ def me():
|
||||
# Return.
|
||||
return json_response(resp)
|
||||
|
||||
# ME
|
||||
|
||||
@app.route('/me/2fa')
|
||||
@authorized_personnel_only
|
||||
def twofa_status():
|
||||
pw = get_mail_password(request.user_email, env)
|
||||
if pw.startswith("{SHA512-CRYPT}"):
|
||||
method = "password-only"
|
||||
elif pw.startswith("{TOTP}"):
|
||||
method = "TOTP 2FA"
|
||||
else:
|
||||
method = "unknown"
|
||||
|
||||
return json_response({
|
||||
"method": method
|
||||
})
|
||||
|
||||
@app.route('/me/2fa/totp/initialize', methods=['POST'])
|
||||
@authorized_personnel_only
|
||||
def twofa_initialize():
|
||||
# Generate a Google Authenticator URI that encodes TOTP info.
|
||||
import urllib.parse, base64, qrcode, io, binascii
|
||||
|
||||
secret = os.urandom(32)
|
||||
uri = "otpauth://totp/%s:%s?secret=%s&issuer=%s&digits=%d&algorithm=%s" % (
|
||||
urllib.parse.quote(env['PRIMARY_HOSTNAME']),
|
||||
urllib.parse.quote(request.user_email),
|
||||
base64.b32encode(secret).decode("ascii").lower().replace("=", ""),
|
||||
urllib.parse.quote(env['PRIMARY_HOSTNAME']),
|
||||
6,
|
||||
"sha1"
|
||||
)
|
||||
|
||||
image_buffer = io.BytesIO()
|
||||
im = qrcode.make(uri)
|
||||
im.save(image_buffer, 'png')
|
||||
|
||||
return json_response({
|
||||
"uri": uri,
|
||||
"secret": binascii.hexlify(secret).decode('ascii'),
|
||||
"qr": base64.b64encode(image_buffer.getvalue()).decode('ascii')
|
||||
})
|
||||
|
||||
@app.route('/me/2fa/totp/activate', methods=['POST'])
|
||||
@authorized_personnel_only
|
||||
def twofa_activate():
|
||||
import oath
|
||||
ok, drift = oath.accept_totp(request.form['secret'], request.form['code'])
|
||||
if ok:
|
||||
# use the user's current plain password as the first_factor
|
||||
# of 2FA.
|
||||
existing_pw = get_mail_password(request.user_email, env)
|
||||
if existing_pw.startswith("{TOTP}"):
|
||||
existing_pw = json.loads(existing_pw)["first_factor"]
|
||||
|
||||
pw = "{TOTP}" + json.dumps({
|
||||
"secret": request.form['secret'],
|
||||
"drift": drift,
|
||||
"first_factor": existing_pw,
|
||||
})
|
||||
|
||||
set_mail_password(request.user_email, pw, env, already_hashed=True)
|
||||
|
||||
return json_response({
|
||||
"status": "ok",
|
||||
"message": "TOTP 2FA installed."
|
||||
})
|
||||
|
||||
else:
|
||||
return json_response({
|
||||
"status": "fail",
|
||||
"message": "The activation code was not right. Try again?"
|
||||
})
|
||||
|
||||
|
||||
# MAIL
|
||||
|
||||
@app.route('/mail/users')
|
||||
|
@ -311,15 +311,16 @@ def add_mail_user(email, pw, privs, env):
|
||||
# Update things in case any new domains are added.
|
||||
return kick(env, "mail user added")
|
||||
|
||||
def set_mail_password(email, pw, env):
|
||||
def set_mail_password(email, pw, env, already_hashed=False):
|
||||
# accept IDNA domain names but normalize to Unicode before going into database
|
||||
email = sanitize_idn_email_address(email)
|
||||
|
||||
# validate that password is acceptable
|
||||
validate_password(pw)
|
||||
|
||||
# hash the password
|
||||
pw = hash_password(pw)
|
||||
if not already_hashed:
|
||||
# Validate and hash the password. Skip if we're providing
|
||||
# a raw hashed password value.
|
||||
validate_password(pw)
|
||||
pw = hash_password(pw)
|
||||
|
||||
# update the database
|
||||
conn, c = open_database(env, with_connection=True)
|
||||
|
79
management/templates/2fa.html
Normal file
79
management/templates/2fa.html
Normal file
@ -0,0 +1,79 @@
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<h2>Two-Factor Authentication</h2>
|
||||
|
||||
<p>Two-factor authentication (2FA) is <i>something you know</i> and <i>something you have</i>.</p>
|
||||
|
||||
<p>Regular password-based logins are one-factor (something you know). 2FA makes an account more secure by guarding against a lost or guessed password, since you also need a special device to access your account. You can turn on 2FA for your account here.</p>
|
||||
|
||||
<p>Your authentication method is currently: <strong id="2fa_current"> </strong></p>
|
||||
|
||||
<h3>TOTP</h3>
|
||||
|
||||
<p>TOTP is a time-based one-time password method of two-factor authentication.</p>
|
||||
|
||||
<p>You will need a TOTP-compatible device, such as any Android device with the <a href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a> app. We’ll generate a QR code that you import into your device or app. After you generate the QR code, you’ll activate 2FA by entering your first activation code provided by your device or app.</p>
|
||||
|
||||
<p><button onclick="totp_initialize()">Generate QR Code</button></p>
|
||||
<div id="totp-form" class="row" style="display: none">
|
||||
<div class="col-sm-6">
|
||||
<center>QR Code</center>
|
||||
<img id="totp_qr_code" src="" class="img-responsive">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<form class="form" role="form" onsubmit="totp_activate(); return false;">
|
||||
<div class="form-group">
|
||||
<label for="inputTOTP" class="control-label">Activation Code</label>
|
||||
<p><input class="form-control" id="inputTOTP" placeholder="enter 6-digit code"></p>
|
||||
<p><input type="submit" class="btn btn-primary" value="Activate"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>When using TOTP 2FA, your password becomes your previous plain password plus a space plus the code generated by your TOTP device.</p>
|
||||
|
||||
<script>
|
||||
function show_2fa() {
|
||||
$('#2fa_current').text('loading...');
|
||||
api(
|
||||
"/me/2fa",
|
||||
"GET",
|
||||
{
|
||||
},
|
||||
function(response) {
|
||||
$('#2fa_current').text(response.method);
|
||||
});
|
||||
}
|
||||
|
||||
var secret = null;
|
||||
|
||||
function totp_initialize() {
|
||||
api(
|
||||
"/me/2fa/totp/initialize",
|
||||
"POST",
|
||||
{
|
||||
},
|
||||
function(response) {
|
||||
$('#totp_qr_code').attr('src', 'data:image/png;base64,' + response.qr);
|
||||
$('#totp-form').fadeIn();
|
||||
secret = response.secret;
|
||||
});
|
||||
}
|
||||
|
||||
function totp_activate() {
|
||||
api(
|
||||
"/me/2fa/totp/activate",
|
||||
"POST",
|
||||
{
|
||||
"secret": secret,
|
||||
"code": $('#inputTOTP').val()
|
||||
},
|
||||
function(response) {
|
||||
show_modal_error("Two-Factor Authentication", $("<pre/>").text(response.message));
|
||||
if (response.status == "OK")
|
||||
$('#totp-form').fadeOut();
|
||||
});
|
||||
}
|
||||
</script>
|
@ -115,6 +115,12 @@
|
||||
</li>
|
||||
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
|
||||
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">You <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#2fa" onclick="return show_panel(this);">Two-Factor Authentication</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
|
||||
@ -168,6 +174,10 @@
|
||||
{% include "ssl.html" %}
|
||||
</div>
|
||||
|
||||
<div id="panel_2fa" class="admin_panel">
|
||||
{% include "2fa.html" %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<footer>
|
||||
|
@ -3,7 +3,10 @@
|
||||
source setup/functions.sh
|
||||
|
||||
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil
|
||||
hide_output pip3 install rtyaml
|
||||
hide_output pip3 install rtyaml
|
||||
|
||||
# For two-factor authentication, the management server uses:
|
||||
hide_output pip3 install git+https://github.com/mail-in-a-box/python-oath qrcode pillow
|
||||
|
||||
# Create a backup directory and a random key for encrypting backups.
|
||||
mkdir -p $STORAGE_ROOT/backup
|
||||
|
Loading…
Reference in New Issue
Block a user