mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2024-11-24 02:37:05 +00:00
implement two factor check during login
This commit is contained in:
parent
a7a66929aa
commit
3c3683429b
@ -2,12 +2,19 @@ import base64, os, os.path, hmac
|
|||||||
|
|
||||||
from flask import make_response
|
from flask import make_response
|
||||||
|
|
||||||
import utils
|
import utils, totp
|
||||||
from mailconfig import get_mail_password, get_mail_user_privileges
|
from mailconfig import get_mail_password, get_mail_user_privileges
|
||||||
|
from mailconfig import get_two_factor_info, set_two_factor_last_used_token
|
||||||
|
|
||||||
DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key'
|
DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key'
|
||||||
DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server'
|
DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server'
|
||||||
|
|
||||||
|
class MissingTokenError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BadTokenError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
class KeyAuthService:
|
class KeyAuthService:
|
||||||
"""Generate an API key for authenticating clients
|
"""Generate an API key for authenticating clients
|
||||||
|
|
||||||
@ -76,23 +83,52 @@ class KeyAuthService:
|
|||||||
return (None, ["admin"])
|
return (None, ["admin"])
|
||||||
else:
|
else:
|
||||||
# The user is trying to log in with a username and user-specific
|
# The user is trying to log in with a username and user-specific
|
||||||
# API key or password. Raises or returns privs.
|
# API key or password. Raises or returns privs and an indicator
|
||||||
return (username, self.get_user_credentials(username, password, env))
|
# whether the user is using their password or a user-specific API-key.
|
||||||
|
privs, is_user_key = self.get_user_credentials(username, password, env)
|
||||||
|
|
||||||
|
# If the user is using their API key to login, 2FA has been passed before
|
||||||
|
if is_user_key:
|
||||||
|
return (username, privs)
|
||||||
|
|
||||||
|
secret, last_token = get_two_factor_info(username, env)
|
||||||
|
|
||||||
|
# 2FA is not enabled, we can skip further checks
|
||||||
|
if secret == "" or secret == None:
|
||||||
|
return (username, privs)
|
||||||
|
|
||||||
|
# If 2FA is enabled, raise if:
|
||||||
|
# 1. no token is provided via `x-auth-token`
|
||||||
|
# 2. a previously supplied token is used (to counter replay attacks)
|
||||||
|
# 3. the token is invalid
|
||||||
|
# in that case, we need to raise and indicate to the client to supply a TOTP
|
||||||
|
token_header = request.headers.get('x-auth-token')
|
||||||
|
if token_header == None or token_header == "":
|
||||||
|
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 == last_token or totp.validate(secret, token_header) != True:
|
||||||
|
raise BadTokenError("Two factor code incorrect")
|
||||||
|
|
||||||
|
set_two_factor_last_used_token(username, token_header, env)
|
||||||
|
return (username, privs)
|
||||||
|
|
||||||
def get_user_credentials(self, email, pw, env):
|
def get_user_credentials(self, email, pw, env):
|
||||||
# Validate a user's credentials. On success returns a list of
|
# Validate a user's credentials. On success returns a list of
|
||||||
# privileges (e.g. [] or ['admin']). On failure raises a ValueError
|
# privileges (e.g. [] or ['admin']). On failure raises a ValueError
|
||||||
# with a login error message.
|
# with a login error message.
|
||||||
|
|
||||||
# Sanity check.
|
# Sanity check.
|
||||||
if email == "" or pw == "":
|
if email == "" or pw == "":
|
||||||
raise ValueError("Enter an email address and password.")
|
raise ValueError("Enter an email address and password.")
|
||||||
|
|
||||||
|
is_user_key = False
|
||||||
|
|
||||||
# The password might be a user-specific API key. create_user_key raises
|
# The password might be a user-specific API key. create_user_key raises
|
||||||
# a ValueError if the user does not exist.
|
# a ValueError if the user does not exist.
|
||||||
if hmac.compare_digest(self.create_user_key(email, env), pw):
|
if hmac.compare_digest(self.create_user_key(email, env), pw):
|
||||||
# OK.
|
# OK.
|
||||||
pass
|
is_user_key = True
|
||||||
else:
|
else:
|
||||||
# Get the hashed password of the user. Raise a ValueError if the
|
# Get the hashed password of the user. Raise a ValueError if the
|
||||||
# email address does not correspond to a user.
|
# email address does not correspond to a user.
|
||||||
@ -119,7 +155,7 @@ class KeyAuthService:
|
|||||||
if isinstance(privs, tuple): raise ValueError(privs[0])
|
if isinstance(privs, tuple): raise ValueError(privs[0])
|
||||||
|
|
||||||
# Return a list of privileges.
|
# Return a list of privileges.
|
||||||
return privs
|
return (privs, is_user_key)
|
||||||
|
|
||||||
def create_user_key(self, email, env):
|
def create_user_key(self, email, env):
|
||||||
# Store an HMAC with the client. The hashed message of the HMAC will be the user's
|
# Store an HMAC with the client. The hashed message of the HMAC will be the user's
|
||||||
|
@ -40,14 +40,23 @@ def authorized_personnel_only(viewfunc):
|
|||||||
error = None
|
error = None
|
||||||
try:
|
try:
|
||||||
email, privs = auth_service.authenticate(request, env)
|
email, privs = auth_service.authenticate(request, env)
|
||||||
|
except auth.MissingTokenError as e:
|
||||||
|
privs = []
|
||||||
|
error = str(e)
|
||||||
|
except auth.BadTokenError as e:
|
||||||
|
# Write a line in the log recording the failed login
|
||||||
|
log_failed_login(request)
|
||||||
|
|
||||||
|
privs = []
|
||||||
|
error = str(e)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
# Write a line in the log recording the failed login
|
||||||
|
log_failed_login(request)
|
||||||
|
|
||||||
# Authentication failed.
|
# Authentication failed.
|
||||||
privs = []
|
privs = []
|
||||||
error = "Incorrect username or password"
|
error = "Incorrect username or password"
|
||||||
|
|
||||||
# Write a line in the log recording the failed login
|
|
||||||
log_failed_login(request)
|
|
||||||
|
|
||||||
# Authorized to access an API view?
|
# Authorized to access an API view?
|
||||||
if "admin" in privs:
|
if "admin" in privs:
|
||||||
# Call view func.
|
# Call view func.
|
||||||
@ -119,6 +128,23 @@ def me():
|
|||||||
# Is the caller authorized?
|
# Is the caller authorized?
|
||||||
try:
|
try:
|
||||||
email, privs = auth_service.authenticate(request, env)
|
email, privs = auth_service.authenticate(request, env)
|
||||||
|
except auth.MissingTokenError as e:
|
||||||
|
# Log the failed login
|
||||||
|
log_failed_login(request)
|
||||||
|
|
||||||
|
return json_response({
|
||||||
|
"status": "missing_token",
|
||||||
|
"reason": str(e),
|
||||||
|
})
|
||||||
|
except auth.BadTokenError as e:
|
||||||
|
# Log the failed login
|
||||||
|
log_failed_login(request)
|
||||||
|
|
||||||
|
return json_response({
|
||||||
|
"status": "bad_token",
|
||||||
|
"reason": str(e),
|
||||||
|
})
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# Log the failed login
|
# Log the failed login
|
||||||
log_failed_login(request)
|
log_failed_login(request)
|
||||||
@ -126,7 +152,7 @@ def me():
|
|||||||
return json_response({
|
return json_response({
|
||||||
"status": "invalid",
|
"status": "invalid",
|
||||||
"reason": "Incorrect username or password",
|
"reason": "Incorrect username or password",
|
||||||
})
|
})
|
||||||
|
|
||||||
resp = {
|
resp = {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
|
@ -297,7 +297,7 @@ function ajax_with_indicator(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var api_credentials = ["", ""];
|
var api_credentials = ["", ""];
|
||||||
function api(url, method, data, callback, callback_error) {
|
function api(url, method, data, callback, callback_error, headers) {
|
||||||
// from http://www.webtoolkit.info/javascript-base64.html
|
// from http://www.webtoolkit.info/javascript-base64.html
|
||||||
function base64encode(input) {
|
function base64encode(input) {
|
||||||
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||||
@ -335,7 +335,7 @@ function api(url, method, data, callback, callback_error) {
|
|||||||
method: method,
|
method: method,
|
||||||
cache: false,
|
cache: false,
|
||||||
data: data,
|
data: data,
|
||||||
|
headers: headers,
|
||||||
// the custom DNS api sends raw POST/PUT bodies --- prevent URL-encoding
|
// the custom DNS api sends raw POST/PUT bodies --- prevent URL-encoding
|
||||||
processData: typeof data != "string",
|
processData: typeof data != "string",
|
||||||
mimeType: typeof data == "string" ? "text/plain; charset=ascii" : null,
|
mimeType: typeof data == "string" ? "text/plain; charset=ascii" : null,
|
||||||
|
@ -1,4 +1,29 @@
|
|||||||
<h1 style="margin: 1em; text-align: center">{{hostname}}</h1>
|
<style>
|
||||||
|
.title {
|
||||||
|
margin: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 32em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #loginOtp {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginForm.twofactor #loginOtp {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h1 class="title">{{hostname}}</h1>
|
||||||
|
|
||||||
{% if no_users_exist or no_admins_exist %}
|
{% if no_users_exist or no_admins_exist %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -20,10 +45,10 @@ sudo tools/mail.py user make-admin me@{{hostname}}</pre>
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p style="margin: 2em; text-align: center;">Log in here for your Mail-in-a-Box control panel.</p>
|
<p class="subtitle">Log in here for your Mail-in-a-Box control panel.</p>
|
||||||
|
|
||||||
<div style="margin: 0 auto; max-width: 32em;">
|
<div class="login">
|
||||||
<form class="form-horizontal" role="form" onsubmit="do_login(); return false;" method="get">
|
<form id="loginForm" class="form-horizontal" role="form" onsubmit="do_login(); return false;" method="get">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputEmail3" class="col-sm-3 control-label">Email</label>
|
<label for="inputEmail3" class="col-sm-3 control-label">Email</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
@ -45,6 +70,12 @@ sudo tools/mail.py user make-admin me@{{hostname}}</pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" id="loginOtp">
|
||||||
|
<label for="loginOtpInput" class="col-sm-3 control-label">Two Factor Code</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="text" class="form-control" id="loginOtpInput" placeholder="123456">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-3 col-sm-9">
|
<div class="col-sm-offset-3 col-sm-9">
|
||||||
<button type="submit" class="btn btn-default">Sign in</button>
|
<button type="submit" class="btn btn-default">Sign in</button>
|
||||||
@ -53,7 +84,6 @@ sudo tools/mail.py user make-admin me@{{hostname}}</pre>
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function do_login() {
|
function do_login() {
|
||||||
if ($('#loginEmail').val() == "") {
|
if ($('#loginEmail').val() == "") {
|
||||||
@ -75,17 +105,21 @@ function do_login() {
|
|||||||
api(
|
api(
|
||||||
"/me",
|
"/me",
|
||||||
"GET",
|
"GET",
|
||||||
{ },
|
{},
|
||||||
function(response){
|
function(response) {
|
||||||
// This API call always succeeds. It returns a JSON object indicating
|
// This API call always succeeds. It returns a JSON object indicating
|
||||||
// whether the request was authenticated or not.
|
// whether the request was authenticated or not.
|
||||||
if (response.status != "ok") {
|
if (response.status != 'ok') {
|
||||||
// Show why the login failed.
|
if (response.status === 'missing_token' && !$('#loginForm').hasClass('twofactor')) {
|
||||||
show_modal_error("Login Failed", response.reason)
|
$('#loginForm').addClass('twofactor');
|
||||||
|
} else {
|
||||||
// Reset any saved credentials.
|
$('#loginForm').removeClass('twofactor');
|
||||||
do_logout();
|
// Show why the login failed.
|
||||||
|
show_modal_error("Login Failed", response.reason)
|
||||||
|
|
||||||
|
// Reset any saved credentials.
|
||||||
|
do_logout();
|
||||||
|
}
|
||||||
} else if (!("api_key" in response)) {
|
} else if (!("api_key" in response)) {
|
||||||
// Login succeeded but user might not be authorized!
|
// Login succeeded but user might not be authorized!
|
||||||
show_modal_error("Login Failed", "You are not an administrator on this system.")
|
show_modal_error("Login Failed", "You are not an administrator on this system.")
|
||||||
@ -102,6 +136,8 @@ function do_login() {
|
|||||||
// Try to wipe the username/password information.
|
// Try to wipe the username/password information.
|
||||||
$('#loginEmail').val('');
|
$('#loginEmail').val('');
|
||||||
$('#loginPassword').val('');
|
$('#loginPassword').val('');
|
||||||
|
$('#loginOtpInput').val('');
|
||||||
|
$('#loginForm').removeClass('twofactor');
|
||||||
|
|
||||||
// Remember the credentials.
|
// Remember the credentials.
|
||||||
if (typeof localStorage != 'undefined' && typeof sessionStorage != 'undefined') {
|
if (typeof localStorage != 'undefined' && typeof sessionStorage != 'undefined') {
|
||||||
@ -119,7 +155,11 @@ function do_login() {
|
|||||||
// which confuses the loading indicator.
|
// which confuses the loading indicator.
|
||||||
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'system_status' : switch_back_to_panel) }, 300);
|
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'system_status' : switch_back_to_panel) }, 300);
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
'x-auth-token': $('#loginOtpInput').val()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function do_logout() {
|
function do_logout() {
|
||||||
@ -132,6 +172,8 @@ function do_logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show_login() {
|
function show_login() {
|
||||||
|
$('#loginForm').removeClass('twofactor');
|
||||||
|
$('#loginOtpInput').val('');
|
||||||
$('#loginEmail,#loginPassword').each(function() {
|
$('#loginEmail,#loginPassword').each(function() {
|
||||||
var input = $(this);
|
var input = $(this);
|
||||||
if (!$.trim(input.val())) {
|
if (!$.trim(input.val())) {
|
||||||
|
Loading…
Reference in New Issue
Block a user