Merge pull request #22513 from edx/robrap/ARCH-1253-login-user-post-only-take-3
ARCH-1253: require POST for login_user
This commit is contained in:
@@ -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/<provider>
|
||||
# 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user