From 871e15eecdcb4540edfb3a83f6ea672f763259d3 Mon Sep 17 00:00:00 2001 From: RuthShryock Date: Wed, 17 Jul 2024 14:50:19 -0400 Subject: [PATCH 1/5] override django-allauth login method to fix sso and first time login authentication --- kobo/apps/accounts/adapter.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/kobo/apps/accounts/adapter.py b/kobo/apps/accounts/adapter.py index 90ce55c95e..8c2ac10058 100644 --- a/kobo/apps/accounts/adapter.py +++ b/kobo/apps/accounts/adapter.py @@ -2,7 +2,7 @@ from allauth.account.forms import SignupForm from constance import config from django.conf import settings -from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth import REDIRECT_FIELD_NAME, login from django.db import transaction from django.shortcuts import resolve_url from django.template.response import TemplateResponse @@ -85,3 +85,9 @@ def set_password(self, user, password): ) user.set_password(password) user.save() + + def login(self, request, user): + # Override django-allauth login method to use specified authentication backend + super().login(request, user) + user.backend = settings.AUTHENTICATION_BACKENDS[0] + login(request, user, backend=user.backend) From b94e2cb4fdd3a53c4c87d3d953fdf3cf74a75b23 Mon Sep 17 00:00:00 2001 From: RuthShryock Date: Mon, 22 Jul 2024 12:21:51 -0400 Subject: [PATCH 2/5] reorder methods alphabetically --- kobo/apps/accounts/adapter.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/kobo/apps/accounts/adapter.py b/kobo/apps/accounts/adapter.py index 8c2ac10058..3dffb750ec 100644 --- a/kobo/apps/accounts/adapter.py +++ b/kobo/apps/accounts/adapter.py @@ -17,6 +17,15 @@ class AccountAdapter(DefaultAccountAdapter): + def is_open_for_signup(self, request): + return config.REGISTRATION_OPEN + + def login(self, request, user): + # Override django-allauth login method to use specified authentication backend + super().login(request, user) + user.backend = settings.AUTHENTICATION_BACKENDS[0] + login(request, user, backend=user.backend) + def pre_login(self, request, user, **kwargs): if parent_response := super().pre_login(request, user, **kwargs): @@ -61,9 +70,6 @@ def pre_login(self, request, user, **kwargs): context=context, ) - def is_open_for_signup(self, request): - return config.REGISTRATION_OPEN - def save_user(self, request, user, form, commit=True): # Compare allauth SignupForm with our custom field standard_fields = set(SignupForm().fields.keys()) @@ -85,9 +91,3 @@ def set_password(self, user, password): ) user.set_password(password) user.save() - - def login(self, request, user): - # Override django-allauth login method to use specified authentication backend - super().login(request, user) - user.backend = settings.AUTHENTICATION_BACKENDS[0] - login(request, user, backend=user.backend) From 68ec044b5382eabd13a019b41ad4d3a6690be89c Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Wed, 24 Jul 2024 17:36:19 -0400 Subject: [PATCH 3/5] Unit test for SSO with auth back end --- kobo/apps/accounts/tests/test_backend.py | 124 +++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 kobo/apps/accounts/tests/test_backend.py diff --git a/kobo/apps/accounts/tests/test_backend.py b/kobo/apps/accounts/tests/test_backend.py new file mode 100644 index 0000000000..62fe92d56e --- /dev/null +++ b/kobo/apps/accounts/tests/test_backend.py @@ -0,0 +1,124 @@ +import json +from mock import patch + +import responses +from allauth.socialaccount.models import SocialAccount, SocialApp +from django.conf import settings +from django.test.utils import override_settings +from django.test import TestCase +from django.urls import reverse +from rest_framework import status + +# TODO Replace this two lines with the two commented out below when merge with +# release 2.024.25 +from django.contrib.auth import get_user_model +User = get_user_model() + +# from kobo.apps.openrosa.apps.main.models import UserProfile +# from kobo.apps.kobo_auth.shortcuts import User + +# TODO refactor this and use `SOCIALACCOUNT_PROVIDERS` at only one place +# see test_templatetags.py + +SOCIALACCOUNT_PROVIDERS = { + "openid_connect": { + "SERVERS": [ + { + "id": "test-app", + "name": "Test App", + "server_url": "http://testserver/oauth", + "APP": { + "client_id": "test.service.id", + "secret": "test.service.secret", + }, + } + ] + } +} + + +class SSOLoginTest(TestCase): + + def setUp(self): + # Create a user for the test + testuser = User.objects.create_user( + username='testuser', + email='testuser@testserver', + password='password', + ) + + # Will be needed when merged in release/2.024.25 + # UserProfile.objects.create(user=testuser) + + # Delete any social app that could be added by migration + # `0007_add_providers_from_environment_to_db` + SocialApp.objects.all().delete() + + self.extra_data = { + 'username': 'testuser', + 'sub': 'testuser', + 'preferred_username': 'testuser', + 'email': 'testuser@testserver', + } + + # Create a social account for user + self.social_account = SocialAccount.objects.create( + user=testuser, + provider='test-app', + uid='testuser', + extra_data=self.extra_data, + ) + + @override_settings(SOCIALACCOUNT_PROVIDERS=SOCIALACCOUNT_PROVIDERS) + @responses.activate + @patch('allauth.socialaccount.models.SocialLogin.verify_and_unstash_state') + def test_keep_django_auth_backend_with_sso(self, mock_verify_and_unstash_state): + mock_verify_and_unstash_state.return_value = {'process': 'login'} + + # Mock `requests` responses to fool django-allauth + responses.add( + responses.GET, + 'http://testserver/oauth/.well-known/openid-configuration', + status=status.HTTP_200_OK, + content_type='application/json', + body=json.dumps({ + 'token_endpoint': 'http://testserver/oauth/token', + 'authorization_endpoint': 'http://testserver/oauth/authorize', + 'userinfo_endpoint': 'http://testserver/oauth/userinfo', + }), + ) + + responses.add( + responses.POST, + 'http://testserver/oauth/token', + status=status.HTTP_200_OK, + content_type='application/json', + body=json.dumps({ + 'access_token': 'mock_access_token', + 'refresh_token': 'mock_refresh_token' + }), + ) + + responses.add( + responses.GET, + 'http://testserver/oauth/userinfo', + status=status.HTTP_200_OK, + content_type='application/json', + body=json.dumps(self.extra_data), + ) + + # Get SSO provider callback URL + sso_login_url = reverse( + 'openid_connect_callback', args=('openid_connect',) + ) + + # Simulate GET request to SSO provider + mock_sso_response = {'code': 'foobar'} + response = self.client.get(sso_login_url, data=mock_sso_response) + + # Ensure user is logged in + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertRedirects(response, reverse(settings.LOGIN_REDIRECT_URL)) + + self.assertTrue(response.wsgi_request.user.is_authenticated) + assert response.wsgi_request.user.backend == settings.AUTHENTICATION_BACKENDS[0] From b45ddf55f1c83498866dcce05889451ec67a7ae4 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Wed, 24 Jul 2024 17:36:19 -0400 Subject: [PATCH 4/5] Unit test for SSO with auth back end --- kobo/apps/accounts/adapter.py | 2 +- kobo/apps/accounts/tests/test_backend.py | 124 +++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 kobo/apps/accounts/tests/test_backend.py diff --git a/kobo/apps/accounts/adapter.py b/kobo/apps/accounts/adapter.py index 3dffb750ec..3330909c4c 100644 --- a/kobo/apps/accounts/adapter.py +++ b/kobo/apps/accounts/adapter.py @@ -23,7 +23,7 @@ def is_open_for_signup(self, request): def login(self, request, user): # Override django-allauth login method to use specified authentication backend super().login(request, user) - user.backend = settings.AUTHENTICATION_BACKENDS[0] + # user.backend = settings.AUTHENTICATION_BACKENDS[0] login(request, user, backend=user.backend) def pre_login(self, request, user, **kwargs): diff --git a/kobo/apps/accounts/tests/test_backend.py b/kobo/apps/accounts/tests/test_backend.py new file mode 100644 index 0000000000..759684d52a --- /dev/null +++ b/kobo/apps/accounts/tests/test_backend.py @@ -0,0 +1,124 @@ +import json +from mock import patch + +import responses +from allauth.socialaccount.models import SocialAccount, SocialApp +from django.conf import settings +from django.test.utils import override_settings +from django.test import TestCase +from django.urls import reverse +from rest_framework import status + +# TODO Replace this two lines with the two commented out below when merge with +# release 2.024.25 +from django.contrib.auth import get_user_model +User = get_user_model() + +# from kobo.apps.openrosa.apps.main.models import UserProfile +# from kobo.apps.kobo_auth.shortcuts import User + +# TODO refactor this and use `SOCIALACCOUNT_PROVIDERS` at only one place +# see test_templatetags.py + +SOCIALACCOUNT_PROVIDERS = { + 'openid_connect': { + 'SERVERS': [ + { + 'id': 'test-app', + 'name': 'Test App', + 'server_url': 'http://testserver/oauth', + 'APP': { + 'client_id': 'test.service.id', + 'secret': 'test.service.secret', + }, + } + ] + } +} + + +class SSOLoginTest(TestCase): + + def setUp(self): + # Create a user for the test + testuser = User.objects.create_user( + username='testuser', + email='testuser@testserver', + password='password', + ) + + # Will be needed when merged in release/2.024.25 + # UserProfile.objects.create(user=testuser) + + # Delete any social app that could be added by migration + # `0007_add_providers_from_environment_to_db` + SocialApp.objects.all().delete() + + self.extra_data = { + 'username': 'testuser', + 'sub': 'testuser', # `sub` is required by django allauth + 'preferred_username': 'testuser', + 'email': 'testuser@testserver', + } + + # Create a social account for user + self.social_account = SocialAccount.objects.create( + user=testuser, + provider='test-app', + uid='testuser', + extra_data=self.extra_data, + ) + + @override_settings(SOCIALACCOUNT_PROVIDERS=SOCIALACCOUNT_PROVIDERS) + @responses.activate + @patch('allauth.socialaccount.models.SocialLogin.verify_and_unstash_state') + def test_keep_django_auth_backend_with_sso(self, mock_verify_and_unstash_state): + mock_verify_and_unstash_state.return_value = {'process': 'login'} + + # Mock `requests` responses to fool django-allauth + responses.add( + responses.GET, + 'http://testserver/oauth/.well-known/openid-configuration', + status=status.HTTP_200_OK, + content_type='application/json', + body=json.dumps({ + 'token_endpoint': 'http://testserver/oauth/token', + 'authorization_endpoint': 'http://testserver/oauth/authorize', + 'userinfo_endpoint': 'http://testserver/oauth/userinfo', + }), + ) + + responses.add( + responses.POST, + 'http://testserver/oauth/token', + status=status.HTTP_200_OK, + content_type='application/json', + body=json.dumps({ + 'access_token': 'mock_access_token', + 'refresh_token': 'mock_refresh_token' + }), + ) + + responses.add( + responses.GET, + 'http://testserver/oauth/userinfo', + status=status.HTTP_200_OK, + content_type='application/json', + body=json.dumps(self.extra_data), + ) + + # Get SSO provider callback URL + sso_login_url = reverse( + 'openid_connect_callback', args=('openid_connect',) + ) + + # Simulate GET request to SSO provider + mock_sso_response = {'code': 'foobar'} + response = self.client.get(sso_login_url, data=mock_sso_response) + + # Ensure user is logged in + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertRedirects(response, reverse(settings.LOGIN_REDIRECT_URL)) + + self.assertTrue(response.wsgi_request.user.is_authenticated) + assert response.wsgi_request.user.backend == settings.AUTHENTICATION_BACKENDS[0] From a320ea7b7e412911bed9550e696926fc58b429cd Mon Sep 17 00:00:00 2001 From: RuthShryock Date: Thu, 25 Jul 2024 10:46:23 -0400 Subject: [PATCH 5/5] relocate SOCIALACCOUNT_PROVIDERS to constants.py and fix imports --- kobo/apps/accounts/tests/constants.py | 15 +++++++++++++ kobo/apps/accounts/tests/test_backend.py | 20 +----------------- kobo/apps/accounts/tests/test_templatetags.py | 21 +++---------------- 3 files changed, 19 insertions(+), 37 deletions(-) create mode 100644 kobo/apps/accounts/tests/constants.py diff --git a/kobo/apps/accounts/tests/constants.py b/kobo/apps/accounts/tests/constants.py new file mode 100644 index 0000000000..bf0a40325a --- /dev/null +++ b/kobo/apps/accounts/tests/constants.py @@ -0,0 +1,15 @@ +SOCIALACCOUNT_PROVIDERS = { + 'openid_connect': { + 'SERVERS': [ + { + 'id': 'test-app', + 'name': 'Test App', + 'server_url': 'http://testserver/oauth', + 'APP': { + 'client_id': 'test.service.id', + 'secret': 'test.service.secret', + }, + } + ] + } +} diff --git a/kobo/apps/accounts/tests/test_backend.py b/kobo/apps/accounts/tests/test_backend.py index 759684d52a..e4394389a7 100644 --- a/kobo/apps/accounts/tests/test_backend.py +++ b/kobo/apps/accounts/tests/test_backend.py @@ -16,25 +16,7 @@ # from kobo.apps.openrosa.apps.main.models import UserProfile # from kobo.apps.kobo_auth.shortcuts import User - -# TODO refactor this and use `SOCIALACCOUNT_PROVIDERS` at only one place -# see test_templatetags.py - -SOCIALACCOUNT_PROVIDERS = { - 'openid_connect': { - 'SERVERS': [ - { - 'id': 'test-app', - 'name': 'Test App', - 'server_url': 'http://testserver/oauth', - 'APP': { - 'client_id': 'test.service.id', - 'secret': 'test.service.secret', - }, - } - ] - } -} +from .constants import SOCIALACCOUNT_PROVIDERS class SSOLoginTest(TestCase): diff --git a/kobo/apps/accounts/tests/test_templatetags.py b/kobo/apps/accounts/tests/test_templatetags.py index bdc6aa8b79..96ba41c663 100644 --- a/kobo/apps/accounts/tests/test_templatetags.py +++ b/kobo/apps/accounts/tests/test_templatetags.py @@ -1,24 +1,9 @@ -from django.test import TestCase, override_settings from allauth.socialaccount.models import SocialApp +from django.test import TestCase, override_settings + from kobo.apps.accounts.models import SocialAppCustomData from kobo.apps.accounts.templatetags.get_provider_appname import get_social_apps - -# example app setup for testing -SOCIALACCOUNT_PROVIDERS = { - "openid_connect": { - "SERVERS": [ - { - "id": "test-app", - "name": "Test App", - "server_url": "https://example.org/oauth", - "APP": { - "client_id": "test.service.id", - "secret": "test.service.secret", - }, - } - ] - } -} +from .constants import SOCIALACCOUNT_PROVIDERS @override_settings(SOCIALACCOUNT_PROVIDERS=SOCIALACCOUNT_PROVIDERS)