diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py index 418f0b7167..21a47f92db 100644 --- a/common/djangoapps/third_party_auth/pipeline.py +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -151,19 +151,6 @@ class AuthEntryError(AuthException): """ -class NotActivatedException(AuthException): - """ Raised when a user tries to login to an unverified account """ - def __init__(self, backend, email): - self.email = email - super(NotActivatedException, self).__init__(backend, email) - - def __str__(self): - return ( - _('This account has not yet been activated. An activation email has been re-sent to {email_address}.') - .format(email_address=self.email) - ) - - class ProviderUserState(object): """Object representing the provider state (attached or not) for a user. @@ -514,26 +501,27 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia # This parameter is used by the auth_exchange app, which always allows users to # login, whether or not their account is validated. pass - # IF the user has just registered a new account as part of this pipeline, that is fine - # and we allow the login to continue this once, because if we pause again to force the - # user to activate their account via email, the pipeline may get lost (e.g. email takes - # too long to arrive, user opens the activation email on a different device, etc.). - # This is consistent with first party auth and ensures that the pipeline completes - # fully, which is critical. - # But if this is an existing account, we refuse to allow them to login again until they - # check their email and activate the account. - elif social is not None: - # This third party account is already linked to a user account. That means that the - # user's account existed before this pipeline originally began (since the creation - # of the 'social' link entry occurs in one of the following pipeline steps). - # Reject this login attempt and tell the user to validate their account first. - - # Send them another activation email: - student.views.reactivation_email_for_user(user) - - raise NotActivatedException(backend, user.email) - # else: The user must have just successfully registered their account, so we proceed. - # We know they did not just login, because the login process rejects unverified users. + elif social is None: + # The user has just registered a new account as part of this pipeline. Their account + # is inactive but we allow the login to continue, because if we pause again to force + # the user to activate their account via email, the pipeline may get lost (e.g. + # email takes too long to arrive, user opens the activation email on a different + # device, etc.). This is consistent with first party auth and ensures that the + # pipeline completes fully, which is critical. + pass + else: + # This is an existing account, linked to a third party provider but not activated. + # Double-check these criteria: + assert user is not None + assert social is not None + # We now also allow them to login again, because if they had entered their email + # incorrectly then there would be no way for them to recover the account, nor + # register anew via SSO. See SOL-1324 in JIRA. + # However, we will log a warning for this case: + logger.warning( + 'User "%s" is using third_party_auth to login but has not yet activated their account. ', + user.username + ) @partial.partial diff --git a/common/djangoapps/third_party_auth/settings.py b/common/djangoapps/third_party_auth/settings.py index a856aefa4f..d6f75a3820 100644 --- a/common/djangoapps/third_party_auth/settings.py +++ b/common/djangoapps/third_party_auth/settings.py @@ -73,11 +73,16 @@ def apply_settings(django_settings): django_settings.SOCIAL_AUTH_RAISE_EXCEPTIONS = False # Allow users to login using social auth even if their account is not verified yet - # The 'ensure_user_information' step controls this and only allows brand new users - # to login without verification. Repeat logins are not permitted until the account - # gets verified. - django_settings.INACTIVE_USER_LOGIN = True - django_settings.INACTIVE_USER_URL = '/auth/inactive' + # This is required since we [ab]use django's 'is_active' flag to indicate verified + # accounts; without this set to True, python-social-auth won't allow us to link the + # user's account to the third party account during registration (since the user is + # not verified at that point). + # We also generally allow unverified third party auth users to login (see the logic + # in ensure_user_information in pipeline.py) because otherwise users who use social + # auth to register with an invalid email address can become "stuck". + # TODO: Remove the following if/when email validation is separated from the is_active flag. + django_settings.SOCIAL_AUTH_INACTIVE_USER_LOGIN = True + django_settings.SOCIAL_AUTH_INACTIVE_USER_URL = '/auth/inactive' # Context processors required under Django. django_settings.SOCIAL_AUTH_UUID_LENGTH = 4 diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index 3b69b1340c..b510c26106 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -10,6 +10,7 @@ from django.contrib import auth from django.contrib.auth import models as auth_models from django.contrib.messages.storage import fallback from django.contrib.sessions.backends import cache +from django.core.urlresolvers import reverse from django.test import utils as django_utils from django.conf import settings as django_settings from edxmako.tests import mako_middleware_process_request @@ -18,6 +19,7 @@ from social.apps.django_app import utils as social_utils from social.apps.django_app import views as social_views from student import models as student_models from student import views as student_views +from student.tests.factories import UserFactory from student_account.views import account_settings_context from third_party_auth import middleware, pipeline @@ -25,6 +27,176 @@ from third_party_auth import settings as auth_settings from third_party_auth.tests import testutil +class IntegrationTestMixin(object): + """ + Mixin base class for third_party_auth integration tests. + This class is newer and simpler than the 'IntegrationTest' alternative below, but it is + currently less comprehensive. Some providers are tested with this, others with + IntegrationTest. + """ + # Provider information: + PROVIDER_NAME = "override" + PROVIDER_BACKEND = "override" + PROVIDER_ID = "override" + # Information about the user expected from the provider: + USER_EMAIL = "override" + USER_NAME = "override" + USER_USERNAME = "override" + + def setUp(self): + super(IntegrationTestMixin, self).setUp() + self.login_page_url = reverse('signin_user') + self.register_page_url = reverse('register_user') + patcher = testutil.patch_mako_templates() + patcher.start() + self.addCleanup(patcher.stop) + # Override this method in a subclass and enable at least one provider. + + def test_register(self): + # The user goes to the register page, and sees a button to register with the provider: + provider_register_url = self._check_register_page() + # The user clicks on the Dummy button: + try_login_response = self.client.get(provider_register_url) + # The user should be redirected to the provider's login page: + self.assertEqual(try_login_response.status_code, 302) + provider_response = self.do_provider_login(try_login_response['Location']) + # We should be redirected to the register screen since this account is not linked to an edX account: + self.assertEqual(provider_response.status_code, 302) + self.assertEqual(provider_response['Location'], self.url_prefix + self.register_page_url) + register_response = self.client.get(self.register_page_url) + tpa_context = register_response.context["data"]["third_party_auth"] + self.assertEqual(tpa_context["errorMessage"], None) + # Check that the "You've successfully signed into [PROVIDER_NAME]" message is shown. + self.assertEqual(tpa_context["currentProvider"], self.PROVIDER_NAME) + # Check that the data (e.g. email) from the provider is displayed in the form: + form_data = register_response.context['data']['registration_form_desc'] + form_fields = {field['name']: field for field in form_data['fields']} + self.assertEqual(form_fields['email']['defaultValue'], self.USER_EMAIL) + self.assertEqual(form_fields['name']['defaultValue'], self.USER_NAME) + self.assertEqual(form_fields['username']['defaultValue'], self.USER_USERNAME) + # Now complete the form: + ajax_register_response = self.client.post( + reverse('user_api_registration'), + { + 'email': 'email-edited@tpa-test.none', + 'name': 'My Customized Name', + 'username': 'new_username', + 'honor_code': True, + } + ) + self.assertEqual(ajax_register_response.status_code, 200) + # Then the AJAX will finish the third party auth: + continue_response = self.client.get(tpa_context["finishAuthUrl"]) + # And we should be redirected to the dashboard: + self.assertEqual(continue_response.status_code, 302) + self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) + + # Now check that we can login again, whether or not we have yet verified the account: + self.client.logout() + self._test_return_login(user_is_activated=False) + + self.client.logout() + self.verify_user_email('email-edited@tpa-test.none') + self._test_return_login(user_is_activated=True) + + def test_login(self): + user = UserFactory.create() + # The user goes to the login page, and sees a button to login with this provider: + provider_login_url = self._check_login_page() + # The user clicks on the provider's button: + try_login_response = self.client.get(provider_login_url) + # The user should be redirected to the provider's login page: + self.assertEqual(try_login_response.status_code, 302) + complete_response = self.do_provider_login(try_login_response['Location']) + # We should be redirected to the login screen since this account is not linked to an edX account: + self.assertEqual(complete_response.status_code, 302) + self.assertEqual(complete_response['Location'], self.url_prefix + self.login_page_url) + login_response = self.client.get(self.login_page_url) + tpa_context = login_response.context["data"]["third_party_auth"] + self.assertEqual(tpa_context["errorMessage"], None) + # Check that the "You've successfully signed into [PROVIDER_NAME]" message is shown. + self.assertEqual(tpa_context["currentProvider"], self.PROVIDER_NAME) + # Now the user enters their username and password. + # The AJAX on the page will log them in: + ajax_login_response = self.client.post( + reverse('user_api_login_session'), + {'email': user.email, 'password': 'test'} + ) + self.assertEqual(ajax_login_response.status_code, 200) + # Then the AJAX will finish the third party auth: + continue_response = self.client.get(tpa_context["finishAuthUrl"]) + # And we should be redirected to the dashboard: + self.assertEqual(continue_response.status_code, 302) + self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) + + # Now check that we can login again: + self.client.logout() + self._test_return_login() + + def do_provider_login(self, provider_redirect_url): + """ + mock logging in to the provider + Should end with loading self.complete_url, which should be returned + """ + raise NotImplementedError + + def _test_return_login(self, user_is_activated=True): + """ Test logging in to an account that is already linked. """ + # Make sure we're not logged in: + dashboard_response = self.client.get(reverse('dashboard')) + self.assertEqual(dashboard_response.status_code, 302) + # The user goes to the login page, and sees a button to login with this provider: + provider_login_url = self._check_login_page() + # The user clicks on the provider's login button: + try_login_response = self.client.get(provider_login_url) + # The user should be redirected to the provider: + self.assertEqual(try_login_response.status_code, 302) + login_response = self.do_provider_login(try_login_response['Location']) + # There will be one weird redirect required to set the login cookie: + self.assertEqual(login_response.status_code, 302) + self.assertEqual(login_response['Location'], self.url_prefix + self.complete_url) + # And then we should be redirected to the dashboard: + login_response = self.client.get(login_response['Location']) + self.assertEqual(login_response.status_code, 302) + if user_is_activated: + url_expected = reverse('dashboard') + else: + url_expected = reverse('third_party_inactive_redirect') + '?next=' + reverse('dashboard') + self.assertEqual(login_response['Location'], self.url_prefix + url_expected) + # Now we are logged in: + dashboard_response = self.client.get(reverse('dashboard')) + self.assertEqual(dashboard_response.status_code, 200) + + def _check_login_page(self): + """ + Load the login form and check that it contains a button for the provider. + Return the URL for logging into that provider. + """ + return self._check_login_or_register_page(self.login_page_url, "loginUrl") + + def _check_register_page(self): + """ + Load the registration form and check that it contains a button for the provider. + Return the URL for registering with that provider. + """ + return self._check_login_or_register_page(self.register_page_url, "registerUrl") + + def _check_login_or_register_page(self, url, url_to_return): + """ Shared logic for _check_login_page() and _check_register_page() """ + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn(self.PROVIDER_NAME, response.content) + context_data = response.context['data']['third_party_auth'] + provider_urls = {provider['id']: provider[url_to_return] for provider in context_data['providers']} + self.assertIn(self.PROVIDER_ID, provider_urls) + return provider_urls[self.PROVIDER_ID] + + @property + def complete_url(self): + """ Get the auth completion URL for this provider """ + return reverse('social:complete', kwargs={'backend': self.PROVIDER_BACKEND}) + + @unittest.skipUnless( testutil.AUTH_FEATURES_KEY in django_settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES') @django_utils.override_settings() # For settings reversion on a method-by-method basis. diff --git a/common/djangoapps/third_party_auth/tests/specs/test_generic.py b/common/djangoapps/third_party_auth/tests/specs/test_generic.py new file mode 100644 index 0000000000..56b7fd8333 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/specs/test_generic.py @@ -0,0 +1,33 @@ +""" +Use the 'Dummy' auth provider for generic integration tests of third_party_auth. +""" +import unittest +from third_party_auth.tests import testutil + +from .base import IntegrationTestMixin + + +@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') +class GenericIntegrationTest(IntegrationTestMixin, testutil.TestCase): + """ + Basic integration tests of third_party_auth using Dummy provider + """ + PROVIDER_ID = "oa2-dummy" + PROVIDER_NAME = "Dummy" + PROVIDER_BACKEND = "dummy" + + USER_EMAIL = "adama@fleet.colonies.gov" + USER_NAME = "William Adama" + USER_USERNAME = "Galactica1" + + def setUp(self): + super(GenericIntegrationTest, self).setUp() + self.configure_dummy_provider(enabled=True) + + def do_provider_login(self, provider_redirect_url): + """ + Mock logging in to the Dummy provider + """ + # For the Dummy provider, the provider redirect URL is self.complete_url + self.assertEqual(provider_redirect_url, self.url_prefix + self.complete_url) + return self.client.get(provider_redirect_url) diff --git a/common/djangoapps/third_party_auth/tests/specs/test_lti.py b/common/djangoapps/third_party_auth/tests/specs/test_lti.py index d6622def7e..9d7469f282 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_lti.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_lti.py @@ -29,6 +29,8 @@ class IntegrationTestLTI(testutil.TestCase): def setUp(self): super(IntegrationTestLTI, self).setUp() + self.client.defaults['SERVER_NAME'] = 'testserver' + self.url_prefix = 'http://testserver' self.configure_lti_provider( name='Other Tool Consumer 1', enabled=True, lti_consumer_key='other1', diff --git a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py index f9c1870577..defea79520 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py @@ -1,38 +1,36 @@ """ Third_party_auth integration tests using a mock version of the TestShib provider """ - -import json import unittest import httpretty from mock import patch -from django.core.urlresolvers import reverse - -from student.tests.factories import UserFactory from third_party_auth.tasks import fetch_saml_metadata from third_party_auth.tests import testutil -from openedx.core.lib.js_utils import escape_json_dumps + +from .base import IntegrationTestMixin TESTSHIB_ENTITY_ID = 'https://idp.testshib.org/idp/shibboleth' TESTSHIB_METADATA_URL = 'https://mock.testshib.org/metadata/testshib-providers.xml' TESTSHIB_SSO_URL = 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO' -TPA_TESTSHIB_LOGIN_URL = '/auth/login/tpa-saml/?auth_entry=login&next=%2Fdashboard&idp=testshib' -TPA_TESTSHIB_REGISTER_URL = '/auth/login/tpa-saml/?auth_entry=register&next=%2Fdashboard&idp=testshib' -TPA_TESTSHIB_COMPLETE_URL = '/auth/complete/tpa-saml/' - @unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') -class TestShibIntegrationTest(testutil.SAMLTestCase): +class TestShibIntegrationTest(IntegrationTestMixin, testutil.SAMLTestCase): """ TestShib provider Integration Test, to test SAML functionality """ + PROVIDER_ID = "saml-testshib" + PROVIDER_NAME = "TestShib" + PROVIDER_BACKEND = "tpa-saml" + + USER_EMAIL = "myself@testshib.org" + USER_NAME = "Me Myself And I" + USER_USERNAME = "myself" + def setUp(self): super(TestShibIntegrationTest, self).setUp() - self.login_page_url = reverse('signin_user') - self.register_page_url = reverse('register_user') self.enable_saml( private_key=self._get_private_key(), public_key=self._get_public_key(), @@ -53,13 +51,14 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): uid_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.generate_unique_id', return_value='TESTID') uid_patch.start() self.addCleanup(uid_patch.stop) + self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. def test_login_before_metadata_fetched(self): self._configure_testshib_provider(fetch_metadata=False) # The user goes to the login page, and sees a button to login with TestShib: - self._check_login_page() + testshib_login_url = self._check_login_page() # The user clicks on the TestShib button: - try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) + try_login_response = self.client.get(testshib_login_url) # The user should be redirected to back to the login page: self.assertEqual(try_login_response.status_code, 302) self.assertEqual(try_login_response['Location'], self.url_prefix + self.login_page_url) @@ -68,115 +67,15 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): self.assertEqual(response.status_code, 200) self.assertIn('Authentication with TestShib is currently unavailable.', response.content) - def test_register(self): - self._configure_testshib_provider() - self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. - # The user goes to the register page, and sees a button to register with TestShib: - self._check_register_page() - # The user clicks on the TestShib button: - try_login_response = self.client.get(TPA_TESTSHIB_REGISTER_URL) - # The user should be redirected to TestShib: - self.assertEqual(try_login_response.status_code, 302) - self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) - # Now the user will authenticate with the SAML provider - testshib_response = self._fake_testshib_login_and_return() - # We should be redirected to the register screen since this account is not linked to an edX account: - self.assertEqual(testshib_response.status_code, 302) - self.assertEqual(testshib_response['Location'], self.url_prefix + self.register_page_url) - register_response = self.client.get(self.register_page_url) - # We'd now like to see if the "You've successfully signed into TestShib" message is - # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this - # type of test, so we just check for the variable that triggers that message. - self.assertIn('"currentProvider": "TestShib"', register_response.content) - self.assertIn('"errorMessage": null', register_response.content) - # Now do a crude check that the data (e.g. email) from the provider is displayed in the form: - self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content) - self.assertIn('"defaultValue": "Me Myself And I"', register_response.content) - # Now complete the form: - ajax_register_response = self.client.post( - reverse('user_api_registration'), - { - 'email': 'myself@testshib.org', - 'name': 'Myself', - 'username': 'myself', - 'honor_code': True, - } - ) - self.assertEqual(ajax_register_response.status_code, 200) - # Then the AJAX will finish the third party auth: - continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) - # And we should be redirected to the dashboard: - self.assertEqual(continue_response.status_code, 302) - self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) - - # Now check that we can login again: - self.client.logout() - self.verify_user_email('myself@testshib.org') - self._test_return_login() - def test_login(self): + """ Configure TestShib before running the login test """ self._configure_testshib_provider() - self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. - user = UserFactory.create() - # The user goes to the login page, and sees a button to login with TestShib: - self._check_login_page() - # The user clicks on the TestShib button: - try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) - # The user should be redirected to TestShib: - self.assertEqual(try_login_response.status_code, 302) - self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) - # Now the user will authenticate with the SAML provider - testshib_response = self._fake_testshib_login_and_return() - # We should be redirected to the login screen since this account is not linked to an edX account: - self.assertEqual(testshib_response.status_code, 302) - self.assertEqual(testshib_response['Location'], self.url_prefix + self.login_page_url) - login_response = self.client.get(self.login_page_url) - # We'd now like to see if the "You've successfully signed into TestShib" message is - # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this - # type of test, so we just check for the variable that triggers that message. - self.assertIn('"currentProvider": "TestShib"', login_response.content) - self.assertIn('"errorMessage": null', login_response.content) - # Now the user enters their username and password. - # The AJAX on the page will log them in: - ajax_login_response = self.client.post( - reverse('user_api_login_session'), - {'email': user.email, 'password': 'test'} - ) - self.assertEqual(ajax_login_response.status_code, 200) - # Then the AJAX will finish the third party auth: - continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) - # And we should be redirected to the dashboard: - self.assertEqual(continue_response.status_code, 302) - self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) + super(TestShibIntegrationTest, self).test_login() - # Now check that we can login again: - self.client.logout() - self._test_return_login() - - def _test_return_login(self): - """ Test logging in to an account that is already linked. """ - # Make sure we're not logged in: - dashboard_response = self.client.get(reverse('dashboard')) - self.assertEqual(dashboard_response.status_code, 302) - # The user goes to the login page, and sees a button to login with TestShib: - self._check_login_page() - # The user clicks on the TestShib button: - try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) - # The user should be redirected to TestShib: - self.assertEqual(try_login_response.status_code, 302) - self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) - # Now the user will authenticate with the SAML provider - login_response = self._fake_testshib_login_and_return() - # There will be one weird redirect required to set the login cookie: - self.assertEqual(login_response.status_code, 302) - self.assertEqual(login_response['Location'], self.url_prefix + TPA_TESTSHIB_COMPLETE_URL) - # And then we should be redirected to the dashboard: - login_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) - self.assertEqual(login_response.status_code, 302) - self.assertEqual(login_response['Location'], self.url_prefix + reverse('dashboard')) - # Now we are logged in: - dashboard_response = self.client.get(reverse('dashboard')) - self.assertEqual(dashboard_response.status_code, 200) + def test_register(self): + """ Configure TestShib before running the register test """ + self._configure_testshib_provider() + super(TestShibIntegrationTest, self).test_register() def _freeze_time(self, timestamp): """ Mock the current time for SAML, so we can replay canned requests/responses """ @@ -184,22 +83,6 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): now_patch.start() self.addCleanup(now_patch.stop) - def _check_login_page(self): - """ Load the login form and check that it contains a TestShib button """ - response = self.client.get(self.login_page_url) - self.assertEqual(response.status_code, 200) - self.assertIn("TestShib", response.content) - self.assertIn(escape_json_dumps(TPA_TESTSHIB_LOGIN_URL), response.content) - return response - - def _check_register_page(self): - """ Load the login form and check that it contains a TestShib button """ - response = self.client.get(self.register_page_url) - self.assertEqual(response.status_code, 200) - self.assertIn("TestShib", response.content) - self.assertIn(escape_json_dumps(TPA_TESTSHIB_REGISTER_URL), response.content) - return response - def _configure_testshib_provider(self, **kwargs): """ Enable and configure the TestShib SAML IdP as a third_party_auth provider """ fetch_metadata = kwargs.pop('fetch_metadata', True) @@ -219,11 +102,12 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): self.assertEqual(num_changed, 1) self.assertEqual(num_total, 1) - def _fake_testshib_login_and_return(self): + def do_provider_login(self, provider_redirect_url): """ Mocked: the user logs in to TestShib and then gets redirected back """ # The SAML provider (TestShib) will authenticate the user, then get the browser to POST a response: + self.assertTrue(provider_redirect_url.startswith(TESTSHIB_SSO_URL)) return self.client.post( - TPA_TESTSHIB_COMPLETE_URL, + self.complete_url, content_type='application/x-www-form-urlencoded', data=self.read_data_file('testshib_response.txt'), ) diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py index 9f9127171b..36ac1c698c 100644 --- a/common/djangoapps/third_party_auth/tests/testutil.py +++ b/common/djangoapps/third_party_auth/tests/testutil.py @@ -10,6 +10,7 @@ from django.contrib.auth.models import User from provider.oauth2.models import Client as OAuth2Client from provider import constants import django.test +from mako.template import Template import mock import os.path @@ -27,6 +28,18 @@ AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH' AUTH_FEATURE_ENABLED = AUTH_FEATURES_KEY in settings.FEATURES +def patch_mako_templates(): + """ Patch mako so the django test client can access template context """ + orig_render = Template.render_unicode + + def wrapped_render(*args, **kwargs): + """ Render the template and send the context info to any listeners that want it """ + django.test.signals.template_rendered.send(sender=None, template=None, context=kwargs) + return orig_render(*args, **kwargs) + + return mock.patch.multiple(Template, render_unicode=wrapped_render, render=wrapped_render) + + class FakeDjangoSettings(object): """A fake for Django settings.""" @@ -109,6 +122,13 @@ class ThirdPartyAuthTestMixin(object): kwargs.setdefault("secret", "test") return cls.configure_oauth_provider(**kwargs) + @classmethod + def configure_dummy_provider(cls, **kwargs): + """ Update the settings for the Twitter third party auth provider/backend """ + kwargs.setdefault("name", "Dummy") + kwargs.setdefault("backend_name", "dummy") + return cls.configure_oauth_provider(**kwargs) + @classmethod def verify_user_email(cls, email): """ Mark the user with the given email as verified """ @@ -135,19 +155,18 @@ class ThirdPartyAuthTestMixin(object): class TestCase(ThirdPartyAuthTestMixin, django.test.TestCase): """Base class for auth test cases.""" - pass + def setUp(self): + super(TestCase, self).setUp() + # Explicitly set a server name that is compatible with all our providers: + # (The SAML lib we use doesn't like the default 'testserver' as a domain) + self.client.defaults['SERVER_NAME'] = 'example.none' + self.url_prefix = 'http://example.none' class SAMLTestCase(TestCase): """ Base class for SAML-related third_party_auth tests """ - - def setUp(self): - super(SAMLTestCase, self).setUp() - self.client.defaults['SERVER_NAME'] = 'example.none' # The SAML lib we use doesn't like testserver' as a domain - self.url_prefix = 'http://example.none' - @classmethod def _get_public_key(cls, key_name='saml_key'): """ Get a public key for use in the test. """ diff --git a/common/djangoapps/third_party_auth/urls.py b/common/djangoapps/third_party_auth/urls.py index 69c600932b..152ffe4777 100644 --- a/common/djangoapps/third_party_auth/urls.py +++ b/common/djangoapps/third_party_auth/urls.py @@ -6,7 +6,7 @@ from .views import inactive_user_view, saml_metadata_view, lti_login_and_complet urlpatterns = patterns( '', - url(r'^auth/inactive', inactive_user_view), + url(r'^auth/inactive', inactive_user_view, name="third_party_inactive_redirect"), url(r'^auth/saml/metadata.xml', saml_metadata_view), url(r'^auth/login/(?Plti)/$', lti_login_and_complete_view), url(r'^auth/', include('social.apps.django_app.urls', namespace='social')), diff --git a/common/djangoapps/third_party_auth/views.py b/common/djangoapps/third_party_auth/views.py index 58fd17c784..daa104383b 100644 --- a/common/djangoapps/third_party_auth/views.py +++ b/common/djangoapps/third_party_auth/views.py @@ -17,8 +17,13 @@ URL_NAMESPACE = getattr(settings, setting_name('URL_NAMESPACE'), None) or 'socia def inactive_user_view(request): """ - A newly registered user has completed the social auth pipeline. - Their account is not yet activated, but we let them login this once. + A newly or recently registered user has completed the social auth pipeline. + Their account is not yet activated, but we let them login since the third party auth + provider is trusted to vouch for them. See details in pipeline.py. + + The reason this view exists is that if we don't define this as the + SOCIAL_AUTH_INACTIVE_USER_URL, inactive users will get sent to LOGIN_ERROR_URL, which we + don't want. """ # 'next' may be set to '/account/finish_auth/.../' if this user needs to be auto-enrolled # in a course. Otherwise, just redirect them to the dashboard, which displays a message