test_authentication_security.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """Test authentication security to prevent user enumeration."""
  2. from unittest.mock import MagicMock, patch
  3. import pytest
  4. from flask import Flask
  5. from flask_restx import Api
  6. import services.errors.account
  7. from controllers.console.auth.error import AuthenticationFailedError
  8. from controllers.console.auth.login import LoginApi
  9. from controllers.console.error import AccountNotFound
  10. class TestAuthenticationSecurity:
  11. """Test authentication endpoints for security against user enumeration."""
  12. def setup_method(self):
  13. """Set up test fixtures."""
  14. self.app = Flask(__name__)
  15. self.api = Api(self.app)
  16. self.api.add_resource(LoginApi, "/login")
  17. self.client = self.app.test_client()
  18. self.app.config["TESTING"] = True
  19. @patch("controllers.console.wraps.db")
  20. @patch("controllers.console.auth.login.FeatureService.get_system_features")
  21. @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
  22. @patch("controllers.console.auth.login.AccountService.authenticate")
  23. @patch("controllers.console.auth.login.AccountService.send_reset_password_email")
  24. @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
  25. @patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
  26. def test_login_invalid_email_with_registration_allowed(
  27. self, mock_get_invitation, mock_send_email, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
  28. ):
  29. """Test that invalid email sends reset password email when registration is allowed."""
  30. # Arrange
  31. mock_is_rate_limit.return_value = False
  32. mock_get_invitation.return_value = None
  33. mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
  34. mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
  35. mock_features.return_value.is_allow_register = True
  36. mock_send_email.return_value = "token123"
  37. # Act
  38. with self.app.test_request_context(
  39. "/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"}
  40. ):
  41. login_api = LoginApi()
  42. result = login_api.post()
  43. # Assert
  44. assert result == {"result": "fail", "data": "token123", "code": "account_not_found"}
  45. mock_send_email.assert_called_once_with(email="nonexistent@example.com", language="en-US")
  46. @patch("controllers.console.wraps.db")
  47. @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
  48. @patch("controllers.console.auth.login.AccountService.authenticate")
  49. @patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
  50. @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
  51. @patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
  52. def test_login_wrong_password_returns_error(
  53. self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_db
  54. ):
  55. """Test that wrong password returns AuthenticationFailedError."""
  56. # Arrange
  57. mock_is_rate_limit.return_value = False
  58. mock_get_invitation.return_value = None
  59. mock_authenticate.side_effect = services.errors.account.AccountPasswordError("Wrong password")
  60. mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
  61. # Act
  62. with self.app.test_request_context(
  63. "/login", method="POST", json={"email": "existing@example.com", "password": "WrongPass123!"}
  64. ):
  65. login_api = LoginApi()
  66. # Assert
  67. with pytest.raises(AuthenticationFailedError) as exc_info:
  68. login_api.post()
  69. assert exc_info.value.error_code == "authentication_failed"
  70. assert exc_info.value.description == "Invalid email or password."
  71. mock_add_rate_limit.assert_called_once_with("existing@example.com")
  72. @patch("controllers.console.wraps.db")
  73. @patch("controllers.console.auth.login.FeatureService.get_system_features")
  74. @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
  75. @patch("controllers.console.auth.login.AccountService.authenticate")
  76. @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
  77. @patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
  78. def test_login_invalid_email_with_registration_disabled(
  79. self, mock_get_invitation, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
  80. ):
  81. """Test that invalid email raises AccountNotFound when registration is disabled."""
  82. # Arrange
  83. mock_is_rate_limit.return_value = False
  84. mock_get_invitation.return_value = None
  85. mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
  86. mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
  87. mock_features.return_value.is_allow_register = False
  88. # Act
  89. with self.app.test_request_context(
  90. "/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"}
  91. ):
  92. login_api = LoginApi()
  93. # Assert
  94. with pytest.raises(AccountNotFound) as exc_info:
  95. login_api.post()
  96. assert exc_info.value.error_code == "account_not_found"
  97. @patch("controllers.console.wraps.db")
  98. @patch("controllers.console.auth.login.FeatureService.get_system_features")
  99. @patch("controllers.console.auth.login.AccountService.get_user_through_email")
  100. @patch("controllers.console.auth.login.AccountService.send_reset_password_email")
  101. def test_reset_password_with_existing_account(self, mock_send_email, mock_get_user, mock_features, mock_db):
  102. """Test that reset password returns success with token for existing accounts."""
  103. # Mock the setup check
  104. mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
  105. # Test with existing account
  106. mock_get_user.return_value = MagicMock(email="existing@example.com")
  107. mock_send_email.return_value = "token123"
  108. with self.app.test_request_context("/reset-password", method="POST", json={"email": "existing@example.com"}):
  109. from controllers.console.auth.login import ResetPasswordSendEmailApi
  110. api = ResetPasswordSendEmailApi()
  111. result = api.post()
  112. assert result == {"result": "success", "data": "token123"}