Skip to content

Commit

Permalink
fix: Prevent login via credentials when Auth Method is Mealie (#4370)
Browse files Browse the repository at this point in the history
  • Loading branch information
cmintey authored Oct 16, 2024
1 parent 03485ec commit 80caa5f
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Mealie supports 3rd party authentication via [OpenID Connect (OIDC)](https://ope

Signing in with OAuth will automatically find your account in Mealie and link to it. If a user does not exist in Mealie, then one will be created (if enabled), but will be unable to log in with any other authentication method. An admin can configure another authentication method for such a user.

If a user previously accessed Mealie via credentials and you want to no longer allow users to log in with `LDAP` or `Mealie` credentials, then you can set the user's *Authentication Method* to `OIDC`. Conversely, if a user's auth method is not `OIDC`, then they can still log in with whatever their auth method is as well as OIDC.

## Provider Setup

Before you can start using OIDC Authentication, you must first configure a new client application in your identity provider. Your identity provider must support the OAuth **Authorization Code flow with PKCE**. The steps will vary by provider, but generally, the steps are as follows.
Expand Down
21 changes: 16 additions & 5 deletions mealie/core/security/providers/credentials_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from mealie.core.exceptions import UserLockedOut
from mealie.core.security.hasher import get_hasher
from mealie.core.security.providers.auth_provider import AuthProvider
from mealie.db.models.users.users import AuthMethod
from mealie.repos.all_repositories import get_repositories
from mealie.schema.user.auth import CredentialsRequest
from mealie.services.user_services.user_service import UserService
Expand All @@ -27,11 +28,13 @@ def authenticate(self) -> tuple[str, timedelta] | None:
user = self.try_get_user(self.data.username)

if not user:
# To prevent user enumeration we perform the verify_password computation to ensure
# server side time is relatively constant and not vulnerable to timing attacks.
CredentialsProvider.verify_password(
"abc123cba321",
"$2b$12$JdHtJOlkPFwyxdjdygEzPOtYmdQF5/R5tHxw5Tq8pxjubyLqdIX5i",
self.verify_fake_password()
return None

if user.auth_method != AuthMethod.MEALIE:
self.verify_fake_password()
self._logger.warning(
"Found user but their auth method is not 'Mealie'. Unable to continue with credentials login"
)
return None

Expand All @@ -52,6 +55,14 @@ def authenticate(self) -> tuple[str, timedelta] | None:
user = db.users.update(user.id, user)
return self.get_access_token(user, self.data.remember_me) # type: ignore

def verify_fake_password(self):
# To prevent user enumeration we perform the verify_password computation to ensure
# server side time is relatively constant and not vulnerable to timing attacks.
CredentialsProvider.verify_password(
"abc123cba321",
"$2b$12$JdHtJOlkPFwyxdjdygEzPOtYmdQF5/R5tHxw5Tq8pxjubyLqdIX5i",
)

@staticmethod
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Compares a plain string to a hashed password"""
Expand Down
8 changes: 6 additions & 2 deletions mealie/core/security/providers/ldap_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ def __init__(self, session: Session, data: CredentialsRequest) -> None:
self.conn = None

def authenticate(self) -> tuple[str, timedelta] | None:
"""Attempt to authenticate a user given a username and password"""
"""Attempt to authenticate a user given a username and password against an LDAP provider"""
# When LDAP is enabled, we need to still also support authentication with Mealie backend
# First we look to see if we have a user. If we don't we'll attempt to create one with LDAP
# If we do find a user, we will check if their auth method is LDAP and attempt to authenticate
# Otherwise, we will proceed with Mealie authentication
user = self.try_get_user(self.data.username)
if not user or user.password == "LDAP" or user.auth_method == AuthMethod.LDAP:
if not user or user.auth_method == AuthMethod.LDAP:
user = self.get_user()
if user:
return self.get_access_token(user, self.data.remember_me)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from mealie.core.security.providers.credentials_provider import CredentialsProvider
from mealie.db.models.users.users import AuthMethod
from mealie.schema.user.auth import CredentialsRequest
from tests.utils.fixture_schemas import TestUser


def test_login(unique_user: TestUser):
data = {"username": unique_user.username, "password": unique_user.password}
auth_provider = CredentialsProvider(unique_user.repos.session, CredentialsRequest(**data))

assert auth_provider.authenticate() is not None


def test_login_incorrect_auth_method(unique_user: TestUser):
db = unique_user.repos
user = db.users.get_by_username(unique_user.username)
user.auth_method = AuthMethod.OIDC
db.users.update(unique_user.user_id, user)

data = {"username": unique_user.username, "password": unique_user.password}
auth_provider = CredentialsProvider(db.session, CredentialsRequest(**data))

assert auth_provider.authenticate() is None

0 comments on commit 80caa5f

Please sign in to comment.