Coverage for opt/mealie/lib/python3.12/site-packages/mealie/core/security/providers/credentials_provider.py: 64%

43 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 03:04 +0000

1from datetime import timedelta 

2 

3from sqlalchemy.orm.session import Session 

4 

5from mealie.core import root_logger 

6from mealie.core.config import get_app_settings 

7from mealie.core.exceptions import UserLockedOut 

8from mealie.core.security.hasher import get_hasher 

9from mealie.core.security.providers.auth_provider import AuthProvider 

10from mealie.db.models.users.users import AuthMethod 

11from mealie.repos.all_repositories import get_repositories 

12from mealie.schema.user.auth import CredentialsRequest 

13from mealie.services.user_services.user_service import UserService 

14 

15 

16class CredentialsProvider(AuthProvider[CredentialsRequest]): 

17 """Authentication provider that authenticates a user the database using username/password combination""" 

18 

19 _logger = root_logger.get_logger("credentials_provider") 

20 

21 def __init__(self, session: Session, data: CredentialsRequest) -> None: 

22 super().__init__(session, data) 

23 

24 def authenticate(self) -> tuple[str, timedelta] | None: 

25 """Attempt to authenticate a user given a username and password""" 

26 settings = get_app_settings() 

27 db = get_repositories(self.session, group_id=None, household_id=None) 

28 user = self.try_get_user(self.data.username) 

29 

30 if not user: 30 ↛ 31line 30 didn't jump to line 31 because the condition on line 30 was never true

31 self.verify_fake_password() 

32 return None 

33 

34 if user.auth_method != AuthMethod.MEALIE: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true

35 self.verify_fake_password() 

36 self._logger.warning( 

37 "Found user but their auth method is not 'Mealie'. Unable to continue with credentials login" 

38 ) 

39 return None 

40 

41 if user.login_attemps >= settings.SECURITY_MAX_LOGIN_ATTEMPTS or user.is_locked: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true

42 raise UserLockedOut() 

43 

44 if not CredentialsProvider.verify_password(self.data.password, user.password): 44 ↛ 45line 44 didn't jump to line 45 because the condition on line 44 was never true

45 user.login_attemps += 1 

46 db.users.update(user.id, user) 

47 

48 if user.login_attemps >= settings.SECURITY_MAX_LOGIN_ATTEMPTS: 

49 user_service = UserService(db) 

50 user_service.lock_user(user) 

51 

52 return None 

53 

54 user.login_attemps = 0 

55 user = db.users.update(user.id, user) 

56 return self.get_access_token(user, self.data.remember_me) # type: ignore 

57 

58 def verify_fake_password(self): 

59 # To prevent user enumeration we perform the verify_password computation to ensure 

60 # server side time is relatively constant and not vulnerable to timing attacks. 

61 CredentialsProvider.verify_password( 

62 "abc123cba321", 

63 "$2b$12$JdHtJOlkPFwyxdjdygEzPOtYmdQF5/R5tHxw5Tq8pxjubyLqdIX5i", 

64 ) 

65 

66 @staticmethod 

67 def verify_password(plain_password: str, hashed_password: str) -> bool: 

68 """Compares a plain string to a hashed password""" 

69 return get_hasher().verify(plain_password, hashed_password)