Coverage for opt/mealie/lib/python3.12/site-packages/mealie/core/security/providers/credentials_provider.py: 72%
43 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 17:29 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 17:29 +0000
1from datetime import timedelta 1a
3from sqlalchemy.orm.session import Session 1a
5from mealie.core import root_logger 1a
6from mealie.core.config import get_app_settings 1a
7from mealie.core.exceptions import UserLockedOut 1a
8from mealie.core.security.hasher import get_hasher 1a
9from mealie.core.security.providers.auth_provider import AuthProvider 1a
10from mealie.db.models.users.users import AuthMethod 1a
11from mealie.repos.all_repositories import get_repositories 1a
12from mealie.schema.user.auth import CredentialsRequest 1a
13from mealie.services.user_services.user_service import UserService 1a
16class CredentialsProvider(AuthProvider[CredentialsRequest]): 1a
17 """Authentication provider that authenticates a user the database using username/password combination"""
19 _logger = root_logger.get_logger("credentials_provider") 1a
21 def __init__(self, session: Session, data: CredentialsRequest) -> None: 1a
22 super().__init__(session, data) 1fghqrsijklmnotbuvwxyzcABCDEFGdHIJKLMNpOPQRSTUe
24 def authenticate(self) -> tuple[str, timedelta] | None: 1a
25 """Attempt to authenticate a user given a username and password"""
26 settings = get_app_settings() 1fghqrsijklmnotbuvwxyzcABCDEFGdHIJKLMNpOPQRSTUe
27 db = get_repositories(self.session, group_id=None, household_id=None) 1fghqrsijklmnotbuvwxyzcABCDEFGdHIJKLMNpOPQRSTUe
28 user = self.try_get_user(self.data.username) 1fghqrsijklmnotbuvwxyzcABCDEFGdHIJKLMNpOPQRSTUe
30 if not user: 1fghqrsijklmnotbuvwxyzcABCDEFGdHIJKLMNpOPQRSTUe
31 self.verify_fake_password() 1qrstbuvwxyzcABCDEFGdHIJKLMNOPQRSTUe
32 return None 1qrstbuvwxyzcABCDEFGdHIJKLMNOPQRSTUe
34 if user.auth_method != AuthMethod.MEALIE: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true1fghijklmnobcdpe
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
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 true1fghijklmnobcdpe
42 raise UserLockedOut()
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 true1fghijklmnobcdpe
45 user.login_attemps += 1
46 db.users.update(user.id, user)
48 if user.login_attemps >= settings.SECURITY_MAX_LOGIN_ATTEMPTS:
49 user_service = UserService(db)
50 user_service.lock_user(user)
52 return None
54 user.login_attemps = 0 1fghijklmnobcdpe
55 user = db.users.update(user.id, user) 1fghijklmnobcdpe
56 return self.get_access_token(user, self.data.remember_me) # type: ignore 1fghijklmnobcdpe
58 def verify_fake_password(self): 1a
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( 1qrstbuvwxyzcABCDEFGdHIJKLMNOPQRSTUe
62 "abc123cba321",
63 "$2b$12$JdHtJOlkPFwyxdjdygEzPOtYmdQF5/R5tHxw5Tq8pxjubyLqdIX5i",
64 )
66 @staticmethod 1a
67 def verify_password(plain_password: str, hashed_password: str) -> bool: 1a
68 """Compares a plain string to a hashed password"""
69 return get_hasher().verify(plain_password, hashed_password) 1fghqrsijklmnotbuvwxyzcABCDEFGdHIJKLMNpOPQRSTUe