diff --git a/.github/workflows/pip-compile.yml b/.github/workflows/pip-compile.yml index 9f906bd1..72bd62a0 100644 --- a/.github/workflows/pip-compile.yml +++ b/.github/workflows/pip-compile.yml @@ -23,7 +23,7 @@ jobs: python-version: "3.10" - name: Update requirements files - uses: UW-GAC/pip-tools-actions/update-requirements-files@v0.1 + uses: UW-GAC/pip-tools-actions/update-requirements-files@v0.2 with: requirements_files: |- requirements/requirements.in diff --git a/config/settings/base.py b/config/settings/base.py index 3ed866c2..2f98275c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -160,6 +160,7 @@ "maintenance_mode.middleware.MaintenanceModeMiddleware", "simple_history.middleware.HistoryRequestMiddleware", "django_htmx.middleware.HtmxMiddleware", + "allauth.account.middleware.AccountMiddleware", ] # STATIC diff --git a/primed/drupal_oauth_provider/provider.py b/primed/drupal_oauth_provider/provider.py index 92c84dd0..a43370cd 100644 --- a/primed/drupal_oauth_provider/provider.py +++ b/primed/drupal_oauth_provider/provider.py @@ -2,11 +2,14 @@ from allauth.account.models import EmailAddress from allauth.socialaccount import app_settings, providers +from allauth.socialaccount.adapter import get_adapter from allauth.socialaccount.providers.base import ProviderAccount from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from .views import CustomAdapter + logger = logging.getLogger(__name__) DRUPAL_PROVIDER_ID = "drupal_oauth_provider" @@ -24,9 +27,16 @@ class CustomAccount(ProviderAccount): class CustomProvider(OAuth2Provider): - id = "drupal_oauth_provider" + id = DRUPAL_PROVIDER_ID name = OVERRIDE_NAME account_class = CustomAccount + oauth2_adapter_class = CustomAdapter + supports_token_authentication = True + + def __init__(self, request, app=None): + if app is None: + app = get_adapter().get_app(request, self.id) + super().__init__(request, app=app) def extract_uid(self, data): return str(data["sub"]) diff --git a/primed/drupal_oauth_provider/tests.py b/primed/drupal_oauth_provider/tests.py index 5ccfffbc..e59eb6f2 100644 --- a/primed/drupal_oauth_provider/tests.py +++ b/primed/drupal_oauth_provider/tests.py @@ -1,11 +1,27 @@ +import base64 +import copy import datetime +import hashlib import json +import sys +from urllib.parse import parse_qs, urlparse import jwt +import requests +from allauth.socialaccount import app_settings from allauth.socialaccount.adapter import get_adapter +from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken +from allauth.socialaccount.providers.oauth2.client import OAuth2Error from allauth.socialaccount.tests import OAuth2TestsMixin from allauth.tests import MockedResponse, TestCase +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.messages.storage.fallback import FallbackStorage +from django.contrib.sites.models import Site +from django.core.exceptions import ImproperlyConfigured +from django.test import RequestFactory from django.test.utils import override_settings +from django.urls import reverse from .provider import CustomProvider @@ -65,25 +81,43 @@ def sign_id_token(payload): # Mocked version of the test data from /oauth/jwks -KEY_SERVER_RESP_JSON = json.dumps( - { - "keys": [ - { - "kty": TESTING_JWT_KEYSET["kty"], - "n": TESTING_JWT_KEYSET["n"], - "e": TESTING_JWT_KEYSET["e"], - } - ] - } -) +KEY_SERVER_RESP = { + "keys": [ + { + "kty": TESTING_JWT_KEYSET["kty"], + "n": TESTING_JWT_KEYSET["n"], + "e": TESTING_JWT_KEYSET["e"], + } + ] +} +KEY_SERVER_RESP_INVALID = copy.deepcopy(KEY_SERVER_RESP) +KEY_SERVER_RESP_INVALID["keys"][0]["kty"] = "nuts" +KEY_SERVER_RESP_JSON = json.dumps(KEY_SERVER_RESP) +KEY_SERVER_RESP_JSON_INVALID = json.dumps(KEY_SERVER_RESP_INVALID) +print(f"KEY_RESP_VALID: {KEY_SERVER_RESP_JSON}", file=sys.stderr) # disable token storing for testing as it conflicts with drupals use # of tokens for user info -@override_settings(SOCIALACCOUNT_STORE_TOKENS=False) +@override_settings(SOCIALACCOUNT_STORE_TOKENS=True) class CustomProviderTests(OAuth2TestsMixin, TestCase): provider_id = CustomProvider.id + def setUp(self): + super(CustomProviderTests, self).setUp() + self.factory = RequestFactory() + # workaround to create a session. see: + # https://code.djangoproject.com/ticket/11475 + User = get_user_model() + user = User.objects.create_user("testuser", "testuser@testuser.com", "testpw") + self.client.login(username="testuser", password="testpw") + self.setup_time = datetime.datetime.now(datetime.timezone.utc) + + # Create a social account for testing + self.social_account = SocialAccount.objects.create( + provider=self.provider.id, user=user, uid="1234", extra_data={} + ) + # Provide two mocked responses, first is to the public key request # second is used for the profile request for extra data def get_mocked_response(self): @@ -101,21 +135,83 @@ def get_mocked_response(self): ), ] - # This login response mimics drupals in that it contains a set of scopes - # and the uid which has the name sub - def get_login_response_json(self, with_refresh_token=True): - now = datetime.datetime.now(datetime.timezone.utc) + def login(self, resp_mock=None, process="login", with_refresh_token=True): + """ + Unfortunately due to how our provider works we need to alter + this test login function as the default one fails. + """ + with self.mocked_response(): + resp = self.client.post(self.provider.get_login_url(self.request, process=process)) + p = urlparse(resp["location"]) + q = parse_qs(p.query) + pkce_enabled = app_settings.PROVIDERS.get(self.app.provider, {}).get( + "OAUTH_PKCE_ENABLED", self.provider.pkce_enabled_default + ) + + self.assertEqual("code_challenge" in q, pkce_enabled) + self.assertEqual("code_challenge_method" in q, pkce_enabled) + if pkce_enabled: + code_challenge = q["code_challenge"][0] + self.assertEqual(q["code_challenge_method"][0], "S256") + + complete_url = self.provider.get_callback_url() + self.assertGreater(q["redirect_uri"][0].find(complete_url), 0) + response_json = self.get_login_response_json(with_refresh_token=with_refresh_token) + + resp_mocks = resp_mock if isinstance(resp_mock, list) else ([resp_mock] if resp_mock is not None else []) + + with self.mocked_response( + MockedResponse(200, response_json, {"content-type": "application/json"}), + *resp_mocks, + ): + resp = self.client.get(complete_url, self.get_complete_parameters(q)) + + # Find the access token POST request, and assert that it contains + # the correct code_verifier if and only if PKCE is enabled + request_calls = requests.Session.request.call_args_list + + for args, kwargs in request_calls: + data = kwargs.get("data", {}) + if ( + args + and args[0] == "POST" + and isinstance(data, dict) + and data.get("redirect_uri", "").endswith(complete_url) + ): + self.assertEqual("code_verifier" in data, pkce_enabled) + + if pkce_enabled: + hashed_code_verifier = hashlib.sha256(data["code_verifier"].encode("ascii")) + expected_code_challenge = ( + base64.urlsafe_b64encode(hashed_code_verifier.digest()).rstrip(b"=").decode() + ) + self.assertEqual(code_challenge, expected_code_challenge) + + return resp + + def get_id_token(self): app = get_adapter().get_app(request=None, provider=self.provider_id) allowed_audience = app.client_id - id_token = sign_id_token( + return sign_id_token( { - "exp": now + datetime.timedelta(hours=1), - "iat": now, + "exp": self.setup_time + datetime.timedelta(hours=1), + "iat": self.setup_time, "aud": allowed_audience, "scope": ["authenticated", "oauth_client_user"], "sub": 20122, } ) + + def get_access_token(self) -> str: + return self.get_id_token() + + def get_expected_to_str(self): + return "test@testmaster.net" + + # This login response mimics drupals in that it contains a set of scopes + # and the uid which has the name sub + def get_login_response_json(self, with_refresh_token=True): + id_token = self.get_id_token() response_data = { "access_token": id_token, "expires_in": 3600, @@ -125,3 +221,113 @@ def get_login_response_json(self, with_refresh_token=True): if with_refresh_token: response_data["refresh_token"] = "testrf" return json.dumps(response_data) + + def test_authentication_error(self): + # Create a request + + request = self.factory.get(reverse("drupal_oauth_provider_login")) + + # Add session and messages middleware + from django.contrib.sessions.middleware import SessionMiddleware + + middleware = SessionMiddleware(lambda x: x) + middleware.process_request(request) + request.session.save() + + # Add messages support + + messages = FallbackStorage(request) + setattr(request, "_messages", messages) + + # Create adapter instance + from primed.drupal_oauth_provider.views import CustomAdapter + + adapter = CustomAdapter(request) + # Create a SocialToken instance + token = SocialToken(app=self.app, account=self.social_account, token="invalid_token") + + with self.assertRaisesRegex(OAuth2Error, "Invalid id_token"): + # Simulate the error condition of a bad token + with self.mocked_response(self.get_mocked_response()[0]): + adapter.complete_login(request, app=self.app, token=token, response={"error": "invalid_grant"}) + + with self.assertRaisesRegex(OAuth2Error, "Error retrieving drupal public key"): + # Simulate the error condition of invalid json + with self.mocked_response(MockedResponse(200, "[lkjsdd]")): + adapter.complete_login(request, app=self.app, token=token, response={"error": "invalid_grant"}) + + with self.assertRaisesRegex(OAuth2Error, "failed to convert jwk"): + # Simulate the error condition of invalid jwk + with self.mocked_response( + MockedResponse(200, KEY_SERVER_RESP_JSON_INVALID), + ): + adapter.complete_login(request, app=self.app, token=token, response={"error": "invalid_grant"}) + + +class TestProviderConfig(TestCase): + def setUp(self): + # workaround to create a session. see: + # https://code.djangoproject.com/ticket/11475 + current_site = Site.objects.get_current() + app = SocialApp.objects.create( + provider=CustomProvider.id, + name=CustomProvider.id, + client_id="app123id", + key=CustomProvider.id, + secret="dummy", + ) + self.app = app + self.app.sites.add(current_site) + + def test_custom_provider_no_app(self): + rf = RequestFactory() + request = rf.get("/fake-url/") + provider = CustomProvider(request) + assert provider.app is not None + + def test_custom_provider_scope_config(self): + custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS + rf = RequestFactory() + request = rf.get("/fake-url/") + custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = None + with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): + with self.assertRaises(ImproperlyConfigured): + CustomProvider(request, app=self.app).get_provider_scope_config() + + def test_custom_provider_scope_config_not_list(self): + custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS + rf = RequestFactory() + request = rf.get("/fake-url/") + custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = {"not_a_list": 1} + with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): + with self.assertRaises(ImproperlyConfigured): + CustomProvider(request, app=self.app).get_provider_scope_config() + + def test_custom_provider_scope_detail_config(self): + custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS + rf = RequestFactory() + request = rf.get("/fake-url/") + custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = [ + { + "z_drupal_machine_name": "X", + "request_scope": True, + "django_group_name": "Z", + } + ] + with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): + with self.assertRaises(ImproperlyConfigured): + CustomProvider(request, app=self.app).get_provider_managed_scope_status() + + def test_custom_provider_has_scope(self): + custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS + rf = RequestFactory() + request = rf.get("/fake-url/") + custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = [ + { + "drupal_machine_name": "X", + "request_scope": True, + "django_group_name": "Z", + } + ] + with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): + CustomProvider(request, app=self.app).get_provider_managed_scope_status(scopes_granted=["X"]) diff --git a/primed/drupal_oauth_provider/views.py b/primed/drupal_oauth_provider/views.py index c8a78575..16a69e7f 100644 --- a/primed/drupal_oauth_provider/views.py +++ b/primed/drupal_oauth_provider/views.py @@ -12,13 +12,11 @@ OAuth2LoginView, ) -from .provider import CustomProvider - logger = logging.getLogger(__name__) class CustomAdapter(OAuth2Adapter): - provider_id = CustomProvider.id + provider_id = "drupal_oauth_provider" provider_settings = app_settings.PROVIDERS.get(provider_id, {}) @@ -60,7 +58,8 @@ def get_public_key(self, headers): try: public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(public_key_jwk)) except Exception as e: - logger.error(f"[get_public_key] failed to convert jwk to public key {e}") + logger.error(f"[get_public_key] failed to convert jwk {public_key_jwk} to public key {e}") + raise OAuth2Error(f"[get_public_key] failed to convert jwk {public_key_jwk} to public key {e}") else: return public_key @@ -74,7 +73,6 @@ def get_scopes_from_token(self, id_token, headers): scopes = None try: - unverified_header = jwt.get_unverified_header(id_token.token) token_payload = jwt.decode( id_token.token, public_key, @@ -85,9 +83,6 @@ def get_scopes_from_token(self, id_token, headers): except jwt.PyJWTError as e: logger.error(f"Invalid id_token {e} {id_token.token}") raise OAuth2Error("Invalid id_token") from e - except Exception as e: - logger.error(f"Other exception parsing token {e} header {unverified_header} token {id_token}") - raise OAuth2Error("Error when decoding token {e}") else: scopes = token_payload.get("scope") diff --git a/primed/templates/socialaccount/authentication_error.html b/primed/templates/socialaccount/authentication_error.html index 747a4513..c4d9bd56 100644 --- a/primed/templates/socialaccount/authentication_error.html +++ b/primed/templates/socialaccount/authentication_error.html @@ -1,8 +1,8 @@ -{% extends "socialaccount/base.html" %} +{% extends "base.html" %} {% load i18n %} -{% block head_title %}{% trans "Social Network Login Failure" %}{% endblock %} +{% block title %}{% trans "Social Network Login Failure" %}{% endblock %} {% block content %}

