test_authentication.py 6.8 KB

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