Beispiel #1
0
    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="/")
Beispiel #2
0
    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="/")
Beispiel #3
0
 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()
Beispiel #4
0
    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"
            ))
Beispiel #5
0
    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()
Beispiel #6
0
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)
Beispiel #7
0
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)
Beispiel #8
0
    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="/")
Beispiel #9
0
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)
Beispiel #10
0
 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)
Beispiel #11
0
    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)
Beispiel #12
0
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)
Beispiel #13
0
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)
Beispiel #14
0
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
Beispiel #15
0
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)
Beispiel #16
0
 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="/")
Beispiel #17
0
 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="/")
Beispiel #18
0
    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="/")
Beispiel #19
0
    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))
Beispiel #20
0
    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"))
Beispiel #21
0
    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))
Beispiel #22
0
    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)))
Beispiel #23
0
    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")
Beispiel #24
0
    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")
Beispiel #25
0
 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))
         )
Beispiel #26
0
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