def post(self, **post): try: data = Bunch(register_form.native(post)[0]) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message=_("Unable to parse data."), data=post, exc=str(e)) if not data.username or not data.email or not data.password or data.password != data.pass2: return 'json:', dict(success=False, message=_("Missing data or passwords do not match."), data=data) #Make sure that the provided email address is a valid form for an email address v = EmailValidator() email = data.email email, err = v.validate(email) if err: return 'json:', dict(success=False, message=_("Invalid email address provided."), data=data) #If the password isn't strong enough, reject it if(zxcvbn.password_strength(data.password).get("score") < MINIMUM_PASSWORD_STRENGTH): return 'json:', dict(success=False, message=_("Password provided is too weak. please add more characters, or include lowercase, uppercase, and special characters."), data=data) #Ensures that the provided username and email are lowercase user = User(data.username.lower(), data.email.lower(), active=True) user.password = data.password try: user.save() except ValidationError: return 'json:', dict(success=False, message=_("Invalid email address provided."), data=data) except NotUniqueError: return 'json:', dict(success=False, message=_("Either the username or email address provided is already taken."), data=data) authenticate(user.username, data.password) return 'json:', dict(success=True, location="/")
def __post_recovery(self, **post): try: data = Bunch(reset_password_form.native(post)[0]) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message=_("Unable to parse data."), data=post, exc=str(e)) recovery = self.__get_recovery(data.email, data.recovery_key) if not recovery: return 'json:', dict(success=False, message=_("Sorry that recovery link has already expired"), location="/account/recover") passwd_ok, error_msg = _check_password(data.password, data.pass2) if not passwd_ok: return 'json:', dict(success=False, message=error_msg) #If the password isn't strong enough, reject it if(zxcvbn.password_strength(data.password).get("score") < MINIMUM_PASSWORD_STRENGTH): return 'json:', dict(success=False, message=_("Password provided is too weak. please add more characters, or include lowercase, uppercase, and special characters."), data=data) #set new password user = recovery.user user.password = data.password user.save() #remove recovery key recovery.delete() authenticate(user.username, data.password) return 'json:', dict(success=True, message=_("Password changed, forwarding ..."), location="/")
def __init__(self, *args, **kw): super(RootController, self).__init__(*args, **kw) self.authorize = AuthorizeHandler() # to avoid gumming up the @authorize decorator if boolean(config.get('debug', False)): self.dev = DeveloperTools()
def __post_email(self, **post): try: data = Bunch(recover_form.native(post)[0]) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message=_("Unable to parse data."), data=post, exc=str(e)) user = lookup_email(data.email) if not user: user = lookup_email(data.email.lower()) if not user: # FixMe: possibly do send success any way, to prevent email address confirmation # - would be necessary for register as well return 'json:', dict(success=False, message=_("Unknown email."), data=post) send_recover_email(user) return 'json:', dict( success=True, message=_( "Recovery e-mail sent - " "please follow the instructions in that mail to restore your password" ))
def __init__(self, *args, **kw): super(RootController, self).__init__(*args, **kw) self.authorize = AuthorizeHandler( ) # to avoid gumming up the @authorize decorator if boolean(config.get('debug', False)): self.dev = DeveloperTools()
def trigger_api_validation(sender, document, **kwargs): if not kwargs.get("created", False): return if config.get("debug", False): validate_key(document.id) else: validator_pool.submit(validate_key, document.id)
def trigger_api_validation(sender, document, **kwargs): if not kwargs.get('created', False): return if config.get('debug', False): validate_key(document.id) else: validator_pool.submit(validate_key, document.id)
def post(self, **post): try: data = Bunch(register_form.native(post)[0]) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message=_("Unable to parse data."), data=post, exc=str(e)) if not data.username or not data.email or not data.password or data.password != data.pass2: return 'json:', dict( success=False, message=_("Missing data or passwords do not match."), data=data) #Make sure that the provided email address is a valid form for an email address v = EmailValidator() email = data.email email, err = v.validate(email) if err: return 'json:', dict(success=False, message=_("Invalid email address provided."), data=data) #If the password isn't strong enough, reject it if (zxcvbn.password_strength(data.password).get("score") < MINIMUM_PASSWORD_STRENGTH): return 'json:', dict( success=False, message= _("Password provided is too weak. please add more characters, or include lowercase, uppercase, and special characters." ), data=data) #Ensures that the provided username and email are lowercase user = User(data.username.lower(), data.email.lower(), active=True) user.password = data.password try: user.save() except ValidationError: return 'json:', dict(success=False, message=_("Invalid email address provided."), data=data) except NotUniqueError: return 'json:', dict( success=False, message= _("Either the username or email address provided is already taken." ), data=data) authenticate(user.username, data.password) return 'json:', dict(success=True, location="/")
def trigger_api_validation(sender, document, **kwargs): """Trigger validation of newly created EVE API Credential documents.""" from brave.core.key.model import EVECredential if not kwargs.get('created', False): return if config.get('debug', False): EVECredential.objects(id=document.id).first().pull() else: validator_pool.submit(EVECredential.object(id=document.id).first().pull)
def refresh(self): try: self.key.pull() except: log.exception("Unable to reload key.") if boolean(config.get('debug', False)): raise return 'json:', dict(success=False) return 'json:', dict(success=True)
def refresh(self): try: self.key.pull() except: log.exception("Unable to reload key.") if boolean(config.get("debug", False)): raise return "json:", dict(success=False) return "json:", dict(success=True)
def trigger_private_key_generation(sender, document, **kwargs): """Trigger creation of a new application-specific private key.""" if not kwargs.get('created', False): return if config.get('debug', False): generate_key(document.id) else: receipt = validator_pool.submit(generate_key, document.id) receipt.add_done_callback(log_error)
def authenticate(identifier, password): """Given an e-mail address (or Yubikey OTP) and password, authenticate a user.""" ts = time() # Record the query = dict(active=True) # Gracefully handle extended characters in passwords. # The password storage algorithm works in binary. if isinstance(password, unicode): password = password.encode('utf8') # Build the MongoEngine query to find if '@' in identifier: query[b'email'] = identifier elif len(identifier) == 44: query[b'otp'] = identifier[:12] else: query[b'username'] = identifier user = User.objects(**query).first() if not user or not User.password.check(user.password, password) or (user.rotp and len(user.otp) != 0 and not 'otp' in query): if user: LoginHistory(user, False, request.remote_addr).save() # Prevent basic timing attacks; always take at least one second to process. sleep(max(min(1 - (time() - ts), 0), 1)) return None # Validate Yubikey OTP if 'otp' in query: client = yubico.Yubico( config['yubico.client'], config['yubico.key'], boolean(config.get('yubico.secure', False)) ) try: status = client.verify(identifier, return_response=True) except: return None if not status: return None user.update(set__seen=datetime.utcnow()) # Record the fact the user signed in. LoginHistory(user, True, request.remote_addr).save() return user.id, user
def trigger_api_validation(sender, document, **kwargs): """Trigger validation of newly created EVE API Credential documents.""" from brave.core.key.model import EVECredential if not kwargs.get('created', False): return if config.get('debug', False): EVECredential.objects(id=document.id).first().pull() else: validator_pool.submit( EVECredential.object(id=document.id).first().pull)
def post(self, **post): try: data = Bunch(register_form.native(post)[0]) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message="Unable to parse data.", data=post, exc=str(e)) if not data.username or not data.email or not data.password or data.password != data.pass2: return 'json:', dict(success=False, message="Missing data?", data=data) user = User(data.username, data.email, active=True) user.password = data.password user.save() authenticate(data.username, data.password) return 'json:', dict(success=True, location="/")
def __post_recovery(self, **post): try: data = Bunch(reset_password_form.native(post)[0]) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message=_("Unable to parse data."), data=post, exc=str(e)) recovery = self.__get_recovery(data.email, data.recovery_key) if not recovery: return 'json:', dict( success=False, message=_("Sorry that recovery link has already expired"), location="/account/recover") passwd_ok, error_msg = _check_password(data.password, data.pass2) if not passwd_ok: return 'json:', dict(success=False, message=error_msg) #If the password isn't strong enough, reject it if (zxcvbn.password_strength(data.password).get("score") < MINIMUM_PASSWORD_STRENGTH): return 'json:', dict( success=False, message= _("Password provided is too weak. please add more characters, or include lowercase, uppercase, and special characters." ), data=data) #set new password user = recovery.user user.password = data.password user.save() #remove recovery key recovery.delete() authenticate(user.username, data.password) return 'json:', dict(success=True, message=_("Password changed, forwarding ..."), location="/")
def get(self, redirect=None, **get): if redirect is None: referrer = request.referrer redirect = '/' if not referrer or referrer.endswith(request.script_name) else referrer try: data = Bunch(reset_password_form.native(get)[0]) except Exception as e: if config.get('debug', False): raise raise HTTPFound(location='/') # Todo redirect to recover with error message if not data.recovery_key: # no key passed, so show email entry form = recover_form(dict(redirect=redirect)) button_label = _("Recover") else: form = reset_password_form(dict(email=data.email, recovery_key=data.recovery_key)) button_label = _("Set Password") return "brave.core.account.template.recover", dict(form=form, button_label=str(button_label))
def __post_email(self, **post): try: data = Bunch(recover_form.native(post)[0]) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message=_("Unable to parse data."), data=post, exc=str(e)) user = lookup_email(data.email) if not user: user = lookup_email(data.email.lower()) if not user: # FixMe: possibly do send success any way, to prevent email address confirmation # - would be necessary for register as well return 'json:', dict(success=False, message=_("Unknown email."), data=post) send_recover_email(user) return 'json:', dict(success=True, message=_("Recovery e-mail sent - " "please follow the instructions in that mail to restore your password"))
def get(self, redirect=None, **get): if redirect is None: referrer = request.referrer redirect = '/' if not referrer or referrer.endswith( request.script_name) else referrer try: data = Bunch(reset_password_form.native(get)[0]) except Exception as e: if config.get('debug', False): raise raise HTTPFound( location='/') # Todo redirect to recover with error message if not data.recovery_key: # no key passed, so show email entry form = recover_form(dict(redirect=redirect)) button_label = _("Recover") else: form = reset_password_form( dict(email=data.email, recovery_key=data.recovery_key)) button_label = _("Set Password") return "brave.core.account.template.recover", dict( form=form, button_label=str(button_label))
def authorize(self, success=None, failure=None): """Prepare a incoming session request. Error 'message' attributes are temporary; base your logic on the status and code attributes. success: web.core.url:URL (required) failure: web.core.url:URL (required) returns: location: web.core.url:URL the location to direct users to """ # Ensure success and failure URLs are present. if success is None: response.status_int = 400 return dict( status='error', code='argument.success.missing', message= "URL to return users to upon successful authentication is missing from your request." ) if failure is None: response.status_int = 400 return dict( status='error', code='argument.failure.missing', message= "URL to return users to upon authentication failure or dismissal is missing from your request." ) # Also ensure they are valid URIs. try: success_ = success success = URL(success) except: response.status_int = 400 return dict(status='error', code='argument.success.malformed', message="Successful authentication URL is malformed.") try: failure_ = failure failure = URL(failure) except: response.status_int = 400 return dict( status='error', code='argument.response.malformed', message= "URL to return users to upon successful authentication is missing from your request." ) # Deny localhost/127.0.0.1 loopbacks and 192.* and 10.* unless in development mode. if not boolean(config.get('debug', False)) and (success.host in ('localhost', '127.0.0.1') or \ success.host.startswith('192.168.') or \ success.host.startswith('10.')): response.status_int = 400 return dict( status='error', code='development-only', message= "Loopback and local area-network URLs disallowd in production." ) # Check blacklist and bail early. if AuthenticationBlacklist.objects( reduce(__or__, [ Q(scheme=success.scheme), Q(scheme=failure.scheme), Q(protocol=success.port or success.scheme), Q(protocol=failure.port or failure.scheme), ] + ([] if not success.host else [Q(domain=success.host)]) + ([] if not failure.host else [Q( domain=failure.host)]))).count(): response.status_int = 400 return dict( status='error', code='blacklist', message="You have been blacklisted. To dispute, contact {0}". format(config['mail.blackmail.author'])) # TODO: Check DNS. Yes, really. # Generate authentication token. log.info("Creating request for {0} with callbacks {1} and {2}.".format( request.service, success_, failure_)) ar = AuthenticationRequest( request. service, # We have an authenticated request, so we know the service ID is valid. success=success_, failure=failure_) ar.save() return dict(location=url.complete('/authorize/{0}'.format(ar.id)))
def post(self, **post): try: data = Bunch(post) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message=_("Unable to parse data."), data=post, exc=str(e)) query = dict(active=True) query[b'username'] = data.id user = User.objects(**query).first() if data.form == "changepassword": passwd_ok, error_msg = _check_password(data.passwd, data.passwd1) if not passwd_ok: return 'json:', dict(success=False, message=error_msg, data=data) if isinstance(data.old, unicode): data.old = data.old.encode('utf-8') if not User.password.check(user.password, data.old): return 'json:', dict(success=False, message=_("Old password incorrect."), data=data) #If the password isn't strong enough, reject it if (zxcvbn.password_strength(data.passwd).get("score") < MINIMUM_PASSWORD_STRENGTH): return 'json:', dict( success=False, message= _("Password provided is too weak. please add more characters, or include lowercase, uppercase, and special characters." ), data=data) user.password = data.passwd user.save() elif data.form == "addotp": if isinstance(data.password, unicode): data.password = data.password.encode('utf-8') identifier = data.otp client = yubico.Yubico(config['yubico.client'], config['yubico.key'], boolean(config.get('yubico.secure', False))) if not User.password.check(user.password, data.password): return 'json:', dict(success=False, message=_("Password incorrect."), data=data) try: status = client.verify(identifier, return_response=True) except: return 'json:', dict(success=False, message=_("Failed to contact YubiCloud."), data=data) if not status: return 'json:', dict(success=False, message=_("Failed to verify key."), data=data) if not User.addOTP(user, identifier[:12]): return 'json:', dict(success=False, message=_("YubiKey already exists."), data=data) elif data.form == "removeotp": identifier = data.otp if not User.removeOTP(user, identifier[:12]): return 'json:', dict(success=False, message=_("YubiKey invalid."), data=data) elif data.form == "configureotp": if isinstance(data.password, unicode): data.password = data.password.encode('utf-8') rotp = True if 'rotp' in data else False if not User.password.check(user.password, data.password): return 'json:', dict(success=False, message=_("Password incorrect."), data=data) user.rotp = rotp user.save() #Handle the user attempting to delete their account elif data.form == "deleteaccount": if isinstance(data.passwd, unicode): data.passwd = data.passwd.encode('utf-8') #Make the user enter their username so they know what they're doing. if not user.username == data.username.lower(): return 'json:', dict(success=False, message=_("Username incorrect."), data=data) #Check whether the user's supplied password is correct if not User.password.check(user.password, data.passwd): return 'json:', dict(success=False, message=_("Password incorrect."), data=data) #Make them type "delete" exactly if not data.confirm == "delete": return 'json:', dict( success=False, message=_( "Delete was either misspelled or not lowercase."), data=data) #Delete the user account and then deauthenticate the browser session log.info("User %s authorized the deletion of their account.", user) user.delete() deauthenticate() #Redirect user to the root of the server instead of the settings page return 'json:', dict(success=True, location="/") #Handle the user attempting to change the email address associated with their account elif data.form == "changeemail": if isinstance(data.passwd, unicode): data.passwd = data.passwd.encode('utf-8') #Check whether the user's supplied password is correct if not User.password.check(user.password, data.passwd): return 'json:', dict(success=False, message=_("Password incorrect."), data=data) #Check that the two provided email addresses match if not data.newEmail.lower() == data.newEmailConfirm.lower(): return 'json:', dict( success=False, message=_("Provided email addresses do not match."), data=data) #Make sure that the provided email address is a valid form for an email address v = EmailValidator() email = data.newEmail email, err = v.validate(email) if err: return 'json:', dict( success=False, message=_("Invalid email address provided."), data=data) #Make sure that the new email address is not already taken count = User.objects.filter(**{ "email": data.newEmail.lower() }).count() if not count == 0: return 'json:', dict( success=False, message=_("The email address provided is already taken."), data=data) #Change the email address in the database and catch any email validation exceptions that mongo throws user.email = data.newEmail.lower() try: user.save() except ValidationError: return 'json:', dict( success=False, message=_("Invalid email address provided."), data=data) except NotUniqueError: return 'json:', dict( success=False, message=_("The email address provided is already taken."), data=data) #Handle the user attempting to merge 2 accounts together elif data.form == "mergeaccount": if isinstance(data.passwd, unicode): data.passwd = data.passwd.encode('utf-8') if isinstance(data.passwd2, unicode): data.passwd2 = data.passwd2.encode('utf-8') #Make the user enter their username so they know what they're doing. if user.username != data.username.lower( ) and user.username != data.username: return 'json:', dict(success=False, message=_("First username incorrect."), data=data) #Check whether the user's supplied password is correct if not User.password.check(user.password, data.passwd): return 'json:', dict(success=False, message=_("First password incorrect."), data=data) #Make sure the user isn't trying to merge their account into itself. if data.username.lower() == data.username2.lower(): return 'json:', dict( success=False, message=_("You can't merge an account into itself."), data=data) #Make the user enter the second username so we can get the User object they want merged in. if not User.objects( username=data.username2.lower()) and not User.objects( username=data.username2): return 'json:', dict( success=False, message=_("Unable to find user by second username."), data=data) other = User.objects(username=data.username2).first() if not other: other = User.objects(username=data.username2.lower()).first() #Check whether the user's supplied password is correct if not User.password.check(other.password, data.passwd2): return 'json:', dict(success=False, message=_("Second password incorrect."), data=data) #Make them type "merge" exactly if data.confirm != "merge": return 'json:', dict( success=False, message=_("Merge was either misspelled or not lowercase."), data=data) log.info("User %s merged account %s into %s.", user.username, other.username, user.username) user.merge(other) #Redirect user to the root of the server instead of the settings page return 'json:', dict(success=True, location="/") else: return 'json:', dict(success=False, message=_("Form does not exist."), location="/") return 'json:', dict(success=True, location="/account/settings")
def post(self, **post): try: data = Bunch(post) except Exception as e: if config.get('debug', False): raise return 'json:', dict(success=False, message=_("Unable to parse data."), data=post, exc=str(e)) query = dict(active=True) query[b'username'] = data.id query_user = User.objects(**query).first() if query_user.id != user.id: raise HTTPForbidden if data.form == "changepassword": passwd_ok, error_msg = _check_password(data.passwd, data.passwd1) if not passwd_ok: return 'json:', dict(success=False, message=error_msg, data=data) if isinstance(data.old, unicode): data.old = data.old.encode('utf-8') if not User.password.check(user.password, data.old): return 'json:', dict(success=False, message=_("Old password incorrect."), data=data) #If the password isn't strong enough, reject it if(zxcvbn.password_strength(data.passwd).get("score") < MINIMUM_PASSWORD_STRENGTH): return 'json:', dict(success=False, message=_("Password provided is too weak. please add more characters, or include lowercase, uppercase, and special characters."), data=data) user.password = data.passwd user.save() elif data.form == "addotp": if isinstance(data.password, unicode): data.password = data.password.encode('utf-8') identifier = data.otp client = yubico.Yubico( config['yubico.client'], config['yubico.key'], boolean(config.get('yubico.secure', False)) ) if not User.password.check(user.password, data.password): return 'json:', dict(success=False, message=_("Password incorrect."), data=data) try: status = client.verify(identifier, return_response=True) except: return 'json:', dict(success=False, message=_("Failed to contact YubiCloud."), data=data) if not status: return 'json:', dict(success=False, message=_("Failed to verify key."), data=data) if not User.addOTP(user, identifier[:12]): return 'json:', dict(success=False, message=_("YubiKey already exists."), data=data) elif data.form == "removeotp": identifier = data.otp if not User.removeOTP(user, identifier[:12]): return 'json:', dict(success=False, message=_("YubiKey invalid."), data=data) elif data.form == "configureotp": if isinstance(data.password, unicode): data.password = data.password.encode('utf-8') rotp = True if 'rotp' in data else False if not User.password.check(user.password, data.password): return 'json:', dict(success=False, message=_("Password incorrect."), data=data) user.rotp = rotp user.save() #Handle the user attempting to delete their account elif data.form == "deleteaccount": if isinstance(data.passwd, unicode): data.passwd = data.passwd.encode('utf-8') #Make the user enter their username so they know what they're doing. if not user.username == data.username.lower(): return 'json:', dict(success=False, message=_("Username incorrect."), data=data) #Check whether the user's supplied password is correct if not User.password.check(user.password, data.passwd): return 'json:', dict(success=False, message=_("Password incorrect."), data=data) #Make them type "delete" exactly if not data.confirm == "delete": return 'json:', dict(success=False, message=_("Delete was either misspelled or not lowercase."), data=data) #Delete the user account and then deauthenticate the browser session log.info("User %s authorized the deletion of their account.", user) user.delete() deauthenticate() #Redirect user to the root of the server instead of the settings page return 'json:', dict(success=True, location="/") #Handle the user attempting to change the email address associated with their account elif data.form == "changeemail": if isinstance(data.passwd, unicode): data.passwd = data.passwd.encode('utf-8') #Check whether the user's supplied password is correct if not User.password.check(user.password, data.passwd): return 'json:', dict(success=False, message=_("Password incorrect."), data=data) #Check that the two provided email addresses match if not data.newEmail.lower() == data.newEmailConfirm.lower(): return 'json:', dict(success=False, message=_("Provided email addresses do not match."), data=data) #Make sure that the provided email address is a valid form for an email address v = EmailValidator() email = data.newEmail email, err = v.validate(email) if err: return 'json:', dict(success=False, message=_("Invalid email address provided."), data=data) #Make sure that the new email address is not already taken count = User.objects.filter(**{"email": data.newEmail.lower()}).count() if not count == 0: return 'json:', dict(success=False, message=_("The email address provided is already taken."), data=data) #Change the email address in the database and catch any email validation exceptions that mongo throws user.email = data.newEmail.lower() try: user.save() except ValidationError: return 'json:', dict(success=False, message=_("Invalid email address provided."), data=data) except NotUniqueError: return 'json:', dict(success=False, message=_("The email address provided is already taken."), data=data) #Handle the user attempting to merge 2 accounts together elif data.form == "mergeaccount": if isinstance(data.passwd, unicode): data.passwd = data.passwd.encode('utf-8') if isinstance(data.passwd2, unicode): data.passwd2 = data.passwd2.encode('utf-8') #Make the user enter their username so they know what they're doing. if user.username != data.username.lower() and user.username != data.username: return 'json:', dict(success=False, message=_("First username incorrect."), data=data) #Check whether the user's supplied password is correct if not User.password.check(user.password, data.passwd): return 'json:', dict(success=False, message=_("First password incorrect."), data=data) #Make sure the user isn't trying to merge their account into itself. if data.username.lower() == data.username2.lower(): return 'json:', dict(success=False, message=_("You can't merge an account into itself."), data=data) #Make the user enter the second username so we can get the User object they want merged in. if not User.objects(username=data.username2.lower()) and not User.objects(username=data.username2): return 'json:', dict(success=False, message=_("Unable to find user by second username."), data=data) other = User.objects(username=data.username2).first() if not other: other = User.objects(username=data.username2.lower()).first() #Check whether the user's supplied password is correct if not User.password.check(other.password, data.passwd2): return 'json:', dict(success=False, message=_("Second password incorrect."), data=data) #Make them type "merge" exactly if data.confirm != "merge": return 'json:', dict(success=False, message=_("Merge was either misspelled or not lowercase."), data=data) log.info("User %s merged account %s into %s.", user.username, other.username, user.username) user.merge(other) #Redirect user to the root of the server instead of the settings page return 'json:', dict(success=True, location="/") else: return 'json:', dict(success=False, message=_("Form does not exist."), location="/") return 'json:', dict(success=True, location="/account/settings")
def authorize(self, success=None, failure=None): """Prepare a incoming session request. Error 'message' attributes are temporary; base your logic on the status and code attributes. success: web.core.url:URL (required) failure: web.core.url:URL (required) returns: location: web.core.url:URL the location to direct users to """ # Ensure success and failure URLs are present. if success is None: response.status_int = 400 return dict( status = 'error', code = 'argument.success.missing', message = "URL to return users to upon successful authentication is missing from your request." ) if failure is None: response.status_int = 400 return dict( status = 'error', code = 'argument.failure.missing', message = "URL to return users to upon authentication failure or dismissal is missing from your request." ) # Also ensure they are valid URIs. try: success_ = success success = URL(success) except: response.status_int = 400 return dict( status = 'error', code = 'argument.success.malformed', message = "Successful authentication URL is malformed." ) try: failure_ = failure failure = URL(failure) except: response.status_int = 400 return dict( status = 'error', code = 'argument.response.malformed', message = "URL to return users to upon successful authentication is missing from your request." ) # Deny localhost/127.0.0.1 loopbacks and 192.* and 10.* unless in development mode. if not boolean(config.get('debug', False)) and (success.host in ('localhost', '127.0.0.1') or \ success.host.startswith('192.168.') or \ success.host.startswith('10.')): response.status_int = 400 return dict( status = 'error', code = 'development-only', message = "Loopback and local area-network URLs disallowd in production." ) # Check blacklist and bail early. if AuthenticationBlacklist.objects(reduce(__or__, [ Q(scheme=success.scheme), Q(scheme=failure.scheme), Q(protocol=success.port or success.scheme), Q(protocol=failure.port or failure.scheme), ] + ([] if not success.host else [ Q(domain=success.host) ]) + ([] if not failure.host else [ Q(domain=failure.host) ]))).count(): response.status_int = 400 return dict( status = 'error', code = 'blacklist', message = "You have been blacklisted. To dispute, contact {0}".format(config['mail.blackmail.author']) ) # TODO: Check DNS. Yes, really. # Generate authentication token. log.info("Creating request for {0} with callbacks {1} and {2}.".format(request.service, success_, failure_)) ar = AuthenticationRequest( request.service, # We have an authenticated request, so we know the service ID is valid. success = success_, failure = failure_ ) ar.save() return dict( location = url.complete('/authorize/{0}'.format(ar.id)) )
def authenticate(identifier, password): """Given an e-mail address (or Yubikey OTP) and password, authenticate a user.""" ts = time() # Record the query = dict(active=True) # Gracefully handle extended characters in passwords. # The password storage algorithm works in binary. if isinstance(password, unicode): password = password.encode("utf8") # Build the MongoEngine query to find if "@" in identifier: query[b"email"] = identifier elif len(identifier) == 44: query[b"otp"] = identifier[:12] else: query[b"username"] = identifier user = User.objects(**query).first() if ( not user or not User.password.check(user.password, password) or (user.rotp and len(user.otp) != 0 and not "otp" in query) ): if user: LoginHistory(user, False, request.remote_addr).save() # Prevent basic timing attacks; always take at least one second to process. sleep(max(min(1 - (time() - ts), 0), 1)) return None # Validate Yubikey OTP if "otp" in query: client = yubico.Yubico( config["yubico.client"], config["yubico.key"], boolean(config.get("yubico.secure", False)) ) try: status = client.verify(identifier, return_response=True) except: return None if not status: return None user.update(set__seen=datetime.utcnow()) # Record the fact the user signed in. LoginHistory(user, True, request.remote_addr).save() # Update the user's host user.host = request.remote_addr # Check for other accounts with this IP address if len(User.objects(host=request.remote_addr)) > 1: # Quite possibly the worst code ever for u in User.objects(host=request.remote_addr): User.add_duplicate(user, u, IP=True) user.save() return user.id, user