Commit 1039a08b authored by Joshua Tauberer's avatar Joshua Tauberer

/admin login now issues a user-specific key for future calls (rather than...

/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)
parent 023b38df
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: ...@@ -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: ...@@ -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: ...@@ -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
# Authenticate. else:
try: # Get the hashed password of the user. Raise a ValueError if the
# Use 'doveadm pw' to check credentials. doveadm will return # email address does not correspond to a user.
# a non-zero exit status if the credentials are no good, pw_hash = get_mail_password(email, env)
# and check_call will raise an exception in that case.
utils.shell('check_call', [ # Authenticate.
"/usr/bin/doveadm", "pw", try:
"-p", pw, # Use 'doveadm pw' to check credentials. doveadm will return
"-t", pw_hash, # a non-zero exit status if the credentials are no good,
]) # and check_call will raise an exception in that case.
except: utils.shell('check_call', [
# Login failed. "/usr/bin/doveadm", "pw",
raise ValueError("Invalid password.") "-p", pw,
"-t", pw_hash,
])
except:
# Login failed.
raise ValueError("Invalid password.")
# Get privileges for authorization. # Get privileges for authorization.
...@@ -115,6 +121,9 @@ class KeyAuthService: ...@@ -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')
...@@ -31,7 +31,7 @@ def authorized_personnel_only(viewfunc): ...@@ -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(): ...@@ -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(): ...@@ -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)
......
...@@ -85,7 +85,7 @@ function do_login() { ...@@ -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('');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment