Repository: hydroshare/hydroshare

Issue:
Allow basic auth using email
**Description of the bug**
Authentication attempt using hsrest client with bad (typo etc) credentials.
```
---------------------------------------------------------------------------
HydroShareHTTPException                   Traceback (most recent call last)
<ipython-input-4-9e65448ed82e> in <module>
----> 1 hs.getResourceFile('bc14db4a50bd4d89bb750126a0a1ba79', file_name, destination=PROJECT_DIR)

/cvmfs/cybergis.illinois.edu/software/conda/cjw/pyrhessys-2024-02/lib/python3.7/site-packages/hs_restclient/__init__.py in getResourceFile(self, pid, filename, destination)
    901                 raise HydroShareNotFound((pid, filename))
    902             else:
--> 903                 raise HydroShareHTTPException(r)
    904 
    905         if destination is None:

HydroShareHTTPException: Received status 500 <!doctype html>
<html lang="en">
```
```
[06/Apr/2024 09:29:39] ERROR [django.request:224] Internal Server Error: /hsapi/resource/bc14db4a50bd4d89bb750126a0a1ba79/files/CoweetaSub18_10m_TEST.zip/
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 497, in dispatch
    self.initial(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 414, in initial
    self.perform_authentication(request)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 324, in perform_authentication
    request.user
  File "/usr/local/lib/python3.9/site-packages/rest_framework/request.py", line 227, in user
    self._authenticate()
  File "/usr/local/lib/python3.9/site-packages/rest_framework/request.py", line 380, in _authenticate
    user_auth_tuple = authenticator.authenticate(self)
  File "/hydroshare/hs_core/authentication.py", line 96, in authenticate
    user = User.objects.get(username=decoded_username)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 435, in get
    raise self.model.DoesNotExist(
django.contrib.auth.models.User.DoesNotExist: User matching query does not exist.
```

Add exception handling to [query for user](https://github.com/hydroshare/hydroshare/blob/2.13.6/hs_core/authentication.py#L96) to return 403 or other appropriate responses instead of 500.

**Additional information**
hs V 2.13.6



Discussion:
The query for the user needs to be updated to support both username and email. The [current query](https://github.com/hydroshare/hydroshare/blob/2.13.6/hs_core/authentication.py#L96) only queries the username.
Refactor the [username_or_email](https://github.com/hydroshare/hydroshare/blob/develop/theme/backends.py#L45) query to a utility function and use it for the queries.
This is making more sense to me as I think about it. @imanmaghami was happily using his email/password and the query works with the authenticator that checks the django password. The implementation for checking the keycloak password was implemented and has a bug that only supports the username and does not support the email. With the latest release, the new authenticator runs first and throws User Does Not Exist exception when the email is provided. The old authenticator never runs because of the exception thrown.
related to https://github.com/hydroshare/hydroshare/issues/5415

Resolving patch:
diff --git a/hs_core/authentication.py b/hs_core/authentication.py
--- a/hs_core/authentication.py
+++ b/hs_core/authentication.py
@@ -5,9 +5,9 @@
 from django.urls import reverse, resolve
 from django.conf import settings
 from django.utils.http import urlencode
-from django.contrib.auth.models import User
 from rest_framework.authentication import BaseAuthentication
 from keycloak.keycloak_openid import KeycloakOpenID
+from theme.utils import get_user_from_username_or_email
 
 
 class HydroShareOIDCAuthenticationBackend(OIDCAuthenticationBackend):
@@ -92,6 +92,4 @@ def authenticate(self, request):
             KEYCLOAK.token(decoded_username, decoded_password)
         except Exception:
             return None
-
-        user = User.objects.get(username=decoded_username)
-        return (user, None)
+        return get_user_from_username_or_email(decoded_username)
diff --git a/theme/backends.py b/theme/backends.py
--- a/theme/backends.py
+++ b/theme/backends.py
@@ -3,9 +3,9 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth.backends import ModelBackend
 from django.contrib.auth.tokens import default_token_generator, PasswordResetTokenGenerator
-from django.db.models import Q
 from django.utils.http import base36_to_int, urlsafe_base64_decode
 from six import text_type
+from theme.utils import get_user_from_username_or_email
 
 
 class WithoutLoggedInDateTokenGenerator(PasswordResetTokenGenerator):
@@ -40,17 +40,12 @@ class CaseInsensitiveMezzanineBackend(ModelBackend):
 
     def authenticate(self, *args, **kwargs):
         if kwargs:
-            username = kwargs.pop("username", None)
-            if username:
-                username_or_email = Q(username__iexact=username) | Q(email__iexact=username)
+            username_or_email = kwargs.pop("username", None)
+            if username_or_email:
                 password = kwargs.pop("password", None)
-                try:
-                    user = User.objects.get(username_or_email, **kwargs)
-                except User.DoesNotExist:
-                    pass
-                else:
-                    if user.check_password(password):
-                        return user
+                user = get_user_from_username_or_email(username_or_email, **kwargs)
+                if user and user.check_password(password):
+                    return user
             else:
                 if 'uidb36' in kwargs:
                     kwargs["id"] = base36_to_int(kwargs.pop("uidb36"))
diff --git a/theme/utils.py b/theme/utils.py
--- a/theme/utils.py
+++ b/theme/utils.py
@@ -1,6 +1,7 @@
 import os
 from uuid import uuid4
 from datetime import date, timedelta
+from django.db.models import Q
 
 
 def _get_upload_path(folder_name, name, filename):
@@ -82,3 +83,19 @@ def get_quota_message(user):
                                                               zone=uq.zone,
                                                               percent=rounded_percent)
         return return_msg
+
+
+def get_user_from_username_or_email(username_or_email, **kwargs):
+    """
+    Get a user object from a username or email address
+    :param username_or_email: username or email address
+    :return: a user object, or None if no user is found
+    """
+    from django.contrib.auth import get_user_model
+    User = get_user_model()
+    try:
+        return User.objects.get(
+            Q(username__iexact=username_or_email)
+            | Q(email__iexact=username_or_email), **kwargs)
+    except User.DoesNotExist:
+        return None


Test patch: