def check_user_auth(self, email, pw, request, env): # Validate a user's login email address and password. If MFA is enabled, # check the MFA token in the X-Auth-Token header. # # On login failure, raises a ValueError with a login error message. On # success, nothing is returned. # Authenticate. try: # Get the hashed password of the user. Raise a ValueError if the # email address does not correspond to a user. But wrap it in the # same exception as if a password fails so we don't easily reveal # if an email address is valid. pw_hash = get_mail_password(email, env) # 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("Incorrect email address or password.") # If MFA is enabled, check that MFA passes. status, hints = validate_auth_mfa(email, request, env) if not status: # Login valid. Hints may have more info. raise ValueError(",".join(hints))
def create_user_key(self, email, env): # Store an HMAC with the client. The hashed message of the HMAC will be the user's # email address & hashed password and the key will be the master API key. The user of # course has their own email address and password. We assume they do not have the master # API key (unless they are trusted anyway). The HMAC proves that they authenticated # with us in some other way to get the HMAC. Including the password means that when # a user's password is reset, the HMAC changes and they will correctly need to log # in to the control panel again. This method raises a ValueError if the user does # not exist, due to get_mail_password. msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password(email, env).encode("utf8") return hmac.new(self.key.encode("ascii"), msg, digestmod="sha256").hexdigest()
def create_user_key(self, email, env): # Store an HMAC with the client. The hashed message of the HMAC will be the user's # email address & hashed password and the key will be the master API key. The user of # course has their own email address and password. We assume they do not have the master # API key (unless they are trusted anyway). The HMAC proves that they authenticated # with us in some other way to get the HMAC. Including the password means that when # a user's password is reset, the HMAC changes and they will correctly need to log # in to the control panel again. This method raises a ValueError if the user does # not exist, due to get_mail_password. msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password(email, env).encode("utf8") return hmac.new(self.key.encode('ascii'), msg, digestmod="sha256").hexdigest()
def check_user_auth(self, email, pw, request, env): # Validate a user's login email address and password. If MFA is enabled, # check the MFA token in the X-Auth-Token header. # # On success returns a list of privileges (e.g. [] or ['admin']). On login # failure, raises a ValueError with a login error message. # Sanity check. if email == "" or pw == "": raise ValueError("Enter an email address and password.") # The password might be a user-specific API key. create_user_key raises # a ValueError if the user does not exist. if hmac.compare_digest(self.create_user_key(email, env), 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.") # If MFA is enabled, check that MFA passes. status, hints = validate_auth_mfa(email, request, env) if not status: # Login valid. Hints may have more info. raise ValueError(",".join(hints)) # Get privileges for authorization. This call should never fail because by this # point we know the email address is a valid user. But on error the call will # return a tuple of an error message and an HTTP status code. privs = get_mail_user_privileges(email, env) if isinstance(privs, tuple): raise ValueError(privs[0]) # Return a list of privileges. return privs
def create_user_password_state_token(self, email, env): # Create a token that changes if the user's password or MFA options change # so that sessions become invalid if any of that information changes. msg = get_mail_password(email, env).encode("utf8") # Add to the message the current MFA state, which is a list of MFA information. # Turn it into a string stably. msg += b" " + json.dumps(get_hash_mfa_state(email, env), sort_keys=True).encode("utf8") # Make a HMAC using the system API key as a hash key. hash_key = self.key.encode('ascii') return hmac.new(hash_key, msg, digestmod="sha256").hexdigest()
def get_user_credentials(self, email, pw, env): # Validate a user's credentials. On success returns a list of # privileges (e.g. [] or ['admin']). On failure raises a ValueError # with a login error message. # Sanity check. if email == "" or pw == "": raise ValueError("Enter an email address and password.") # 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.") # Get privileges for authorization. # (This call should never fail on a valid user. But if it did fail, it would # return a tuple of an error message and an HTTP status code.) privs = get_mail_user_privileges(email, env) if isinstance(privs, tuple): raise Exception("Error getting privileges.") # Return a list of privileges. return privs
def create_user_key(self, email, env): # 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 # email address, current hashed password, and current MFA state, so that the # key becomes invalid if any of that information changes. # # Use an HMAC to generate the API key using our master API key as a key, # which also means that the API key becomes invalid when our master API key # changes --- i.e. when this process is restarted. # # Raises ValueError via get_mail_password if the user doesn't exist. # Construct the HMAC message from the user's email address and current password. msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password( email, env).encode("utf8") # Add to the message the current MFA state, which is a list of MFA information. # Turn it into a string stably. msg += b" " + json.dumps(get_hash_mfa_state(email, env), sort_keys=True).encode("utf8") # Make the HMAC. hash_key = self.key.encode('ascii') return hmac.new(hash_key, msg, digestmod="sha256").hexdigest()