From 1039a08be6d0b811df18c946c57afb445868b971 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Mon, 1 Dec 2014 19:20:46 +0000 Subject: [PATCH] /admin login now issues a user-specific key for future calls (rather than providing the system-wide API key or passing the password on each request) --- management/auth.py | 55 +++++++++++++++++++-------------- management/daemon.py | 9 +++--- management/templates/login.html | 2 +- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/management/auth.py b/management/auth.py index e11a64d6..1ae46d1e 100644 --- a/management/auth.py +++ b/management/auth.py @@ -1,4 +1,4 @@ -import base64, os, os.path +import base64, os, os.path, hmac from flask import make_response @@ -43,8 +43,9 @@ class KeyAuthService: def authenticate(self, request, env): """Test if the client key passed in HTTP Authorization header matches the service key or if the or username/password passed in the header matches an administrator user. - Returns a list of user privileges (e.g. [] or ['admin']) raise a ValueError on - login failure.""" + Returns a tuple of the user's email address and list of user privileges (e.g. + ('my@email', []) or ('my@email', ['admin']); raises a ValueError on login failure. + If the user used an API key, the user's email is returned as None.""" def decode(s): return base64.b64decode(s.encode('ascii')).decode('ascii') @@ -72,11 +73,11 @@ class KeyAuthService: raise ValueError("Authorization header invalid.") elif username == self.key: # The user passed the API key which grants administrative privs. - return ["admin"] + return (None, ["admin"]) else: - # The user is trying to log in with a username and password. - # Raises or returns privs. - return self.get_user_credentials(username, password, env) + # The user is trying to log in with a username and user-specific + # API key or password. Raises or returns privs. + return (username, self.get_user_credentials(username, password, env)) def get_user_credentials(self, email, pw, env): # Validate a user's credentials. On success returns a list of @@ -87,23 +88,28 @@ class KeyAuthService: if email == "" or pw == "": raise ValueError("Enter an email address and password.") - # Get the hashed password of the user. Raise a ValueError if the - # email address does not correspond to a user. - pw_hash = get_mail_password(email, env) + # The password might be a user-specific API key. + if hmac.compare_digest(self.create_user_key(email), pw): + # OK. + pass + else: + # Get the hashed password of the user. Raise a ValueError if the + # email address does not correspond to a user. + pw_hash = get_mail_password(email, env) - # Authenticate. - try: - # Use 'doveadm pw' to check credentials. doveadm will return - # a non-zero exit status if the credentials are no good, - # and check_call will raise an exception in that case. - utils.shell('check_call', [ - "/usr/bin/doveadm", "pw", - "-p", pw, - "-t", pw_hash, - ]) - except: - # Login failed. - raise ValueError("Invalid password.") + # Authenticate. + try: + # Use 'doveadm pw' to check credentials. doveadm will return + # a non-zero exit status if the credentials are no good, + # and check_call will raise an exception in that case. + utils.shell('check_call', [ + "/usr/bin/doveadm", "pw", + "-p", pw, + "-t", pw_hash, + ]) + except: + # Login failed. + raise ValueError("Invalid password.") # Get privileges for authorization. @@ -115,6 +121,9 @@ class KeyAuthService: # Return a list of privileges. return privs + def create_user_key(self, email): + return hmac.new(self.key.encode('ascii'), b"AUTH:" + email.encode("utf8"), digestmod="sha1").hexdigest() + def _generate_key(self): raw_key = os.urandom(32) return base64.b64encode(raw_key).decode('ascii') diff --git a/management/daemon.py b/management/daemon.py index cd172237..bc2099b8 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -31,7 +31,7 @@ def authorized_personnel_only(viewfunc): # Authenticate the passed credentials, which is either the API key or a username:password pair. error = None try: - privs = auth_service.authenticate(request, env) + email, privs = auth_service.authenticate(request, env) except ValueError as e: # Authentication failed. privs = [] @@ -95,7 +95,7 @@ def index(): def me(): # Is the caller authorized? try: - privs = auth_service.authenticate(request, env) + email, privs = auth_service.authenticate(request, env) except ValueError as e: return json_response({ "status": "invalid", @@ -104,12 +104,13 @@ def me(): resp = { "status": "ok", + "email": email, "privileges": privs, } - # Is authorized as admin? + # Is authorized as admin? Return an API key for future use. if "admin" in privs: - resp["api_key"] = auth_service.key + resp["api_key"] = auth_service.create_user_key(email) # Return. return json_response(resp) diff --git a/management/templates/login.html b/management/templates/login.html index 8623e81d..2fd12435 100644 --- a/management/templates/login.html +++ b/management/templates/login.html @@ -85,7 +85,7 @@ function do_login() { // Login succeeded. // Save the new credentials. - api_credentials = [response.api_key, ""]; + api_credentials = [response.email, response.api_key]; // Try to wipe the username/password information. $('#loginEmail').val('');