diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index c2af79b603..610eb185c7 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -248,7 +248,7 @@ class HelperMixin(object): return defaults def get_request_and_strategy(self, auth_entry=None, redirect_uri=None): - """Gets a fully-configured request and strategy. + """Gets a fully-configured GET request and strategy. These two objects contain circular references, so we create them together. The references themselves are a mixture of normal __init__ @@ -272,6 +272,21 @@ class HelperMixin(object): return request, strategy + def _get_login_post_request(self, strategy): + """Gets a fully-configured login POST request given a strategy and pipeline.""" + request = self.request_factory.post(reverse('login_api')) + + # Note: The shared GET request can't be used for login, which is now POST-only, + # so this POST request is given a copy of all configuration from the GET request + # with the active third-party auth pipeline and strategy. + request.site = strategy.request.site + request.social_strategy = strategy + request.user = strategy.request.user + request.session = strategy.request.session + request.backend = strategy.request.backend + + return request + @contextmanager def _patch_edxmako_current_request(self, request): """Make ``request`` be the current request for edxmako template rendering.""" @@ -534,12 +549,12 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): @mock.patch('third_party_auth.pipeline.segment.track') def test_full_pipeline_succeeds_for_linking_account(self, _mock_segment_track): - # First, create, the request and strategy that store pipeline state, + # First, create, the GET request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. - request, strategy = self.get_request_and_strategy( + get_request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') - request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) - request.user = self.create_user_models_for_existing_account( + get_request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) + get_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) @@ -548,75 +563,82 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # expected state. self.client.get( pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) - actions.do_complete(request.backend, social_views._do_login, # pylint: disable=protected-access - request=request) + actions.do_complete(get_request.backend, social_views._do_login, # pylint: disable=protected-access + request=get_request) - login_user(strategy.request) - actions.do_complete(request.backend, social_views._do_login, # pylint: disable=protected-access - request=request) + post_request = self._get_login_post_request(strategy) + login_user(post_request) + actions.do_complete(post_request.backend, social_views._do_login, # pylint: disable=protected-access, no-member + request=post_request) # First we expect that we're in the unlinked state, and that there # really is no association in the backend. - self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=False) - self.assert_social_auth_does_not_exist_for_user(request.user, strategy) + self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=False) + self.assert_social_auth_does_not_exist_for_user(get_request.user, strategy) # We should be redirected back to the complete page, setting # the "logged in" cookie for the marketing site. - self.assert_logged_in_cookie_redirect(self.do_complete(strategy, request, partial_pipeline_token, partial_data)) + self.assert_logged_in_cookie_redirect( + self.do_complete(strategy, get_request, partial_pipeline_token, partial_data) + ) # Set the cookie and try again - self.set_logged_in_cookies(request) + self.set_logged_in_cookies(get_request) # Fire off the auth pipeline to link. self.assert_redirect_after_pipeline_completes( - self.do_complete(strategy, request, partial_pipeline_token, partial_data) + self.do_complete(strategy, get_request, partial_pipeline_token, partial_data) ) # Now we expect to be in the linked state, with a backend entry. - self.assert_social_auth_exists_for_user(request.user, strategy) - self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True) + self.assert_social_auth_exists_for_user(get_request.user, strategy) + self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=True) def test_full_pipeline_succeeds_for_unlinking_account(self): - # First, create, the request and strategy that store pipeline state, + # First, create, the GET request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. - request, strategy = self.get_request_and_strategy( + get_request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') - request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) + get_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()) self.assert_social_auth_exists_for_user(user, strategy) # We're already logged in, so simulate that the cookie is set correctly - self.set_logged_in_cookies(request) + self.set_logged_in_cookies(get_request) # Instrument the pipeline to get to the dashboard with the full # expected state. self.client.get( pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) - actions.do_complete(request.backend, social_views._do_login, # pylint: disable=protected-access - request=request) + actions.do_complete(get_request.backend, social_views._do_login, # pylint: disable=protected-access + request=get_request) - with self._patch_edxmako_current_request(strategy.request): - login_user(strategy.request) - actions.do_complete(request.backend, social_views._do_login, user=user, # pylint: disable=protected-access - request=request) + post_request = self._get_login_post_request(strategy) + with self._patch_edxmako_current_request(post_request): + login_user(post_request) + actions.do_complete(post_request.backend, social_views._do_login, user=user, # pylint: disable=protected-access, no-member + request=post_request) + + # Copy the user that was set on the post_request object back to the original get_request object. + get_request.user = post_request.user # First we expect that we're in the linked state, with a backend entry. - self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True) - self.assert_social_auth_exists_for_user(request.user, strategy) + self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=True) + self.assert_social_auth_exists_for_user(get_request.user, strategy) # Fire off the disconnect pipeline to unlink. self.assert_redirect_after_pipeline_completes( actions.do_disconnect( - request.backend, - request.user, + get_request.backend, + get_request.user, None, redirect_field_name=auth.REDIRECT_FIELD_NAME ) ) # Now we expect to be in the unlinked state, with no backend entry. - self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=False) + self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=False) self.assert_social_auth_does_not_exist_for_user(user, strategy) def test_linking_already_associated_account_raises_auth_already_associated(self): @@ -650,7 +672,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # unlinked, but getting that behavior is cumbersome here and already # covered in other tests. Using linked=True does, however, let us test # that the duplicate error has no effect on the state of the controls. - request, strategy = self.get_request_and_strategy( + get_request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( @@ -659,28 +681,29 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): self.client.get('/login') self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) - actions.do_complete(request.backend, social_views._do_login, # pylint: disable=protected-access - request=request) + actions.do_complete(get_request.backend, social_views._do_login, # pylint: disable=protected-access + request=get_request) - with self._patch_edxmako_current_request(strategy.request): - login_user(strategy.request) - actions.do_complete(request.backend, social_views._do_login, # pylint: disable=protected-access - user=user, request=request) + post_request = self._get_login_post_request(strategy) + with self._patch_edxmako_current_request(post_request): + login_user(post_request) + actions.do_complete(post_request.backend, social_views._do_login, # pylint: disable=protected-access, no-member + user=user, request=post_request) # Monkey-patch storage for messaging; pylint: disable=protected-access - request._messages = fallback.FallbackStorage(request) + post_request._messages = fallback.FallbackStorage(post_request) middleware.ExceptionMiddleware().process_exception( - request, + post_request, exceptions.AuthAlreadyAssociated(self.provider.backend_name, 'account is already in use.')) self.assert_account_settings_context_looks_correct( - account_settings_context(request), duplicate=True, linked=True) + account_settings_context(post_request), duplicate=True, linked=True) @mock.patch('third_party_auth.pipeline.segment.track') def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self, _mock_segment_track): - # First, create, the request and strategy that store pipeline state, + # First, create, the GET request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. - request, strategy = self.get_request_and_strategy( + get_request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( @@ -704,33 +727,37 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # Next, the provider makes a request against /auth/complete/ # to resume the pipeline. # pylint: disable=protected-access - self.assert_redirect_to_login_looks_correct(actions.do_complete(request.backend, social_views._do_login, - request=request)) + self.assert_redirect_to_login_looks_correct(actions.do_complete(get_request.backend, social_views._do_login, + request=get_request)) # At this point we know the pipeline has resumed correctly. Next we # fire off the view that displays the login form and posts it via JS. with self._patch_edxmako_current_request(strategy.request): - self.assert_login_response_in_pipeline_looks_correct(login_user(strategy.request)) + self.assert_login_response_in_pipeline_looks_correct(login_and_registration_form(strategy.request)) # Next, we invoke the view that handles the POST, and expect it # redirects to /auth/complete. In the browser ajax handlers will # redirect the user to the dashboard; we invoke it manually here. - self.assert_json_success_response_looks_correct(login_user(strategy.request), verify_redirect_url=True) + post_request = self._get_login_post_request(strategy) + self.assert_json_success_response_looks_correct(login_user(post_request), verify_redirect_url=True) # 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 + post_request.backend, social_views._do_login, post_request.user, None, # pylint: disable=protected-access, no-member + redirect_field_name=auth.REDIRECT_FIELD_NAME, request=post_request )) # Set the cookie and try again - self.set_logged_in_cookies(request) + self.set_logged_in_cookies(get_request) + + # Copy the user that was set on the post_request object back to the original get_request object. + get_request.user = post_request.user self.assert_redirect_after_pipeline_completes( - self.do_complete(strategy, request, partial_pipeline_token, partial_data, user) + self.do_complete(strategy, get_request, partial_pipeline_token, partial_data, user) ) - self.assert_account_settings_context_looks_correct(account_settings_context(request)) + self.assert_account_settings_context_looks_correct(account_settings_context(get_request)) def test_signin_fails_if_account_not_active(self): _, strategy = self.get_request_and_strategy( @@ -742,8 +769,9 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): user.is_active = False user.save() - with self._patch_edxmako_current_request(strategy.request): - self.assert_json_failure_response_is_inactive_account(login_user(strategy.request)) + post_request = self._get_login_post_request(strategy) + with self._patch_edxmako_current_request(post_request): + self.assert_json_failure_response_is_inactive_account(login_user(post_request)) def test_signin_fails_if_no_account_associated(self): _, strategy = self.get_request_and_strategy( @@ -752,7 +780,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): self.create_user_models_for_existing_account( strategy, 'user@example.com', 'password', self.get_username(), skip_social_auth=True) - self.assert_json_failure_response_is_missing_social_auth(login_user(strategy.request)) + post_request = self._get_login_post_request(strategy) + self.assert_json_failure_response_is_missing_social_auth(login_user(post_request)) def test_first_party_auth_trumps_third_party_auth_but_is_invalid_when_only_email_in_request(self): self.assert_first_party_auth_trumps_third_party_auth(email='user@example.com') @@ -924,15 +953,16 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): self.create_user_models_for_existing_account( strategy, email, password, self.get_username(), skip_social_auth=True) - strategy.request.POST = dict(strategy.request.POST) + post_request = self._get_login_post_request(strategy) + post_request.POST = dict(post_request.POST) if email: - strategy.request.POST['email'] = email + post_request.POST['email'] = email if password: - strategy.request.POST['password'] = 'bad_' + password if success is False else password + post_request.POST['password'] = 'bad_' + password if success is False else password - self.assert_pipeline_running(strategy.request) - payload = json.loads(login_user(strategy.request).content.decode('utf-8')) + self.assert_pipeline_running(post_request) + payload = json.loads(login_user(post_request).content.decode('utf-8')) if success is None: # Request malformed -- just one of email/password given. diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py index 32a96e6c5c..d83c77d943 100644 --- a/openedx/core/djangoapps/user_authn/views/login.py +++ b/openedx/core/djangoapps/user_authn/views/login.py @@ -333,6 +333,7 @@ def finish_auth(request): # pylint: disable=unused-argument @ensure_csrf_cookie +@require_http_methods(['POST']) def login_user(request): """ AJAX request to log in the user.