diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py index b4a683e9b5f395aef06f93ce5d3df4a7343d6a6c..c3a17d642fbbb62abf7226a72dcdc3b66be04d1c 100644 --- a/common/djangoapps/third_party_auth/pipeline.py +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -209,6 +209,11 @@ def get(request): """Gets the running pipeline's data from the passed request.""" strategy = social_django.utils.load_strategy(request) token = strategy.session_get('partial_pipeline_token') + + if not token: + strategy.session_set('partial_pipeline_token', strategy.session_get('partial_pipeline_token_')) + token = strategy.session_get('partial_pipeline_token') + partial_object = strategy.partial_load(token) pipeline_data = None if partial_object: @@ -560,6 +565,10 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia return (current_provider and current_provider.slug in [saml_provider.slug for saml_provider in saml_providers_list]) + if current_partial: + strategy.session_set('partial_pipeline_token_', current_partial.token) + strategy.storage.partial.store(current_partial) + if not user: # Use only email for user existence check in case of saml provider if is_provider_saml(): diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index f29099308d0739392704b54472d81f2435a3e3dd..73521de0f5617730b26c1641bb2d6582a2961fc3 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -542,6 +542,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) request.user = self.create_user_models_for_existing_account( strategy, 'user@example.com', 'password', self.get_username(), skip_social_auth=True) + partial_pipeline_token = strategy.session_get('partial_pipeline_token') + partial_data = strategy.storage.partial.load(partial_pipeline_token) # Instrument the pipeline to get to the dashboard with the full # expected state. @@ -561,24 +563,14 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # We should be redirected back to the complete page, setting # the "logged in" cookie for the marketing site. - self.assert_logged_in_cookie_redirect(actions.do_complete( - request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access - redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request - )) + self.assert_logged_in_cookie_redirect(self.do_complete(strategy, request, partial_pipeline_token, partial_data)) # Set the cookie and try again self.set_logged_in_cookies(request) # Fire off the auth pipeline to link. self.assert_redirect_after_pipeline_completes( - actions.do_complete( - request.backend, - social_views._do_login, # pylint: disable=protected-access - request.user, - None, - redirect_field_name=auth.REDIRECT_FIELD_NAME, - request=request - ) + self.do_complete(strategy, request, partial_pipeline_token, partial_data) ) # Now we expect to be in the linked state, with a backend entry. @@ -694,6 +686,9 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( strategy, 'user@example.com', 'password', self.get_username()) + partial_pipeline_token = strategy.session_get('partial_pipeline_token') + partial_data = strategy.storage.partial.load(partial_pipeline_token) + self.assert_social_auth_exists_for_user(user, strategy) self.assertTrue(user.is_active) @@ -734,7 +729,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): self.set_logged_in_cookies(request) self.assert_redirect_after_pipeline_completes( - actions.do_complete(request.backend, social_views._do_login, user=user, request=request)) + self.do_complete(strategy, request, partial_pipeline_token, partial_data, user) + ) self.assert_account_settings_context_looks_correct(account_settings_context(request)) def test_signin_fails_if_account_not_active(self): @@ -793,6 +789,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri='social:complete') strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) + partial_pipeline_token = strategy.session_get('partial_pipeline_token') + partial_data = strategy.storage.partial.load(partial_pipeline_token) # Begin! Grab the registration page and check the login control on it. self.assert_register_response_before_pipeline_looks_correct(self.client.get('/register')) @@ -846,15 +844,13 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # We should be redirected back to the complete page, setting # the "logged in" cookie for the marketing site. - self.assert_logged_in_cookie_redirect(actions.do_complete( - request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access - redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request - )) + self.assert_logged_in_cookie_redirect(self.do_complete(strategy, request, partial_pipeline_token, partial_data)) # Set the cookie and try again self.set_logged_in_cookies(request) self.assert_redirect_after_pipeline_completes( - actions.do_complete(strategy.request.backend, social_views._do_login, user=created_user, request=request)) + self.do_complete(strategy, request, partial_pipeline_token, partial_data, created_user) + ) # Now the user has been redirected to the dashboard. Their third party account should now be linked. self.assert_social_auth_exists_for_user(created_user, strategy) self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True) @@ -974,6 +970,19 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): """ raise NotImplementedError + def do_complete(self, strategy, request, partial_pipeline_token, partial_data, user=None): + """ + Makes sure that strategy store includes the partial data object before + calling actions.do_complete + """ + strategy.storage.partial.store(partial_data) + if not user: + user = request.user + return actions.do_complete( + request.backend, social_views._do_login, user, None, # pylint: disable=protected-access + redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request, partial_token=partial_pipeline_token + ) + # pylint: disable=abstract-method @django_utils.override_settings(ECOMMERCE_API_URL=TEST_API_URL) diff --git a/common/djangoapps/third_party_auth/tests/specs/test_linkedin.py b/common/djangoapps/third_party_auth/tests/specs/test_linkedin.py index ac4f47d895a95e06a375db16573435e3681536bc..091471657d3cefa4d55d5e5b9b711a000673aa85 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_linkedin.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_linkedin.py @@ -4,6 +4,15 @@ from __future__ import absolute_import from third_party_auth.tests.specs import base +def get_localized_name(name): + """Returns the localizedName from the name object""" + locale = "{}_{}".format( + name["preferredLocale"]["language"], + name["preferredLocale"]["country"] + ) + return name['localized'].get(locale, '') + + class LinkedInOauth2IntegrationTest(base.Oauth2IntegrationTest): """Integration tests for provider.LinkedInOauth2.""" @@ -21,11 +30,29 @@ class LinkedInOauth2IntegrationTest(base.Oauth2IntegrationTest): 'expires_in': 'expires_in_value', } USER_RESPONSE_DATA = { - 'lastName': 'lastName_value', + 'lastName': { + "localized": { + "en_US": "Doe" + }, + "preferredLocale": { + "country": "US", + "language": "en" + } + }, 'id': 'id_value', - 'firstName': 'firstName_value', + 'firstName': { + "localized": { + "en_US": "Doe" + }, + "preferredLocale": { + "country": "US", + "language": "en" + } + }, } def get_username(self): response_data = self.get_response_data() - return response_data.get('firstName') + response_data.get('lastName') + first_name = get_localized_name(response_data.get('firstName')) + last_name = get_localized_name(response_data.get('lastName')) + return first_name + last_name diff --git a/common/djangoapps/third_party_auth/tests/utils.py b/common/djangoapps/third_party_auth/tests/utils.py index 8fe9ee136a44d91a679413665b2f99187ba089bc..47b8bb51736bcb1d5c97e2b03a4484ff8fa94e60 100644 --- a/common/djangoapps/third_party_auth/tests/utils.py +++ b/common/djangoapps/third_party_auth/tests/utils.py @@ -98,7 +98,7 @@ class ThirdPartyOAuthTestMixinFacebook(object): class ThirdPartyOAuthTestMixinGoogle(object): """Tests oauth with the Google backend""" BACKEND = "google-oauth2" - USER_URL = "https://www.googleapis.com/plus/v1/people/me" + USER_URL = "https://www.googleapis.com/oauth2/v3/userinfo" # In google-oauth2 responses, the "email" field is used as the user's identifier UID_FIELD = "email" diff --git a/requirements/edx/base.in b/requirements/edx/base.in index fb89cec707b26a69bed2b63aca741a77c696e0bd..781ae4a5e4ba68e742275d8de1c7c0dd83d21843 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -133,8 +133,8 @@ pyuca==1.1 # For more accurate sorting of translated co recommender-xblock # https://github.com/edx/RecommenderXBlock rest-condition # DRF's recommendation for supporting complex permissions rfc6266-parser # Used to generate Content-Disposition headers. -social-auth-app-django<3.0.0 -social-auth-core<2.0.0 +social-auth-app-django==3.1.0 +social-auth-core==3.2.0 pysrt # Support for SubRip subtitle files, used in the video XModule pytz # Time zone information database PyYAML # Used to parse XModule resource templates diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index a2419357d729ea5d60c1f6e94764cbc4c128f733..4ab31d76b230c860a62d4b6c2543b6977961b57b 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -227,8 +227,8 @@ simplejson==3.17.0 singledispatch==3.4.0.3 six==1.13.0 slumber==0.7.1 # via edx-bulk-grades, edx-enterprise, edx-rest-api-client -social-auth-app-django==2.1.0 -social-auth-core==1.7.0 +social-auth-app-django==3.1.0 +social-auth-core==3.2.0 sorl-thumbnail==12.3 sortedcontainers==2.1.0 soupsieve==1.9.5 # via beautifulsoup4 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 151a45b3670984f408f697665471a73142275557..9007b65d3f0968bbaef7ab20a827c5bcd53764e6 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -305,8 +305,8 @@ singledispatch==3.4.0.3 six==1.13.0 slumber==0.7.1 snowballstemmer==2.0.0 # via sphinx -social-auth-app-django==2.1.0 -social-auth-core==1.7.0 +social-auth-app-django==3.1.0 +social-auth-core==3.2.0 sorl-thumbnail==12.3 sortedcontainers==2.1.0 soupsieve==1.9.5 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index d9dea4233ae6bfff93233154f1eb275adb7d8022..70e3d991ebd3b8f11ea1561ad923839b4cefedbc 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -292,8 +292,8 @@ simplejson==3.17.0 singledispatch==3.4.0.3 six==1.13.0 slumber==0.7.1 -social-auth-app-django==2.1.0 -social-auth-core==1.7.0 +social-auth-app-django==3.1.0 +social-auth-core==3.2.0 sorl-thumbnail==12.3 sortedcontainers==2.1.0 soupsieve==1.9.5