def _basic_auth_login(username, password, request): login_service = request.find_service(IUserService, context=None) breach_service = request.find_service(IPasswordBreachedService, context=None) userid = login_service.find_userid(username) if userid is not None: user = login_service.get_user(userid) is_disabled, disabled_for = login_service.is_disabled(user.id) if is_disabled and disabled_for == DisableReason.CompromisedPassword: # This technically violates the contract a little bit, this function is # meant to return None if the user cannot log in. However we want to present # a different error message than is normal when we're denying the log in # becasue of a compromised password. So to do that, we'll need to raise a # HTTPError that'll ultimately get returned to the client. This is OK to do # here because we've already successfully authenticated the credentials, so # it won't screw up the fall through to other authentication mechanisms # (since we wouldn't have fell through to them anyways). raise _format_exc_status(BasicAuthBreachedPassword(), breach_service.failure_message_plain) elif login_service.check_password( user.id, password, tags=["method:auth", "auth_method:basic"]): if breach_service.check_password( password, tags=["method:auth", "auth_method:basic"]): send_password_compromised_email(request, user) login_service.disable_password( user.id, reason=DisableReason.CompromisedPassword) raise _format_exc_status(BasicAuthBreachedPassword(), breach_service.failure_message_plain) else: login_service.update_user( user.id, last_login=datetime.datetime.utcnow()) return _authenticate(user.id, request)
def validate_password(self, field): # Before we try to validate the user's password, we'll first to check to see if # they are disabled. userid = self.user_service.find_userid(self.username.data) if userid is not None: is_disabled, disabled_for = self.user_service.is_disabled(userid) if is_disabled and disabled_for == DisableReason.CompromisedPassword: raise wtforms.validators.ValidationError( jinja2.Markup(self.breach_service.failure_message) ) # Do our typical validation of the password. super().validate_password(field) # If we have a user ID, then we'll go and check it against our breached password # service. If the password has appeared in a breach or is otherwise compromised # we will disable the user and reject the login. if userid is not None: if self.breach_service.check_password( field.data, tags=["method:auth", "auth_method:login_form"] ): user = self.user_service.get_user(userid) send_password_compromised_email(self.request, user) self.user_service.disable_password( user.id, reason=DisableReason.CompromisedPassword ) raise wtforms.validators.ValidationError( jinja2.Markup(self.breach_service.failure_message) )
def validate_password(self, field): # Before we try to validate the user's password, we'll first to check to see if # they are disabled. userid = self.user_service.find_userid(self.username.data) if userid is not None: is_disabled, disabled_for = self.user_service.is_disabled(userid) if is_disabled and disabled_for == DisableReason.CompromisedPassword: raise wtforms.validators.ValidationError( jinja2.Markup(self.breach_service.failure_message) ) # Do our typical validation of the password. super().validate_password(field) # If we have a user ID, then we'll go and check it against our breached password # service. If the password has appeared in a breach or is otherwise compromised # we will disable the user and reject the login. if userid is not None: if self.breach_service.check_password( field.data, tags=["method:auth", "auth_method:login_form"] ): user = self.user_service.get_user(userid) send_password_compromised_email(self.request, user) self.user_service.disable_password( user.id, reason=DisableReason.CompromisedPassword ) raise wtforms.validators.ValidationError( jinja2.Markup(self.breach_service.failure_message) )
def user_reset_password(request): user = request.db.query(User).get(request.matchdict["user_id"]) if user.username != request.params.get("username"): request.session.flash(f"Wrong confirmation input", queue="error") return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id)) login_service = request.find_service(IUserService, context=None) send_password_compromised_email(request, user) login_service.disable_password(user.id, reason=DisableReason.CompromisedPassword) request.session.flash(f"Reset password for {user.username!r}", queue="success") return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
def _basic_auth_login(username, password, request): login_service = request.find_service(IUserService, context=None) breach_service = request.find_service(IPasswordBreachedService, context=None) userid = login_service.find_userid(username) if userid is not None: user = login_service.get_user(userid) is_disabled, disabled_for = login_service.is_disabled(user.id) if is_disabled and disabled_for == DisableReason.CompromisedPassword: # This technically violates the contract a little bit, this function is # meant to return None if the user cannot log in. However we want to present # a different error message than is normal when we're denying the log in # becasue of a compromised password. So to do that, we'll need to raise a # HTTPError that'll ultimately get returned to the client. This is OK to do # here because we've already successfully authenticated the credentials, so # it won't screw up the fall through to other authentication mechanisms # (since we wouldn't have fell through to them anyways). raise _format_exc_status( BasicAuthBreachedPassword(), breach_service.failure_message_plain ) elif login_service.check_password( user.id, password, tags=["method:auth", "auth_method:basic"] ): if breach_service.check_password( password, tags=["method:auth", "auth_method:basic"] ): send_password_compromised_email(request, user) login_service.disable_password( user.id, reason=DisableReason.CompromisedPassword ) raise _format_exc_status( BasicAuthBreachedPassword(), breach_service.failure_message_plain ) else: login_service.update_user( user.id, last_login=datetime.datetime.utcnow() ) return _authenticate(user.id, request)
def test_password_compromised_email( self, pyramid_request, pyramid_config, monkeypatch, verified ): stub_user = pretend.stub( username="******", name="", email="*****@*****.**", primary_email=pretend.stub(email="*****@*****.**", verified=verified), ) subject_renderer = pyramid_config.testing_add_renderer( "email/password-compromised/subject.txt" ) subject_renderer.string_response = "Email Subject" body_renderer = pyramid_config.testing_add_renderer( "email/password-compromised/body.txt" ) body_renderer.string_response = "Email Body" html_renderer = pyramid_config.testing_add_renderer( "email/password-compromised/body.html" ) html_renderer.string_response = "Email HTML Body" send_email = pretend.stub( delay=pretend.call_recorder(lambda *args, **kwargs: None) ) pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email) monkeypatch.setattr(email, "send_email", send_email) result = email.send_password_compromised_email(pyramid_request, stub_user) assert result == {} assert pyramid_request.task.calls == [pretend.call(send_email)] assert send_email.delay.calls == [ pretend.call( f"{stub_user.username} <{stub_user.email}>", attr.asdict( EmailMessage( subject="Email Subject", body_text="Email Body", body_html=( "<html>\n<head></head>\n" "<body><p>Email HTML Body</p></body>\n</html>\n" ), ) ), ) ]
def test_password_compromised_email(self, pyramid_request, pyramid_config, monkeypatch, verified): stub_user = pretend.stub( username="******", name="", email="*****@*****.**", primary_email=pretend.stub(email="*****@*****.**", verified=verified), ) subject_renderer = pyramid_config.testing_add_renderer( "email/password-compromised/subject.txt") subject_renderer.string_response = "Email Subject" body_renderer = pyramid_config.testing_add_renderer( "email/password-compromised/body.txt") body_renderer.string_response = "Email Body" html_renderer = pyramid_config.testing_add_renderer( "email/password-compromised/body.html") html_renderer.string_response = "Email HTML Body" send_email = pretend.stub( delay=pretend.call_recorder(lambda *args, **kwargs: None)) pyramid_request.task = pretend.call_recorder( lambda *args, **kwargs: send_email) monkeypatch.setattr(email, "send_email", send_email) result = email.send_password_compromised_email(pyramid_request, stub_user) assert result == {} assert pyramid_request.task.calls == [pretend.call(send_email)] assert send_email.delay.calls == [ pretend.call( f"{stub_user.username} <{stub_user.email}>", attr.asdict( EmailMessage( subject="Email Subject", body_text="Email Body", body_html=( "<html>\n<head></head>\n" "<body><p>Email HTML Body</p></body>\n</html>\n"), )), ) ]