{% trans "Social Network Login Failure" %}

diff --git a/primed/templates/socialaccount/snippets/provider_list.html b/primed/templates/socialaccount/snippets/provider_list.html index d89feade..1fd9f5d3 100644 --- a/primed/templates/socialaccount/snippets/provider_list.html +++ b/primed/templates/socialaccount/snippets/provider_list.html @@ -3,16 +3,6 @@ {% get_providers as socialaccount_providers %} {% for provider in socialaccount_providers %} -{% if provider.id == "openid" %} -{% for brand in provider.get_brands %} -

- {{brand.name}} -

-{% endfor %} -{% endif %}

diff --git a/primed/users/adapters.py b/primed/users/adapters.py index a10c944c..12011304 100644 --- a/primed/users/adapters.py +++ b/primed/users/adapters.py @@ -32,20 +32,20 @@ def update_user_info(self, user, extra_data: Dict, apply_update=True): user_changed = False if user.name != full_name: logger.info( - f"[SocialAccountAdatpter:update_user_name] user {user} " f"name updated from {user.name} to {full_name}" + f"[SocialAccountAdatpter:update_user_info] user {user} " f"name updated from {user.name} to {full_name}" ) user.name = full_name user_changed = True if user.username != drupal_username: logger.info( - f"[SocialAccountAdatpter:update_user_name] user {user} " + f"[SocialAccountAdatpter:update_user_info] user {user} " f"username updated from {user.username} to {drupal_username}" ) user.username = drupal_username user_changed = True if user.email != drupal_email: logger.info( - f"[SocialAccountAdatpter:update_user_name] user {user}" + f"[SocialAccountAdatpter:update_user_info] user {user}" f" email updated from {user.email} to {drupal_email}" ) user.email = drupal_email @@ -140,10 +140,13 @@ def update_user_data(self, sociallogin: Any): self.update_user_study_sites(user, extra_data) self.update_user_groups(user, extra_data) - def authentication_error(self, request, provider_id, error, exception, extra_context): + def on_authentication_error(self, request, provider_id, error, exception, extra_context): """ Invoked when there is an error in auth cycle. Log so we know what is going on. """ - logger.error(f"[SocialAccountAdapter:authentication_error] Error {error} Exception: {exception}") - super().authentication_error(request, provider_id, error, exception, extra_context) + logger.error( + f"[SocialAccountAdapter:on_authentication_error] Provider: {provider_id} " + f"Error {error} Exception: {exception} extra {extra_context}" + ) + super().on_authentication_error(request, provider_id, error, exception, extra_context) diff --git a/primed/users/tests/test_adapters.py b/primed/users/tests/test_adapters.py index cae571dc..df8835a8 100644 --- a/primed/users/tests/test_adapters.py +++ b/primed/users/tests/test_adapters.py @@ -1,63 +1,122 @@ import pytest from allauth.account import app_settings as account_settings -from allauth.socialaccount.helpers import complete_social_login -from allauth.socialaccount.models import SocialAccount, SocialLogin -from allauth.utils import get_user_model +from allauth.account import signals +from allauth.socialaccount.models import SocialAccount, SocialApp, SocialLogin +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.auth.models import AnonymousUser -from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.sites.models import Site from django.core import mail from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings +from primed.drupal_oauth_provider.provider import CustomProvider from primed.primed_anvil.tests.factories import StudySiteFactory from primed.users.adapters import AccountAdapter, SocialAccountAdapter from .factories import GroupFactory, UserFactory +User = get_user_model() -@pytest.mark.django_db -class TestsUserSocialLoginAdapter(object): - @override_settings( - SOCIALACCOUNT_AUTO_SIGNUP=True, - ACCOUNT_SIGNUP_FORM_CLASS=None, - ACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.NONE, # noqa - ) - def test_drupal_social_login_adapter(self): - factory = RequestFactory() - request = factory.get("/accounts/login/callback/") - request.user = AnonymousUser() - SessionMiddleware(lambda request: None).process_request(request) - MessageMiddleware(lambda request: None).process_request(request) - User = get_user_model() - user = User() - old_name = "Old Name" - old_username = "test" - old_email = "test@example.com" - setattr(user, account_settings.USER_MODEL_USERNAME_FIELD, old_username) - setattr(user, "name", old_name) - setattr(user, account_settings.USER_MODEL_EMAIL_FIELD, old_email) - - account = SocialAccount( +class SocialAccountAdapterTest(TestCase): + def setUp(self): + self.factory = RequestFactory() + # Setup a mock social app + current_site = Site.objects.get_current() + self.social_app = SocialApp.objects.create( + provider=CustomProvider.id, + name="DOA", + client_id="test-client-id", + secret="test-client-secret", + ) + self.social_app.sites.add(current_site) + self.user = User.objects.create(username="testuser", email="testuser@example.com") + new_first_name = "Bob" + new_last_name = "Rob" + self.social_account = SocialAccount.objects.create( + user=self.user, provider="drupal_oauth_provider", - uid="123", - extra_data=dict( - first_name="Old", - last_name="Name", - email=old_email, - preferred_username=old_username, - ), + uid="12345", + extra_data={ + "preferred_username": "testuser", + "first_name": new_first_name, + "last_name": new_last_name, + "email": "testuser@example.com", + }, ) - sociallogin = SocialLogin(user=user, account=account) - complete_social_login(request, sociallogin) - user = User.objects.get(**{account_settings.USER_MODEL_USERNAME_FIELD: old_username}) - assert SocialAccount.objects.filter(user=user, uid=account.uid).exists() is True - assert user.name == old_name - assert user.username == old_username - assert user.email == old_email + # Create a mock SocialLogin object and associate the user and social account + self.sociallogin = SocialLogin(user=self.user, account=self.social_account) + + def test_social_login_success(self): + # Mock user + request = self.factory.get("/") + middleware = SessionMiddleware(lambda x: None) + middleware.process_request(request) + request.session.save() + middleware = AuthenticationMiddleware(lambda x: None) + middleware.process_request(request) + request.user = AnonymousUser() + # user = User.objects.create(username="testuser", email="testuser@example.com") + + # # # Mock social login + # # Create a mock SocialAccount and link it to the user + new_first_name = "Bob" + new_last_name = "Rob" + # social_account = SocialAccount.objects.create( + # user=user, + # provider="drupal_oauth_provider", + # uid="12345", + # extra_data={ + # "preferred_username": "testuser", + # "first_name": new_first_name, + # "last_name": new_last_name, + # "email": "testuser@example.com", + # }, + # ) + + # # Create a mock SocialLogin object and associate the user and social account + # sociallogin = SocialLogin(user=user, account=social_account) + + # Simulate social login + from allauth.account.adapter import get_adapter + + # adapter = SocialAccountAdapter() + adapter = get_adapter(request) + + adapter.login(request, self.user) + + signals.user_logged_in.send( + sender=self.user.__class__, + request=request, + user=self.user, + sociallogin=self.sociallogin, + ) + # Check if the login completed successfully + self.assertEqual(self.sociallogin.user, self.user) + self.assertEqual(request.user, self.user) + self.assertEqual(self.user.name, f"{new_first_name} {new_last_name}") + + def test_authentication_error_with_callback(self): + """Test authentication error during callback processing""" + from django.urls import reverse + + callback_url = reverse("drupal_oauth_provider_callback") + response = self.client.get(callback_url, {"error": "access_denied"}) + self.assertTemplateUsed( + response, + "socialaccount/authentication_error.%s" % getattr(settings, "ACCOUNT_TEMPLATE_EXTENSION", "html"), + ) + # # Check if the response redirects to the login error page + # #self.assertEqual(response.status_code, 302) + # import sys + # print(f"RESP {response} ", file=sys.stderr) + # self.assertIn('socialaccount/authentication_error', response.url) def test_update_user_info(self): adapter = SocialAccountAdapter() @@ -122,7 +181,7 @@ def test_update_user_study_sites_remove(self): assert user.study_sites.all().count() == 1 @override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend") - def test_update_user_study_sites_unknown(self, settings): + def test_update_user_study_sites_unknown(self): adapter = SocialAccountAdapter() user = UserFactory() @@ -198,10 +257,14 @@ def test_update_user_groups_malformed(self): def test_account_is_open_for_signup(self): request = RequestFactory() adapter = AccountAdapter() + social_adapter = SocialAccountAdapter() assert adapter.is_open_for_signup(request) is True + assert social_adapter.is_open_for_signup(request=request, sociallogin=self.sociallogin) is True @override_settings(ACCOUNT_ALLOW_REGISTRATION=False) def test_account_is_not_open_for_signup(self): request = RequestFactory() adapter = AccountAdapter() + social_adapter = SocialAccountAdapter() assert adapter.is_open_for_signup(request) is False + assert social_adapter.is_open_for_signup(request=request, sociallogin=self.sociallogin) is False diff --git a/primed/users/tests/test_views.py b/primed/users/tests/test_views.py index a03da768..73d41a58 100644 --- a/primed/users/tests/test_views.py +++ b/primed/users/tests/test_views.py @@ -1,6 +1,7 @@ import json import pytest +from allauth.socialaccount.models import SocialApp from anvil_consortium_manager.models import AnVILProjectManagerAccess from anvil_consortium_manager.tests.factories import ( AccountFactory, @@ -11,6 +12,7 @@ from django.contrib.auth.models import AnonymousUser, Permission from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.sites.models import Site from django.http import HttpRequest from django.shortcuts import resolve_url from django.test import RequestFactory, TestCase @@ -22,6 +24,7 @@ NonDataAffiliateAgreementFactory, ) from primed.dbgap.tests.factories import dbGaPApplicationFactory +from primed.drupal_oauth_provider.provider import CustomProvider from primed.primed_anvil.tests.factories import StudySiteFactory from primed.users.forms import UserChangeForm from primed.users.models import User @@ -189,6 +192,22 @@ def test_view_links(self, client, user: User, rf: RequestFactory): assert account.get_absolute_url() not in str(response.content) +class LoginViewTest(TestCase): + def setUp(self): + current_site = Site.objects.get_current() + self.social_app = SocialApp.objects.create( + provider=CustomProvider.id, + name="DOA", + client_id="test-client-id", + secret="test-client-secret", + ) + self.social_app.sites.add(current_site) + + def test_basic_login_view_render(self): + response = self.client.get(reverse("account_login")) + assert response.status_code == 200 + + class UserDetailTest(TestCase): def setUp(self): self.factory = RequestFactory() diff --git a/requirements/requirements.in b/requirements/requirements.in index acccee7a..9e075aeb 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -5,6 +5,11 @@ pip-tools whitenoise # https://github.com/evansd/whitenoise oauthlib # https://github.com/oauthlib/oauthlib +# Allauth adapter related needs +cryptography # https://github.com/pyca/cryptography +pyjwt # https://github.com/jpadilla/pyjwt +requests-oauthlib # https://github.com/requests/requests-oauthlib + # Password hashing argon2-cffi # https://github.com/hynek/argon2_cffi diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 252db5f1..fbb7b32b 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -37,9 +37,7 @@ crispy-bootstrap5==2024.10 # -r requirements/requirements.in # django-anvil-consortium-manager cryptography==43.0.1 - # via pyjwt -defusedxml==0.7.1 - # via python3-openid + # via -r requirements/requirements.in django==4.2.16 # via # -r requirements/requirements.in @@ -56,13 +54,13 @@ django==4.2.16 # django-picklefield # django-simple-history # django-tables2 -django-allauth==0.54.0 +django-allauth==65.0.2 # via -r requirements/requirements.in django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.25 # via -r requirements/requirements.in django-autocomplete-light==3.11.0 # via django-anvil-consortium-manager -django-constance==4.1.2 +django-constance==4.1.3 # via -r requirements/requirements.in django-crispy-forms==2.3 # via @@ -145,8 +143,8 @@ pyasn1-modules==0.3.0 # via google-auth pycparser==2.21 # via cffi -pyjwt[crypto]==2.4.0 - # via django-allauth +pyjwt==2.4.0 + # via -r requirements/requirements.in pyparsing==3.1.1 # via packaging pyproject-hooks==1.0.0 @@ -159,8 +157,6 @@ python-dateutil==2.8.2 # pronto python-fsutil==0.13.1 # via django-maintenance-mode -python3-openid==3.2.0 - # via django-allauth pytz==2023.3.post1 # via # django-anvil-consortium-manager @@ -173,12 +169,11 @@ referencing==0.33.0 requests==2.32.3 # via # -r requirements/requirements.in - # django-allauth # django-anvil-consortium-manager # jsonapi-requests # requests-oauthlib requests-oauthlib==1.3.1 - # via django-allauth + # via -r requirements/requirements.in rpds-py==0.17.1 # via # jsonschema