/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)

This commit is contained in:
Joshua Tauberer 2014-12-01 19:20:46 +00:00
parent 023b38df50
commit 1039a08be6
3 changed files with 38 additions and 28 deletions

View File

@ -1,4 +1,4 @@
import base64, os, os.path import base64, os, os.path, hmac
from flask import make_response from flask import make_response
@ -43,8 +43,9 @@ class KeyAuthService:
def authenticate(self, request, env): def authenticate(self, request, env):
"""Test if the client key passed in HTTP Authorization header matches the service key """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. 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 Returns a tuple of the user's email address and list of user privileges (e.g.
login failure.""" ('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): def decode(s):
return base64.b64decode(s.encode('ascii')).decode('ascii') return base64.b64decode(s.encode('ascii')).decode('ascii')
@ -72,11 +73,11 @@ class KeyAuthService:
raise ValueError("Authorization header invalid.") raise ValueError("Authorization header invalid.")
elif username == self.key: elif username == self.key:
# The user passed the API key which grants administrative privs. # The user passed the API key which grants administrative privs.
return ["admin"] return (None, ["admin"])
else: else:
# The user is trying to log in with a username and password. # The user is trying to log in with a username and user-specific
# Raises or returns privs. # API key or password. Raises or returns privs.
return self.get_user_credentials(username, password, env) return (username, self.get_user_credentials(username, password, env))
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
@ -87,23 +88,28 @@ class KeyAuthService:
if email == "" or pw == "": if email == "" or pw == "":
raise ValueError("Enter an email address and password.") raise ValueError("Enter an email address and password.")
# Get the hashed password of the user. Raise a ValueError if the # The password might be a user-specific API key.
# email address does not correspond to a user. if hmac.compare_digest(self.create_user_key(email), pw):
pw_hash = get_mail_password(email, env) # 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. # Authenticate.
try: try:
# Use 'doveadm pw' to check credentials. doveadm will return # Use 'doveadm pw' to check credentials. doveadm will return
# a non-zero exit status if the credentials are no good, # a non-zero exit status if the credentials are no good,
# and check_call will raise an exception in that case. # and check_call will raise an exception in that case.
utils.shell('check_call', [ utils.shell('check_call', [
"/usr/bin/doveadm", "pw", "/usr/bin/doveadm", "pw",
"-p", pw, "-p", pw,
"-t", pw_hash, "-t", pw_hash,
]) ])
except: except:
# Login failed. # Login failed.
raise ValueError("Invalid password.") raise ValueError("Invalid password.")
# Get privileges for authorization. # Get privileges for authorization.
@ -115,6 +121,9 @@ class KeyAuthService:
# Return a list of privileges. # Return a list of privileges.
return privs 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): def _generate_key(self):
raw_key = os.urandom(32) raw_key = os.urandom(32)
return base64.b64encode(raw_key).decode('ascii') return base64.b64encode(raw_key).decode('ascii')

View File

@ -31,7 +31,7 @@ def authorized_personnel_only(viewfunc):
# Authenticate the passed credentials, which is either the API key or a username:password pair. # Authenticate the passed credentials, which is either the API key or a username:password pair.
error = None error = None
try: try:
privs = auth_service.authenticate(request, env) email, privs = auth_service.authenticate(request, env)
except ValueError as e: except ValueError as e:
# Authentication failed. # Authentication failed.
privs = [] privs = []
@ -95,7 +95,7 @@ def index():
def me(): def me():
# Is the caller authorized? # Is the caller authorized?
try: try:
privs = auth_service.authenticate(request, env) email, privs = auth_service.authenticate(request, env)
except ValueError as e: except ValueError as e:
return json_response({ return json_response({
"status": "invalid", "status": "invalid",
@ -104,12 +104,13 @@ def me():
resp = { resp = {
"status": "ok", "status": "ok",
"email": email,
"privileges": privs, "privileges": privs,
} }
# Is authorized as admin? # Is authorized as admin? Return an API key for future use.
if "admin" in privs: if "admin" in privs:
resp["api_key"] = auth_service.key resp["api_key"] = auth_service.create_user_key(email)
# Return. # Return.
return json_response(resp) return json_response(resp)

View File

@ -85,7 +85,7 @@ function do_login() {
// Login succeeded. // Login succeeded.
// Save the new credentials. // Save the new credentials.
api_credentials = [response.api_key, ""]; api_credentials = [response.email, response.api_key];
// Try to wipe the username/password information. // Try to wipe the username/password information.
$('#loginEmail').val(''); $('#loginEmail').val('');