test_authentication.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. from datetime import datetime, timedelta
  2. import pytest
  3. from flaskbb.auth.services import authentication as auth
  4. from flaskbb.core.auth.authentication import (AuthenticationFailureHandler,
  5. AuthenticationProvider,
  6. PostAuthenticationHandler,
  7. StopAuthentication)
  8. from flaskbb.plugins import spec
  9. from freezegun import freeze_time
  10. from pluggy import HookimplMarker
  11. from pytz import UTC
  12. pytestmark = pytest.mark.usefixtures('default_settings')
  13. class TestBlockTooManyFailedLogins(object):
  14. provider = auth.BlockTooManyFailedLogins(
  15. auth.
  16. FailedLoginConfiguration(limit=1, lockout_window=timedelta(hours=1))
  17. )
  18. @freeze_time(datetime(2018, 1, 1, 13, 30))
  19. def test_raises_StopAuthentication_if_user_is_at_limit_and_inside_window(
  20. self, Fred
  21. ):
  22. Fred.last_failed_login = datetime(2018, 1, 1, 14, tzinfo=UTC)
  23. Fred.login_attempts = 1
  24. with pytest.raises(StopAuthentication) as excinfo:
  25. self.provider.authenticate(Fred.email, 'not considered')
  26. assert 'too many failed login attempts' in excinfo.value.reason
  27. @freeze_time(datetime(2018, 1, 1, 14))
  28. def test_doesnt_raise_if_user_is_at_limit_but_outside_window(self, Fred):
  29. Fred.last_failed_login = datetime(2018, 1, 1, 12, tzinfo=UTC)
  30. Fred.login_attempts = 1
  31. self.provider.authenticate(Fred.email, 'not considered')
  32. def test_doesnt_raise_if_user_is_below_limit_but_inside_window(self, Fred):
  33. Fred.last_failed_login = datetime(2018, 1, 1, 12, tzinfo=UTC)
  34. Fred.login_attempts = 0
  35. self.provider.authenticate(Fred.email, 'not considered')
  36. def test_handles_if_user_has_no_failed_login_attempts(self, Fred):
  37. Fred.login_attempts = 0
  38. Fred.last_failed_login = None
  39. self.provider.authenticate(Fred.email, 'not considered')
  40. def test_handles_if_user_doesnt_exist(self, Fred):
  41. self.provider.authenticate('completely@made.up', 'not considered')
  42. class TestDefaultFlaskBBAuthProvider(object):
  43. provider = auth.DefaultFlaskBBAuthProvider()
  44. def test_returns_None_if_user_doesnt_exist(self, Fred):
  45. result = self.provider.authenticate('completely@made.up', 'lolnope')
  46. assert result is None
  47. def test_returns_None_if_password_doesnt_match(self, Fred):
  48. result = self.provider.authenticate(Fred.email, 'stillnotit')
  49. assert result is None
  50. def test_returns_user_if_identifer_and_password_match(self, Fred):
  51. result = self.provider.authenticate(Fred.email, 'fred')
  52. assert result.username == Fred.username
  53. class TestMarkFailedLoginAttempt(object):
  54. handler = auth.MarkFailedLogin()
  55. @freeze_time(datetime(2018, 1, 1, 12))
  56. def test_increments_users_failed_logins_and_sets_last_fail_date(
  57. self, Fred
  58. ):
  59. Fred.login_attempts = 0
  60. Fred.last_failed_login = datetime.min.replace(tzinfo=UTC)
  61. self.handler.handle_authentication_failure(Fred.email)
  62. assert Fred.login_attempts == 1
  63. assert Fred.last_failed_login == datetime.now(UTC)
  64. def test_handles_if_user_doesnt_exist(self, Fred):
  65. self.handler.handle_authentication_failure('completely@made.up')
  66. class TestClearFailedLogins(object):
  67. handler = auth.ClearFailedLogins()
  68. def test_clears_failed_logins_attempts(self, Fred):
  69. Fred.login_attempts = 1000
  70. self.handler.handle_post_auth(Fred)
  71. assert Fred.login_attempts == 0
  72. class TestBlockUnactivatedUser(object):
  73. handler = auth.BlockUnactivatedUser()
  74. def test_raises_StopAuthentication_if_user_is_unactivated(
  75. self, unactivated_user
  76. ):
  77. with pytest.raises(StopAuthentication) as excinfo:
  78. self.handler.handle_post_auth(unactivated_user)
  79. assert 'In order to use your account' in excinfo.value.reason
  80. class TestPluginAuthenticationManager(object):
  81. def raises_stop_authentication_if_user_isnt_authenticated(
  82. self, plugin_manager, mocker, database
  83. ):
  84. service = self._get_auth_manager(plugin_manager, database)
  85. auth = mocker.MagicMock(spec=AuthenticationProvider)
  86. plugin_manager.register(self.impls(auth=auth))
  87. with pytest.raises(StopAuthentication) as excinfo:
  88. service.authenticate('doesnt exist', 'nope')
  89. auth.assert_called_once_with(identifier='doesnt exist', secret='nope')
  90. assert excinfo.value.reason == "Wrong username or password."
  91. def test_runs_failed_hooks_when_stopauthentication_is_raised(
  92. self, plugin_manager, mocker, database
  93. ):
  94. service = self._get_auth_manager(plugin_manager, database)
  95. failure = mocker.MagicMock(spec=AuthenticationFailureHandler)
  96. plugin_manager.register(self.impls(failure=failure))
  97. with pytest.raises(StopAuthentication):
  98. service.authenticate('doesnt exist', 'nope')
  99. failure.assert_called_once_with(identifier='doesnt exist')
  100. def test_runs_post_auth_handler_if_user_authenticates(
  101. self, plugin_manager, mocker, Fred, database
  102. ):
  103. service = self._get_auth_manager(plugin_manager, database)
  104. auth = mocker.MagicMock(spec=AuthenticationProvider, return_value=Fred)
  105. post_auth = mocker.MagicMock(spec=PostAuthenticationHandler)
  106. plugin_manager.register(self.impls(auth=auth, post_auth=post_auth))
  107. service.authenticate(Fred.email, 'fred')
  108. auth.assert_called_once_with(identifier=Fred.email, secret='fred')
  109. post_auth.assert_called_once_with(user=Fred)
  110. def test_reraises_if_session_commit_fails(
  111. self, mocker, plugin_manager, Fred
  112. ):
  113. class NotAnActualException(Exception):
  114. pass
  115. db = mocker.Mock()
  116. db.session.commit.side_effect = NotAnActualException
  117. service = self._get_auth_manager(plugin_manager, db)
  118. with pytest.raises(NotAnActualException):
  119. service.authenticate('doesnt exist', 'nope')
  120. db.session.rollback.assert_called_once_with()
  121. def _get_auth_manager(self, plugin_manager, db):
  122. plugin_manager.add_hookspecs(spec)
  123. return auth.PluginAuthenticationManager(
  124. plugin_manager, session=db.session
  125. )
  126. @staticmethod
  127. def impls(auth=None, post_auth=None, failure=None):
  128. impl = HookimplMarker('flaskbb')
  129. class Impls:
  130. if auth is not None:
  131. @impl
  132. def flaskbb_authenticate(self, identifier, secret):
  133. return auth(identifier=identifier, secret=secret)
  134. if post_auth is not None:
  135. @impl
  136. def flaskbb_post_authenticate(self, user):
  137. post_auth(user=user)
  138. if failure is not None:
  139. @impl
  140. def flaskbb_authentication_failed(self, identifier):
  141. failure(identifier=identifier)
  142. return Impls()