mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-20 02:52:11 +00:00
Add token authentication for munin routes
This commit is contained in:
parent
41011f8639
commit
405860cac5
@ -1,6 +1,6 @@
|
|||||||
import base64, os, os.path, hmac, json
|
import base64, os, os.path, hmac, json, secrets
|
||||||
|
|
||||||
from flask import make_response
|
from expiringdict import ExpiringDict
|
||||||
|
|
||||||
import utils
|
import utils
|
||||||
from mailconfig import get_mail_password, get_mail_user_privileges
|
from mailconfig import get_mail_password, get_mail_user_privileges
|
||||||
@ -16,6 +16,8 @@ class KeyAuthService:
|
|||||||
requests. The key is passed as the username field in the standard HTTP
|
requests. The key is passed as the username field in the standard HTTP
|
||||||
Basic Auth header.
|
Basic Auth header.
|
||||||
"""
|
"""
|
||||||
|
token_dict = ExpiringDict(max_len=1024, max_age_seconds=600)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.auth_realm = DEFAULT_AUTH_REALM
|
self.auth_realm = DEFAULT_AUTH_REALM
|
||||||
self.key = self._generate_key()
|
self.key = self._generate_key()
|
||||||
@ -74,11 +76,16 @@ class KeyAuthService:
|
|||||||
raise ValueError("Authorization header invalid.")
|
raise ValueError("Authorization header invalid.")
|
||||||
elif username == self.key:
|
elif username == self.key:
|
||||||
# The user passed the master API key which grants administrative privs.
|
# The user passed the master API key which grants administrative privs.
|
||||||
return (None, ["admin"])
|
return (None, ["admin"], None)
|
||||||
else:
|
else:
|
||||||
# The user is trying to log in with a username and either a password
|
# The user is trying to log in with a username and either a password
|
||||||
# (and possibly a MFA token) or a user-specific API key.
|
# (and possibly a MFA token) or a user-specific API key.
|
||||||
return (username, self.check_user_auth(username, password, request, env))
|
token = None
|
||||||
|
privs = self.check_user_auth(username, password, request, env)
|
||||||
|
if not self.validate_user_token(username, request, env):
|
||||||
|
token = secrets.token_hex(16)
|
||||||
|
KeyAuthService.token_dict[username] = token
|
||||||
|
return (username, privs, token)
|
||||||
|
|
||||||
def check_user_auth(self, email, pw, request, env):
|
def check_user_auth(self, email, pw, request, env):
|
||||||
# Validate a user's login email address and password. If MFA is enabled,
|
# Validate a user's login email address and password. If MFA is enabled,
|
||||||
@ -130,6 +137,14 @@ class KeyAuthService:
|
|||||||
# Return a list of privileges.
|
# Return a list of privileges.
|
||||||
return privs
|
return privs
|
||||||
|
|
||||||
|
def check_user_token(self, email, token, request, env):
|
||||||
|
# Check whether a token matches the one we stored for the user.
|
||||||
|
return token is not None and KeyAuthService.token_dict.get(email) == token
|
||||||
|
|
||||||
|
def validate_user_token(self, email, request, env):
|
||||||
|
# Check whether the provided token in request cookie matches the one we stored for the user.
|
||||||
|
return self.check_user_token(email, request.cookies.get("token"), request, env)
|
||||||
|
|
||||||
def create_user_key(self, email, env):
|
def create_user_key(self, email, env):
|
||||||
# Create a user API key, which is a shared secret that we can re-generate from
|
# Create a user API key, which is a shared secret that we can re-generate from
|
||||||
# static information in our database. The shared secret contains the user's
|
# static information in our database. The shared secret contains the user's
|
||||||
|
@ -51,7 +51,7 @@ def authorized_personnel_only(viewfunc):
|
|||||||
privs = []
|
privs = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
email, privs = auth_service.authenticate(request, env)
|
email, privs, _ = auth_service.authenticate(request, env)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# Write a line in the log recording the failed login
|
# Write a line in the log recording the failed login
|
||||||
log_failed_login(request)
|
log_failed_login(request)
|
||||||
@ -135,7 +135,7 @@ def index():
|
|||||||
def me():
|
def me():
|
||||||
# Is the caller authorized?
|
# Is the caller authorized?
|
||||||
try:
|
try:
|
||||||
email, privs = auth_service.authenticate(request, env)
|
email, privs, token = auth_service.authenticate(request, env)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if "missing-totp-token" in str(e):
|
if "missing-totp-token" in str(e):
|
||||||
return json_response({
|
return json_response({
|
||||||
@ -160,8 +160,13 @@ def me():
|
|||||||
if "admin" in privs:
|
if "admin" in privs:
|
||||||
resp["api_key"] = auth_service.create_user_key(email, env)
|
resp["api_key"] = auth_service.create_user_key(email, env)
|
||||||
|
|
||||||
|
resp = json_response(resp)
|
||||||
|
# Set authentication token for admin munin routes.
|
||||||
|
if "admin" in privs and token:
|
||||||
|
resp.set_cookie("token", value=token, secure=True, httponly=True, samesite='Lax')
|
||||||
|
|
||||||
# Return.
|
# Return.
|
||||||
return json_response(resp)
|
return resp
|
||||||
|
|
||||||
# MAIL
|
# MAIL
|
||||||
|
|
||||||
|
@ -110,6 +110,12 @@ def validate_auth_mfa(email, request, env):
|
|||||||
if len(mfa_state) == 0:
|
if len(mfa_state) == 0:
|
||||||
return (True, [])
|
return (True, [])
|
||||||
|
|
||||||
|
# Try token authentication first for munin routes.
|
||||||
|
if request.full_path.startswith("/munin"):
|
||||||
|
from daemon import auth_service
|
||||||
|
if auth_service.validate_user_token(email, request, env):
|
||||||
|
return (True, [])
|
||||||
|
|
||||||
# Try the enabled MFA modes.
|
# Try the enabled MFA modes.
|
||||||
hints = set()
|
hints = set()
|
||||||
for mfa_mode in mfa_state:
|
for mfa_mode in mfa_state:
|
||||||
|
Loading…
Reference in New Issue
Block a user