def test_login_url_raises_value_error_if_provider_not_enabled(self): provider_id = 'oa2-not-enabled' self.assertIsNone(provider.Registry.get(provider_id)) with self.assertRaises(ValueError): pipeline.get_login_url(provider_id, pipeline.AUTH_ENTRY_LOGIN)
def _third_party_auth_context(request, redirect_to): """Context for third party auth providers and the currently running pipeline. Arguments: request (HttpRequest): The request, used to determine if a pipeline is currently running. redirect_to: The URL to send the user to following successful authentication. Returns: dict """ context = { "currentProvider": None, "providers": [], "secondaryProviders": [], "finishAuthUrl": None, "errorMessage": None, } if third_party_auth.is_enabled(): for enabled in third_party_auth.provider.Registry.accepting_logins(): info = { "id": enabled.provider_id, "name": enabled.name, "iconClass": enabled.icon_class, "loginUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } context["providers" if not enabled.secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) if current_provider is not None: context["currentProvider"] = current_provider.name context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name) if current_provider.skip_registration_form: # As a reliable way of "skipping" the registration form, we just submit it automatically context["autoSubmitRegForm"] = True # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": # msg may or may not be translated. Try translating [again] in case we are able to: context['errorMessage'] = _(unicode(msg)) # pylint: disable=translation-of-non-string break return context
def _third_party_auth_context(request, redirect_to): """Context for third party auth providers and the currently running pipeline. Arguments: request (HttpRequest): The request, used to determine if a pipeline is currently running. redirect_to: The URL to send the user to following successful authentication. Returns: dict """ context = { "currentProvider": None, "providers": [], "finishAuthUrl": None, "errorMessage": None, } if third_party_auth.is_enabled(): context["providers"] = [ { "name": enabled.NAME, "iconClass": enabled.ICON_CLASS, "loginUrl": pipeline.get_login_url( enabled.NAME, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( enabled.NAME, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } for enabled in third_party_auth.provider.Registry.enabled() ] running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_by_backend_name( running_pipeline.get('backend') ) context["currentProvider"] = current_provider.NAME context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.BACKEND_CLASS.name) # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": context['errorMessage'] = unicode(msg) break return context
def test_for_value_error_if_provider_id_invalid(self): provider_id = 'invalid' # Format is normally "{prefix}-{identifier}" with self.assertRaises(ValueError): provider.Registry.get(provider_id) with self.assertRaises(ValueError): pipeline.get_login_url(provider_id, pipeline.AUTH_ENTRY_LOGIN) with self.assertRaises(ValueError): pipeline.get_disconnect_url(provider_id, 1000) with self.assertRaises(ValueError): pipeline.get_complete_url(provider_id)
def test_already_associated_exception_populates_dashboard_with_error(self): # Instrument the pipeline with an exception. We test that the # exception is raised correctly separately, so it's ok that we're # raising it artificially here. This makes the linked=True artificial # in the final assert because in practice the account would be # 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( 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( strategy, '*****@*****.**', 'password', self.get_username()) self.assert_social_auth_exists_for_user(user, strategy) 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) with self._patch_edxmako_current_request(strategy.request): signin_user(strategy.request) login_user(strategy.request) actions.do_complete(request.backend, social_views._do_login, # pylint: disable=protected-access user=user, request=request) # Monkey-patch storage for messaging; pylint: disable=protected-access request._messages = fallback.FallbackStorage(request) middleware.ExceptionMiddleware().process_exception( 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)
def test_full_pipeline_succeeds_for_unlinking_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. 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)) user = self.create_user_models_for_existing_account( strategy, '*****@*****.**', '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) # 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 with self._patch_edxmako_current_request(strategy.request): student_views.signin_user(strategy.request) student_views.login_user(strategy.request) actions.do_complete(request.backend, social_views._do_login, user=user) # pylint: disable=protected-access # 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), user, linked=True) self.assert_social_auth_exists_for_user(request.user, strategy) # Fire off the disconnect pipeline to unlink. self.assert_redirect_to_dashboard_looks_correct(actions.do_disconnect( request.backend, 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), user, linked=False) self.assert_social_auth_does_not_exist_for_user(user, strategy)
def test_full_pipeline_succeeds_for_unlinking_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') strategy.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( strategy, '*****@*****.**', 'password', self.get_username()) self.assert_social_auth_exists_for_user(user, strategy) # Instrument the pipeline to get to the dashboard with the full # expected state. self.client.get( pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN)) actions.do_complete(strategy, social_views._do_login) # pylint: disable-msg=protected-access mako_middleware_process_request(strategy.request) student_views.signin_user(strategy.request) student_views.login_user(strategy.request) actions.do_complete(strategy, social_views._do_login, user=user) # pylint: disable-msg=protected-access # First we expect that we're in the linked state, with a backend entry. self.assert_dashboard_response_looks_correct(student_views.dashboard(request), user, linked=True) self.assert_social_auth_exists_for_user(request.user, strategy) # Fire off the disconnect pipeline to unlink. self.assert_redirect_to_dashboard_looks_correct(actions.do_disconnect( request.social_strategy, 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_dashboard_response_looks_correct(student_views.dashboard(request), user, linked=False) self.assert_social_auth_does_not_exist_for_user(user, strategy)
def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None): """Retrieve URLs for each enabled third-party auth provider. These URLs are used on the "sign up" and "sign in" buttons on the login/registration forms to allow users to begin authentication with a third-party provider. Optionally, we can redirect the user to an arbitrary url after auth completes successfully. We use this to redirect the user to a page that required login, or to send users to the payment flow when enrolling in a course. Args: auth_entry (string): Either `pipeline.AUTH_ENTRY_LOGIN` or `pipeline.AUTH_ENTRY_REGISTER` Keyword Args: redirect_url (unicode): If provided, send users to this URL after they successfully authenticate. course_id (unicode): The ID of the course the user is enrolling in. We use this to send users to the track selection page if the course has a payment option. Note that `redirect_url` takes precedence over the redirect to the track selection page. Returns: dict mapping provider names to URLs """ if not third_party_auth_enabled(): return {} if redirect_url is not None: pipeline_redirect = redirect_url elif course_id is not None: # If the course is white-label (paid), then we send users # to the shopping cart. (There is a third party auth pipeline # step that will add the course to the cart.) if CourseMode.is_white_label(CourseKey.from_string(course_id)): pipeline_redirect = reverse("shoppingcart.views.show_cart") # Otherwise, send the user to the track selection page. # The track selection page may redirect the user to the dashboard # (if the only available mode is honor), or directly to verification # (for professional ed). else: pipeline_redirect = reverse( "course_modes_choose", kwargs={'course_id': unicode(course_id)}) else: pipeline_redirect = None return { provider.NAME: pipeline.get_login_url(provider.NAME, auth_entry, enroll_course_id=course_id, redirect_url=pipeline_redirect) for provider in provider.Registry.enabled() }
def test_full_pipeline_succeeds_for_linking_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. 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)) pipeline.analytics.track = mock.MagicMock() request.user = self.create_user_models_for_existing_account( strategy, '*****@*****.**', 'password', self.get_username(), skip_social_auth=True) # 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 mako_middleware_process_request(strategy.request) student_views.signin_user(strategy.request) student_views.login_user(strategy.request) actions.do_complete(request.backend, social_views._do_login) # pylint: disable=protected-access # 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), request.user, linked=False) self.assert_social_auth_does_not_exist_for_user(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( actions.do_complete( request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access redirect_field_name=auth.REDIRECT_FIELD_NAME)) # Set the cookie and try again self.set_logged_in_cookies(request) # Fire off the auth pipeline to link. self.assert_redirect_to_dashboard_looks_correct( actions.do_complete( request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access redirect_field_name=auth.REDIRECT_FIELD_NAME)) # 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), request.user, linked=True)
def assert_dashboard_response_looks_correct(self, response, user, duplicate=False, linked=None): """Asserts the user's dashboard is in the expected state. We check unconditionally that the dashboard 200s and contains the user's info. If duplicate is True, we expect the duplicate account association error to be present. If linked is passed, we conditionally check the content and controls in the Account Links section of the sidebar. """ duplicate_account_error_needle = '<section class="dashboard-banner third-party-auth">' assert_duplicate_presence_fn = self.assertIn if duplicate else self.assertNotIn self.assertEqual(200, response.status_code) self.assertIn(user.email, response.content) self.assertIn(user.username, response.content) assert_duplicate_presence_fn(duplicate_account_error_needle, response.content) if linked is not None: if linked: expected_control_text = pipeline.ProviderUserState( self.PROVIDER_CLASS, user, False).get_unlink_form_name() else: expected_control_text = pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_DASHBOARD) icon_state = re.search(r'third-party-auth.+icon icon-(\w+)', response.content, re.DOTALL).groups()[0] provider_name = re.search(r'<span class="provider">([^<]+)', response.content, re.DOTALL).groups()[0] self.assertIn(expected_control_text, response.content) self.assertEqual('link' if linked else 'unlink', icon_state) self.assertEqual(self.PROVIDER_CLASS.NAME, provider_name)
def test_login_url_returns_expected_format(self): login_url = pipeline.get_login_url(self.enabled_provider.provider_id, pipeline.AUTH_ENTRY_LOGIN) self.assertTrue(login_url.startswith('/auth/login')) self.assertIn(self.enabled_provider.backend_name, login_url) self.assertTrue(login_url.endswith(pipeline.AUTH_ENTRY_LOGIN))
def get(self, request, *args, **kwargs): """ Return either a redirect to the login page of an identity provider that corresponds to the provider_slug keyword argument or a 404 if the provider_slug does not correspond to an identity provider. Args: request (HttpRequest) Keyword Args: provider_slug (str): a slug corresponding to a configured identity provider Returns: HttpResponse: 302 to a provider's login url if the provider_slug kwarg matches an identity provider HttpResponse: 404 if the provider_slug kwarg does not match an identity provider """ # this gets the url to redirect to after login/registration/third_party_auth # it also handles checking the safety of the redirect url (next query parameter) # it checks against settings.LOGIN_REDIRECT_WHITELIST, so be sure to add the url # to this setting next_destination_url = get_next_url_for_login_page(request) try: url = pipeline.get_login_url(kwargs['provider_slug'], pipeline.AUTH_ENTRY_LOGIN, next_destination_url) return redirect(url) except ValueError: return HttpResponseNotFound()
def assert_signin_button_looks_functional(self, content, auth_entry): """Asserts JS is available to signin buttons and has the right args.""" self.assertTrue(re.search(r'function thirdPartySignin', content)) self.assertEqual( pipeline.get_login_url(self.PROVIDER_CLASS.NAME, auth_entry), re.search(r"thirdPartySignin\(event, '([^']+)", content).groups()[0])
def assert_dashboard_response_looks_correct(self, response, user, duplicate=False, linked=None): """Asserts the user's dashboard is in the expected state. We check unconditionally that the dashboard 200s and contains the user's info. If duplicate is True, we expect the duplicate account association error to be present. If linked is passed, we conditionally check the content and controls in the Account Links section of the sidebar. """ duplicate_account_error_needle = '<section class="dashboard-banner third-party-auth">' assert_duplicate_presence_fn = self.assertIn if duplicate else self.assertNotIn self.assertEqual(200, response.status_code) self.assertIn(user.email, response.content.decode('UTF-8')) self.assertIn(user.username, response.content.decode('UTF-8')) assert_duplicate_presence_fn(duplicate_account_error_needle, response.content) if linked is not None: if linked: expected_control_text = pipeline.ProviderUserState( self.PROVIDER_CLASS, user, False).get_unlink_form_name() else: expected_control_text = pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_DASHBOARD) icon_state = re.search(r'third-party-auth.+icon icon-(\w+)', response.content, re.DOTALL).groups()[0] provider_name = re.search(r'<span class="provider">([^<]+)', response.content, re.DOTALL).groups()[0] self.assertIn(expected_control_text, response.content) self.assertEqual('link' if linked else 'unlink', icon_state) self.assertEqual(self.PROVIDER_CLASS.NAME, provider_name)
def test_full_pipeline_succeeds_for_unlinking_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') strategy.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( strategy, '*****@*****.**', '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_cookie(request) # Instrument the pipeline to get to the dashboard with the full # expected state. self.client.get( pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN)) actions.do_complete(strategy, social_views._do_login) # pylint: disable-msg=protected-access mako_middleware_process_request(strategy.request) student_views.signin_user(strategy.request) student_views.login_user(strategy.request) actions.do_complete(strategy, social_views._do_login, user=user) # pylint: disable-msg=protected-access # First we expect that we're in the linked state, with a backend entry. self.assert_dashboard_response_looks_correct(student_views.dashboard(request), user, linked=True) self.assert_social_auth_exists_for_user(request.user, strategy) # Fire off the disconnect pipeline to unlink. self.assert_redirect_to_dashboard_looks_correct(actions.do_disconnect( request.social_strategy, 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_dashboard_response_looks_correct(student_views.dashboard(request), user, linked=False) self.assert_social_auth_does_not_exist_for_user(user, strategy)
def test_custom_form_links_by_email(self): self._configure_testshib_provider() self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. email = '*****@*****.**' UserFactory(username='******', email=email, password='******') self._verify_user_email(email) self._assert_user_exists('myself', have_social=False) custom_url = pipeline.get_login_url('saml-testshib', 'custom2') self.client.get(custom_url) testshib_response = self._fake_testshib_login_and_return() # We should be redirected to TPA-complete endpoint self.assertEqual(testshib_response.status_code, 302) self.assertEqual(testshib_response['Location'], self.url_prefix + TPA_TESTSHIB_COMPLETE_URL) complete_response = self.client.get(testshib_response['Location']) # And we should be redirected to the dashboard self.assertEqual(complete_response.status_code, 302) self.assertEqual(complete_response['Location'], self.url_prefix + self.dashboard_page_url) # And account should now be linked to social self._assert_user_exists('myself', have_social=True) # Now check that we can login again: self.client.logout() self._test_return_login()
def test_custom_form_error(self): """ Use the Google provider to test the custom login/register failure redirects. """ # The pipeline starts by a user GETting /auth/login/google-oauth2/?auth_entry=custom1 # Synthesize that request and check that it redirects to the correct # provider page. auth_entry = 'custom1' # See definition in lms/envs/test.py login_url = pipeline.get_login_url(self.provider.provider_id, auth_entry) login_url += "&next=/misc/final-destination" self.assert_redirect_to_provider_looks_correct( self.client.get(login_url)) def fake_auth_complete_error(_inst, *_args, **_kwargs): """ Mock the backend's auth_complete() method """ raise AuthException("Mock login failed") # Next, the provider makes a request against /auth/complete/<provider>. complete_url = pipeline.get_complete_url(self.provider.backend_name) with patch.object(self.provider.backend_class, 'auth_complete', fake_auth_complete_error): response = self.client.get(complete_url) # This should redirect to the custom error URL self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://example.none/misc/my-custom-sso-error-page')
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. 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)) pipeline.analytics.track = mock.MagicMock() user = self.create_user_models_for_existing_account( strategy, "*****@*****.**", "password", self.get_username() ) self.assert_social_auth_exists_for_user(user, strategy) self.assertTrue(user.is_active) # Begin! Ensure that the login form contains expected controls before # the user starts the pipeline. self.assert_login_response_before_pipeline_looks_correct(self.client.get("/login")) # The pipeline starts by a user GETting /auth/login/<provider>. # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct( self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) ) # 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)) # 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(student_views.signin_user(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(student_views.login_user(strategy.request)) # 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, ) ) # Set the cookie and try again self.set_logged_in_cookies(request) self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(request.backend, social_views._do_login, user=user) ) self.assert_account_settings_context_looks_correct(account_settings_context(request), user)
def test_full_pipeline_succeeds_registering_new_account(self): # First, create, the request and strategy that store pipeline state. # Mock out wire traffic. request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri='social:complete') strategy.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) # Begin! Grab the registration page and check the login control on it. self.assert_register_response_before_pipeline_looks_correct(self.client.get('/register')) # The pipeline starts by a user GETting /auth/login/<provider>. # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct(self.client.get( pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN))) # Next, the provider makes a request against /auth/complete/<provider>. # pylint:disable-msg=protected-access self.assert_redirect_to_register_looks_correct(actions.do_complete(strategy, social_views._do_login)) mako_middleware_process_request(strategy.request) # At this point we know the pipeline has resumed correctly. Next we # fire off the view that displays the registration form. self.assert_register_response_in_pipeline_looks_correct( student_views.register_user(strategy.request), pipeline.get(request)['kwargs']) # Next, we invoke the view that handles the POST. Not all providers # supply email. Manually add it as the user would have to; this # also serves as a test of overriding provider values. Always provide a # password for us to check that we override it properly. overridden_password = strategy.request.POST.get('password') email = '*****@*****.**' if not strategy.request.POST.get('email'): strategy.request.POST = self.get_registration_post_vars({'email': email}) # The user must not exist yet... with self.assertRaises(auth_models.User.DoesNotExist): self.get_user_by_email(strategy, email) # ...but when we invoke create_account the existing edX view will make # it, but not social auths. The pipeline creates those later. self.assert_json_success_response_looks_correct(student_views.create_account(strategy.request)) # We've overridden the user's password, so authenticate() with the old # value won't work: created_user = self.get_user_by_email(strategy, email) self.assert_password_overridden_by_pipeline(overridden_password, created_user.username) # At this point the user object exists, but there is no associated # social auth. self.assert_social_auth_does_not_exist_for_user(created_user, strategy) # Pick the pipeline back up. This will create the account association # and send the user to the dashboard, where the association will be # displayed. self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(strategy, social_views._do_login, user=created_user)) self.assert_social_auth_exists_for_user(created_user, strategy) self.assert_dashboard_response_looks_correct(student_views.dashboard(request), created_user, linked=True)
def test_with_valid_provider_slug(self): endpoint_url = self.get_idp_redirect_url('saml-test') expected_url = pipeline.get_login_url('saml-test', pipeline.AUTH_ENTRY_LOGIN, reverse('dashboard')) response = self.client.get(endpoint_url) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, expected_url)
def test_full_pipeline_succeeds_registering_new_account(self): # First, create, the request and strategy that store pipeline state. # Mock out wire traffic. request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri='social:complete') strategy.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) # Begin! Grab the registration page and check the login control on it. self.assert_register_response_before_pipeline_looks_correct(self.client.get('/register')) # The pipeline starts by a user GETting /auth/login/<provider>. # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct(self.client.get( pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN))) # Next, the provider makes a request against /auth/complete/<provider>. # pylint:disable-msg=protected-access self.assert_redirect_to_register_looks_correct(actions.do_complete(strategy, social_views._do_login)) # At this point we know the pipeline has resumed correctly. Next we # fire off the view that displays the registration form. self.assert_register_response_in_pipeline_looks_correct( student_views.register_user(strategy.request), pipeline.get(request)['kwargs']) # Next, we invoke the view that handles the POST. Not all providers # supply email. Manually add it as the user would have to; this # also serves as a test of overriding provider values. Always provide a # password for us to check that we override it properly. overridden_password = strategy.request.POST.get('password') email = '*****@*****.**' if not strategy.request.POST.get('email'): strategy.request.POST = self.get_registration_post_vars({'email': email}) # The user must not exist yet... with self.assertRaises(auth_models.User.DoesNotExist): self.get_user_by_email(strategy, email) # ...but when we invoke create_account the existing edX view will make # it, but not social auths. The pipeline creates those later. self.assert_json_success_response_looks_correct(student_views.create_account(strategy.request)) # We've overridden the user's password, so authenticate() with the old # value won't work: created_user = self.get_user_by_email(strategy, email) self.assert_password_overridden_by_pipeline(overridden_password, created_user.username) # At this point the user object exists, but there is no associated # social auth. self.assert_social_auth_does_not_exist_for_user(created_user, strategy) # Pick the pipeline back up. This will create the account association # and send the user to the dashboard, where the association will be # displayed. self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(strategy, social_views._do_login, user=created_user)) self.assert_social_auth_exists_for_user(created_user, strategy) self.assert_dashboard_response_looks_correct(student_views.dashboard(request), created_user, linked=True)
def test_custom_form(self): """ Use the Google provider to test the custom login/register form feature. """ # The pipeline starts by a user GETting /auth/login/google-oauth2/?auth_entry=custom1 # Synthesize that request and check that it redirects to the correct # provider page. auth_entry = 'custom1' # See definition in lms/envs/test.py login_url = pipeline.get_login_url(self.provider.provider_id, auth_entry) login_url += "&next=/misc/final-destination" self.assert_redirect_to_provider_looks_correct(self.client.get(login_url)) def fake_auth_complete(inst, *args, **kwargs): """ Mock the backend's auth_complete() method """ kwargs.update({'response': self.get_response_data(), 'backend': inst}) return inst.strategy.authenticate(*args, **kwargs) # Next, the provider makes a request against /auth/complete/<provider>. complete_url = pipeline.get_complete_url(self.provider.backend_name) with patch.object(self.provider.backend_class, 'auth_complete', fake_auth_complete): response = self.client.get(complete_url) # This should redirect to the custom login/register form: self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://example.none/auth/custom_auth_entry') response = self.client.get(response['Location']) self.assertEqual(response.status_code, 200) self.assertIn('action="/misc/my-custom-registration-form" method="post"', response.content) data_decoded = base64.b64decode(response.context['data']) # pylint: disable=no-member data_parsed = json.loads(data_decoded) # The user's details get passed to the custom page as a base64 encoded query parameter: self.assertEqual(data_parsed, { 'user_details': { 'username': '******', 'email': '*****@*****.**', 'fullname': 'name_value', 'first_name': 'given_name_value', 'last_name': 'family_name_value', } }) # Check the hash that is used to confirm the user's data in the GET parameter is correct secret_key = settings.THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS['custom1']['secret_key'] hmac_expected = hmac.new(secret_key, msg=data_decoded, digestmod=hashlib.sha256).digest() self.assertEqual(base64.b64decode(response.context['hmac']), hmac_expected) # pylint: disable=no-member # Now our custom registration form creates or logs in the user: email, password = data_parsed['user_details']['email'], 'random_password' created_user = UserFactory(email=email, password=password) login_response = self.client.post(reverse('login'), {'email': email, 'password': password}) self.assertEqual(login_response.status_code, 200) # Now our custom login/registration page must resume the pipeline: response = self.client.get(complete_url) self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://example.none/misc/final-destination') _, strategy = self.get_request_and_strategy() self.assert_social_auth_exists_for_user(created_user, strategy)
def test_full_pipeline_succeeds_for_linking_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. 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)) pipeline.analytics.track = mock.MagicMock() request.user = self.create_user_models_for_existing_account( strategy, "*****@*****.**", "password", self.get_username(), skip_social_auth=True ) # 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 student_views.signin_user(strategy.request) student_views.login_user(strategy.request) actions.do_complete(request.backend, social_views._do_login) # pylint: disable=protected-access # 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), request.user, linked=False ) self.assert_social_auth_does_not_exist_for_user(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( actions.do_complete( request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access redirect_field_name=auth.REDIRECT_FIELD_NAME, ) ) # Set the cookie and try again self.set_logged_in_cookies(request) # Fire off the auth pipeline to link. self.assert_redirect_to_dashboard_looks_correct( actions.do_complete( request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access redirect_field_name=auth.REDIRECT_FIELD_NAME, ) ) # 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), request.user, linked=True)
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. 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)) pipeline.analytics.track = mock.MagicMock() user = self.create_user_models_for_existing_account( strategy, '*****@*****.**', 'password', self.get_username()) self.assert_social_auth_exists_for_user(user, strategy) self.assertTrue(user.is_active) # Begin! Ensure that the login form contains expected controls before # the user starts the pipeline. self.assert_login_response_before_pipeline_looks_correct(self.client.get('/login')) # The pipeline starts by a user GETting /auth/login/<provider>. # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct(self.client.get( pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN))) # 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)) # 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(signin_user(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)) # 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 )) # Set the cookie and try again self.set_logged_in_cookies(request) self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(request.backend, social_views._do_login, user=user, request=request)) self.assert_account_settings_context_looks_correct(account_settings_context(request))
def test_full_pipeline_succeeds_for_linking_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. 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)) #pipeline.analytics.track = mock.MagicMock() #request.user = self.create_user_models_for_existing_account( # strategy, '*****@*****.**', 'password', self.get_username(), skip_social_auth=True) # 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))
def get(self, request): """ GET /api/third_party_auth/v0/providers/user_status/ **GET Response Values** ``` { "accepts_logins": true, "name": "Google", "disconnect_url": "/auth/disconnect/google-oauth2/?", "connect_url": "/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings", "connected": false, "id": "oa2-google-oauth2" } ``` """ tpa_states = [] for state in pipeline.get_provider_user_states(request.user): # We only want to include providers if they are either currently available to be logged # in with, or if the user is already authenticated with them. if state.provider.display_for_login or state.has_account: tpa_states.append({ 'id': state.provider.provider_id, 'name': state.provider. name, # The name of the provider e.g. Facebook 'connected': state. has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), }) return Response(tpa_states)
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account( self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') strategy.backend.auth_complete = mock.MagicMock( return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( strategy, '*****@*****.**', 'password', self.get_username()) self.assert_social_auth_exists_for_user(user, strategy) self.assertTrue(user.is_active) # Begin! Ensure that the login form contains expected controls before # the user starts the pipeline. self.assert_login_response_before_pipeline_looks_correct( self.client.get('/login')) # The pipeline starts by a user GETting /auth/login/<provider>. # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct( self.client.get( pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN))) # Next, the provider makes a request against /auth/complete/<provider> # to resume the pipeline. # pylint: disable-msg=protected-access self.assert_redirect_to_login_looks_correct( actions.do_complete(strategy, social_views._do_login)) mako_middleware_process_request(strategy.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. self.assert_login_response_in_pipeline_looks_correct( student_views.signin_user(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( student_views.login_user(strategy.request)) self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(strategy, social_views._do_login, user=user)) self.assert_dashboard_response_looks_correct( student_views.dashboard(request), user)
def test_full_pipeline_succeeds_for_unlinking_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. 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)) #user = self.create_user_models_for_existing_account( # strategy, '*****@*****.**', '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) # 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))
def test_custom_form_does_not_link_by_email(self): self._configure_testshib_provider() self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. email = '*****@*****.**' UserFactory(username='******', email=email, password='******') self._verify_user_email(email) self._assert_user_exists('myself', have_social=False) custom_url = pipeline.get_login_url('saml-testshib', 'custom1') self.client.get(custom_url) testshib_response = self._fake_testshib_login_and_return() # We should be redirected to the custom form since this account is not linked to an edX account, and # automatic linking is not enabled for custom1 entrypoint: self.assertEqual(testshib_response.status_code, 302) self.assertEqual(testshib_response['Location'], self.url_prefix + '/auth/custom_auth_entry')
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') strategy.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( strategy, '*****@*****.**', 'password', self.get_username()) self.assert_social_auth_exists_for_user(user, strategy) self.assertTrue(user.is_active) # Begin! Ensure that the login form contains expected controls before # the user starts the pipeline. self.assert_login_response_before_pipeline_looks_correct(self.client.get('/login')) # The pipeline starts by a user GETting /auth/login/<provider>. # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct(self.client.get( pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN))) # Next, the provider makes a request against /auth/complete/<provider> # to resume the pipeline. # pylint: disable-msg=protected-access self.assert_redirect_to_login_looks_correct(actions.do_complete(strategy, social_views._do_login)) mako_middleware_process_request(strategy.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. self.assert_login_response_in_pipeline_looks_correct(student_views.signin_user(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(student_views.login_user(strategy.request)) self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(strategy, social_views._do_login, user=user)) self.assert_dashboard_response_looks_correct(student_views.dashboard(request), user)
def _redirect_to_login(self, request): """ Returns HttpResponseRedirect object, redirecting User to third-party-auth identity provider on the basis of 'COLARAZ_AUTH_PROVIDER_BACKEND_NAME' or raising Http404 page in-case auth provider isn't configured properly. """ backend_name = getattr(settings, 'COLARAZ_AUTH_PROVIDER_BACKEND_NAME', None) if third_party_auth.is_enabled() and backend_name: provider = [ enabled for enabled in third_party_auth.provider.Registry.enabled() if enabled.backend_name == backend_name ] fallback_url = configuration_helpers.get_value('AUTH_PROVIDER_FALLBACK_URL') \ or configuration_helpers.get_value('LMS_BASE') if not provider and fallback_url: next_url = urlencode({'next': self._get_current_url(request)}) redirect_url = '//{}?{}'.format(fallback_url, next_url) LOGGER.info( 'No Auth Provider found, redirecting to "{}"'.format( redirect_url)) return HttpResponseRedirect(redirect_url) elif provider: login_url = pipeline.get_login_url( provider[0].provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=request.GET.get('next') if request.GET.get('next') else request.path, ) LOGGER.info('Redirecting User to Auth Provider: {}'.format( backend_name)) return HttpResponseRedirect(login_url) LOGGER.error( 'Unable to redirect, Auth Provider is not configured properly') raise Http404
def get(self, request): """ GET /api/third_party_auth/v0/providers/user_status/ **GET Response Values** { "accepts_logins": true, "name": "Google", "disconnect_url": "/auth/disconnect/google-oauth2/?", "connect_url": "/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings", "connected": false, "id": "oa2-google-oauth2" } """ tpa_states = [] for state in pipeline.get_provider_user_states(request.user): # We only want to include providers if they are either currently available to be logged # in with, or if the user is already authenticated with them. if state.provider.display_for_login or state.has_account: tpa_states.append({ 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), }) return Response(tpa_states)
def test_custom_form_error(self): """ Use the Google provider to test the custom login/register failure redirects. """ # The pipeline starts by a user GETting /auth/login/google-oauth2/?auth_entry=custom1 # Synthesize that request and check that it redirects to the correct # provider page. auth_entry = 'custom1' # See definition in lms/envs/test.py login_url = pipeline.get_login_url(self.provider.provider_id, auth_entry) login_url += "&next=/misc/final-destination" self.assert_redirect_to_provider_looks_correct(self.client.get(login_url)) def fake_auth_complete_error(_inst, *_args, **_kwargs): """ Mock the backend's auth_complete() method """ raise AuthException("Mock login failed") # Next, the provider makes a request against /auth/complete/<provider>. complete_url = pipeline.get_complete_url(self.provider.backend_name) with patch.object(self.provider.backend_class, 'auth_complete', fake_auth_complete_error): response = self.client.get(complete_url) # This should redirect to the custom error URL self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], '/misc/my-custom-sso-error-page')
def login(request): """Allow external auth to intercept and handle a login request. Arguments: request (Request): A request for the login page. Returns: Response or None """ # Default to a `None` response, indicating that external auth # is not handling the request. response = None if settings.IONISX_AUTH and third_party_auth.is_enabled(): # IONISx Authentication is enabled so we directly redirect # to IONISx portal redirect_to = request.GET.get('next') redirect_uri = get_login_url('oa2-portal-oauth2', AUTH_ENTRY_LOGIN, redirect_to) response = redirect(redirect_uri) elif settings.FEATURES['AUTH_USE_CERTIFICATES'] and external_auth.views.ssl_get_cert_from_request(request): # SSL login doesn't require a view, so redirect # branding and allow that to process the login if it # is enabled and the header is in the request. response = external_auth.views.redirect_with_get('root', request.GET) elif settings.FEATURES.get('AUTH_USE_CAS'): # If CAS is enabled, redirect auth handling to there response = redirect(reverse('cas-login')) elif settings.FEATURES.get('AUTH_USE_SHIB'): redirect_to = request.GET.get('next') if redirect_to: course_id = _parse_course_id_from_string(redirect_to) if course_id and _get_course_enrollment_domain(course_id): response = external_auth.views.course_specific_login(request, course_id.to_deprecated_string()) return response
def register(request): """Allow external auth to intercept and handle a registration request. Arguments: request (Request): A request for the registration page. Returns: Response or None """ response = None if settings.IONISX_AUTH and third_party_auth.is_enabled(): # IONISx Authentication is enabled so we directly redirect # to IONISx portal redirect_to = request.GET.get('next') redirect_uri = get_login_url('oa2-portal-oauth2', AUTH_ENTRY_REGISTER, redirect_to) response = redirect(redirect_uri) elif settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'): # Redirect to branding to process their certificate if SSL is enabled # and registration is disabled. response = external_auth.views.redirect_with_get('root', request.GET) return response
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception('Error fetching order history from Otto.') # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] context = { 'auth': {}, 'duplicate_provider': None, 'nav_hidden': True, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': all_languages(), }, 'time_zone': { 'options': TIME_ZONE_CHOICES, } }, 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'password_reset_support_link': configuration_helpers.get_value('PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, 'order_history': user_orders, 'enable_account_deletion': configuration_helpers.get_value( 'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False)), 'extended_profile_fields': _get_extended_profile_fields(), } enterprise_customer = get_enterprise_customer_for_learner( site=request.site, user=request.user) update_account_settings_context_for_enterprise(context, enterprise_customer) if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider( messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [ { 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state. has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), # We only want to include providers if they are either currently available to be logged # in with, or if the user is already authenticated with them. } for state in auth_states if state.provider.display_for_login or state.has_account ] return context
def test_full_pipeline_succeeds_registering_new_account(self): # First, create, the request and strategy that store pipeline state. # Mock out wire traffic. 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)) # Begin! Grab the registration page and check the login control on it. self.assert_register_response_before_pipeline_looks_correct(self.client.get('/register')) # The pipeline starts by a user GETting /auth/login/<provider>. # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct(self.client.get( pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN))) # Next, the provider makes a request against /auth/complete/<provider>. # pylint: disable=protected-access self.assert_redirect_to_register_looks_correct(actions.do_complete(request.backend, social_views._do_login, request=request)) # At this point we know the pipeline has resumed correctly. Next we # fire off the view that displays the registration form. with self._patch_edxmako_current_request(request): self.assert_register_response_in_pipeline_looks_correct( register_user(strategy.request), pipeline.get(request)['kwargs'], ['name', 'username', 'email'] ) # Next, we invoke the view that handles the POST. Not all providers # supply email. Manually add it as the user would have to; this # also serves as a test of overriding provider values. Always provide a # password for us to check that we override it properly. overridden_password = strategy.request.POST.get('password') email = '*****@*****.**' if not strategy.request.POST.get('email'): strategy.request.POST = self.get_registration_post_vars({'email': email}) # The user must not exist yet... with self.assertRaises(auth_models.User.DoesNotExist): self.get_user_by_email(strategy, email) # ...but when we invoke create_account the existing edX view will make # it, but not social auths. The pipeline creates those later. with self._patch_edxmako_current_request(strategy.request): self.assert_json_success_response_looks_correct(create_account(strategy.request)) # We've overridden the user's password, so authenticate() with the old # value won't work: created_user = self.get_user_by_email(strategy, email) self.assert_password_overridden_by_pipeline(overridden_password, created_user.username) # At this point the user object exists, but there is no associated # social auth. self.assert_social_auth_does_not_exist_for_user(created_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(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 )) # Set the cookie and try again self.set_logged_in_cookies(request) self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(strategy.request.backend, social_views._do_login, user=created_user, request=request)) # 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)
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] context = { 'auth': {}, 'duplicate_provider': None, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': settings.ALL_LANGUAGES, } }, 'platform_name': settings.PLATFORM_NAME, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [{ 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states] return context
def test_full_pipeline_succeeds_registering_new_account(self): # First, create, the request and strategy that store pipeline state. # Mock out wire traffic. 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)) # Begin! Grab the registration page and check the login control on it. self.assert_register_response_before_pipeline_looks_correct(self.client.get('/register')) # The pipeline starts by a user GETting /auth/login/<provider>. # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct(self.client.get( pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN))) # Next, the provider makes a request against /auth/complete/<provider>. # pylint: disable=protected-access self.assert_redirect_to_register_looks_correct(actions.do_complete(request.backend, social_views._do_login, request=request)) # At this point we know the pipeline has resumed correctly. Next we # fire off the view that displays the registration form. with self._patch_edxmako_current_request(request): self.assert_register_response_in_pipeline_looks_correct( register_user(strategy.request), pipeline.get(request)['kwargs'], ['name', 'username', 'email'] ) # Next, we invoke the view that handles the POST. Not all providers # supply email. Manually add it as the user would have to; this # also serves as a test of overriding provider values. Always provide a # password for us to check that we override it properly. overridden_password = strategy.request.POST.get('password') email = '*****@*****.**' if not strategy.request.POST.get('email'): strategy.request.POST = self.get_registration_post_vars({'email': email}) # The user must not exist yet... with self.assertRaises(auth_models.User.DoesNotExist): self.get_user_by_email(strategy, email) # ...but when we invoke create_account the existing edX view will make # it, but not social auths. The pipeline creates those later. with self._patch_edxmako_current_request(strategy.request): self.assert_json_success_response_looks_correct(create_account(strategy.request)) # We've overridden the user's password, so authenticate() with the old # value won't work: created_user = self.get_user_by_email(strategy, email) self.assert_password_overridden_by_pipeline(overridden_password, created_user.username) # At this point the user object exists, but there is no associated # social auth. self.assert_social_auth_does_not_exist_for_user(created_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(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 )) # 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)) # 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)
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] comuni_options = (("0", "Ninguna"), ("1", "Andalucía"), ("2", "Aragón"), ("3", "Asturias"), ("4", "Baleares"), ("5", "Canarias"), ("6", "Cantabria"), ("7", "Castilla y León"), ("8", "Castilla - La Mancha"), ("9", "Cataluña"), ("10", "Extremadura"), ("11", "Galicia"), ("12", "Madrid"), ("13", "Murcia"), ("14", "Navarra"), ("15", "País Vasco"), ("16", "La Rioja"), ("17", "Valencia"), ("18", "Ceuta"), ("19", "Melilla")) esdoce_options = (("1", _("Yes")), ("2", _("No"))) camp2_options = (("0", "--"), ("1", pgettext("Educational institution", "State")), ("2", _("Subsidized")), ("3", _("Private"))) context = { 'auth': {}, 'duplicate_provider': None, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': settings.ALL_LANGUAGES, }, 'comuni': { 'options': comuni_options, }, 'esdoce': { 'options': esdoce_options, }, 'camp2': { 'options': camp2_options, } }, 'platform_name': settings.PLATFORM_NAME, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider( messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [ { 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state. has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states ] return context
def _third_party_auth_context(request, redirect_to, tpa_hint=None): """Context for third party auth providers and the currently running pipeline. Arguments: request (HttpRequest): The request, used to determine if a pipeline is currently running. redirect_to: The URL to send the user to following successful authentication. tpa_hint (string): An override flag that will return a matching provider as long as its configuration has been enabled Returns: dict """ context = { "currentProvider": None, "providers": [], "secondaryProviders": [], "finishAuthUrl": None, "errorMessage": None, "registerFormSubmitButtonText": _("Create Account"), "syncLearnerProfileData": False, "pipeline_user_details": {} } if third_party_auth.is_enabled(): for enabled in third_party_auth.provider.Registry.displayed_for_login(tpa_hint=tpa_hint): info = { "id": enabled.provider_id, "name": enabled.name, "iconClass": enabled.icon_class or None, "iconImage": enabled.icon_image.url if enabled.icon_image else None, "loginUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } context["providers" if not enabled.secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) user_details = running_pipeline['kwargs']['details'] if user_details: context['pipeline_user_details'] = user_details if current_provider is not None: context["currentProvider"] = current_provider.name context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name) context["syncLearnerProfileData"] = current_provider.sync_learner_profile_data if current_provider.skip_registration_form: # As a reliable way of "skipping" the registration form, we just submit it automatically context["autoSubmitRegForm"] = True # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": # msg may or may not be translated. Try translating [again] in case we are able to: context["errorMessage"] = _(six.text_type(msg)) # pylint: disable=E7610 break return context
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] context = { 'auth': {}, 'duplicate_provider': None, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': settings.ALL_LANGUAGES, } }, 'platform_name': settings.PLATFORM_NAME, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider( messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [ { 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state. has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states ] return context
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard if request.user.is_authenticated(): return redirect(redirect_to) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get( provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. return redirect( pipeline.get_login_url(provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to)) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError): pass # If this is a themed site, revert to the old login/registration pages. # We need to do this for now to support existing themes. # Themed sites can use the new logistration page by setting # 'ENABLE_COMBINED_LOGIN_REGISTRATION' in their # configuration settings. if is_request_in_themed_site() and not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION', False): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Allow external auth to intercept and handle the request ext_auth_response = _external_auth_intercept(request, initial_mode) if ext_auth_response is not None: return ext_auth_response # Account activation message account_activation_messages = [{ 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)) }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER']), } context = update_context_for_enterprise(request, context) response = render_to_response('student_account/login_and_register.html', context) # Remove enterprise cookie so that subsequent requests show default login page. response.delete_cookie( configuration_helpers.get_value( "ENTERPRISE_CUSTOMER_COOKIE_NAME", settings.ENTERPRISE_CUSTOMER_COOKIE_NAME), domain=configuration_helpers.get_value("BASE_COOKIE_DOMAIN", settings.BASE_COOKIE_DOMAIN), ) return response
def _third_party_auth_context(request, redirect_to, tpa_hint=None): """Context for third party auth providers and the currently running pipeline. Arguments: request (HttpRequest): The request, used to determine if a pipeline is currently running. redirect_to: The URL to send the user to following successful authentication. tpa_hint (string): An override flag that will return a matching provider as long as its configuration has been enabled Returns: dict """ context = { "currentProvider": None, "providers": [], "secondaryProviders": [], "finishAuthUrl": None, "errorMessage": None, "registerFormSubmitButtonText": _("Create Account"), } if third_party_auth.is_enabled(): enterprise_customer = enterprise_customer_for_request(request) if not enterprise_customer: for enabled in third_party_auth.provider.Registry.displayed_for_login( tpa_hint=tpa_hint): info = { "id": enabled.provider_id, "name": enabled.name, "iconClass": enabled.icon_class or None, "iconImage": enabled.icon_image.url if enabled.icon_image else None, "loginUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } context["providers" if not enabled. secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_from_pipeline( running_pipeline) if current_provider is not None: context["currentProvider"] = current_provider.name context["finishAuthUrl"] = pipeline.get_complete_url( current_provider.backend_name) if current_provider.skip_registration_form: # For enterprise (and later for everyone), we need to get explicit consent to the # Terms of service instead of auto submitting the registration form outright. if not enterprise_customer: # As a reliable way of "skipping" the registration form, we just submit it automatically context["autoSubmitRegForm"] = True else: context["autoRegisterWelcomeMessage"] = ( 'Thank you for joining {}. ' 'Just a couple steps before you start learning!' ).format( configuration_helpers.get_value( 'PLATFORM_NAME', settings.PLATFORM_NAME)) context["registerFormSubmitButtonText"] = _("Continue") # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": # msg may or may not be translated. Try translating [again] in case we are able to: context['errorMessage'] = _(unicode(msg)) # pylint: disable=translation-of-non-string break return context
def _third_party_auth_context(request, redirect_to, tpa_hint=None): """Context for third party auth providers and the currently running pipeline. Arguments: request (HttpRequest): The request, used to determine if a pipeline is currently running. redirect_to: The URL to send the user to following successful authentication. tpa_hint (string): An override flag that will return a matching provider as long as its configuration has been enabled Returns: dict """ context = { "currentProvider": None, "providers": [], "secondaryProviders": [], "finishAuthUrl": None, "errorMessage": None, "registerFormSubmitButtonText": _("Create Account"), "syncLearnerProfileData": False, "pipeline_user_details": {} } if third_party_auth.is_enabled(): for enabled in third_party_auth.provider.Registry.displayed_for_login(tpa_hint=tpa_hint): info = { "id": enabled.provider_id, "name": enabled.name, "iconClass": enabled.icon_class or None, "iconImage": enabled.icon_image.url if enabled.icon_image else None, "loginUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } context["providers" if not enabled.secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) user_details = running_pipeline['kwargs']['details'] if user_details: context['pipeline_user_details'] = user_details if current_provider is not None: context["currentProvider"] = current_provider.name context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name) context["syncLearnerProfileData"] = current_provider.sync_learner_profile_data if current_provider.skip_registration_form: # As a reliable way of "skipping" the registration form, we just submit it automatically context["autoSubmitRegForm"] = True # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": # msg may or may not be translated. Try translating [again] in case we are able to: context['errorMessage'] = _(unicode(msg)) break return context
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard if request.user.is_authenticated: return redirect(redirect_to) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get(provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. if initial_mode == "register": auth_entry = pipeline.AUTH_ENTRY_REGISTER else: auth_entry = pipeline.AUTH_ENTRY_LOGIN return redirect( pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to) ) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError) as ex: log.error("Unknown tpa_hint provider: %s", ex) # If this is a themed site, revert to the old login/registration pages. # We need to do this for now to support existing themes. # Themed sites can use the new logistration page by setting # 'ENABLE_COMBINED_LOGIN_REGISTRATION' in their # configuration settings. if is_request_in_themed_site() and not configuration_helpers.get_value('ENABLE_COMBINED_LOGIN_REGISTRATION', False): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Allow external auth to intercept and handle the request ext_auth_response = _external_auth_intercept(request, initial_mode) if ext_auth_response is not None: return ext_auth_response # Account activation message account_activation_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags ] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)) }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER'] ), } enterprise_customer = enterprise_customer_for_request(request) update_logistration_context_for_enterprise(request, context, enterprise_customer) response = render_to_response('student_account/login_and_register.html', context) handle_enterprise_cookies_for_logistration(request, response, context) return response
def test_full_pipeline_succeeds_for_unlinking_testshib_account( self, mock_get_enterprise_customer_for_learner_settings_view, mock_get_enterprise_customer_for_learner, mock_enterprise_customer_for_request, ): # First, create, the request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. self.provider = self._configure_testshib_provider() request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') request.backend.auth_complete = MagicMock( return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( strategy, '*****@*****.**', 'password', self.get_username()) self.assert_social_auth_exists_for_user(user, strategy) request.user = user # We're already logged in, so simulate that the cookie is set correctly self.set_logged_in_cookies(request) # linking a learner with enterprise customer. enterprise_customer = EnterpriseCustomerFactory() assert EnterpriseCustomerUser.objects.count( ) == 0, "Precondition check: no link records should exist" EnterpriseCustomerUser.objects.link_user(enterprise_customer, user.email) self.assertTrue( EnterpriseCustomerUser.objects.filter( enterprise_customer=enterprise_customer, user_id=user.id).count() == 1) EnterpriseCustomerIdentityProvider.objects.get_or_create( enterprise_customer=enterprise_customer, provider_id=self.provider.provider_id) enterprise_customer_data = { 'uuid': enterprise_customer.uuid, 'name': enterprise_customer.name, 'identity_provider': 'saml-default', } mock_enterprise_customer_for_request.return_value = enterprise_customer_data mock_get_enterprise_customer_for_learner.return_value = enterprise_customer_data mock_get_enterprise_customer_for_learner_settings_view.return_value = enterprise_customer_data # 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) 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) # 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) FEATURES_WITH_ENTERPRISE_ENABLED = settings.FEATURES.copy() FEATURES_WITH_ENTERPRISE_ENABLED[ 'ENABLE_ENTERPRISE_INTEGRATION'] = True with patch.dict("django.conf.settings.FEATURES", FEATURES_WITH_ENTERPRISE_ENABLED): # Fire off the disconnect pipeline without the user information. actions.do_disconnect(request.backend, None, None, redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request) self.assertNotEqual( EnterpriseCustomerUser.objects.filter( enterprise_customer=enterprise_customer, user_id=user.id).count(), 0) # Fire off the disconnect pipeline to unlink. self.assert_redirect_after_pipeline_completes( actions.do_disconnect( request.backend, user, None, redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request)) # 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_social_auth_does_not_exist_for_user(user, strategy) self.assertEqual( EnterpriseCustomerUser.objects.filter( enterprise_customer=enterprise_customer, user_id=user.id).count(), 0)
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception('Error fetching order history from Otto.') # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] context = { 'auth': {}, 'duplicate_provider': None, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': all_languages(), }, 'time_zone': { 'options': UserPreference.TIME_ZONE_CHOICES, 'enabled': settings.FEATURES.get('ENABLE_TIME_ZONE_PREFERENCE'), } }, 'platform_name': get_themed_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.current().show_program_listing, 'order_history': user_orders } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [{ 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states] return context
def test_custom_form(self): """ Use the Google provider to test the custom login/register form feature. """ # The pipeline starts by a user GETting /auth/login/google-oauth2/?auth_entry=custom1 # Synthesize that request and check that it redirects to the correct # provider page. auth_entry = 'custom1' # See definition in lms/envs/test.py login_url = pipeline.get_login_url(self.provider.provider_id, auth_entry) login_url += "&next=/misc/final-destination" self.assert_redirect_to_provider_looks_correct( self.client.get(login_url)) def fake_auth_complete(inst, *args, **kwargs): """ Mock the backend's auth_complete() method """ kwargs.update({ 'response': self.get_response_data(), 'backend': inst }) return inst.strategy.authenticate(*args, **kwargs) # Next, the provider makes a request against /auth/complete/<provider>. complete_url = pipeline.get_complete_url(self.provider.backend_name) with patch.object(self.provider.backend_class, 'auth_complete', fake_auth_complete): response = self.client.get(complete_url) # This should redirect to the custom login/register form: self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://example.none/auth/custom_auth_entry') response = self.client.get(response['Location']) self.assertEqual(response.status_code, 200) self.assertIn( 'action="/misc/my-custom-registration-form" method="post"', response.content) data_decoded = base64.b64decode(response.context['data']) # pylint: disable=no-member data_parsed = json.loads(data_decoded) # The user's details get passed to the custom page as a base64 encoded query parameter: self.assertEqual( data_parsed, { 'auth_entry': 'custom1', 'backend_name': 'google-oauth2', 'provider_id': 'oa2-google-oauth2', 'user_details': { 'username': '******', 'email': '*****@*****.**', 'fullname': 'name_value', 'first_name': 'given_name_value', 'last_name': 'family_name_value', }, }) # Check the hash that is used to confirm the user's data in the GET parameter is correct secret_key = settings.THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS['custom1'][ 'secret_key'] hmac_expected = hmac.new(secret_key, msg=data_decoded, digestmod=hashlib.sha256).digest() self.assertEqual(base64.b64decode(response.context['hmac']), hmac_expected) # pylint: disable=no-member # Now our custom registration form creates or logs in the user: email, password = data_parsed['user_details'][ 'email'], 'random_password' created_user = UserFactory(email=email, password=password) login_response = self.client.post(reverse('login'), { 'email': email, 'password': password }) self.assertEqual(login_response.status_code, 200) # Now our custom login/registration page must resume the pipeline: response = self.client.get(complete_url) self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://example.none/misc/final-destination') _, strategy = self.get_request_and_strategy() self.assert_social_auth_exists_for_user(created_user, strategy)
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception('Error fetching order history from Otto.') # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] beta_language = {} dark_lang_config = DarkLangConfig.current() if dark_lang_config.enable_beta_languages: user_preferences = get_user_preferences(user) pref_language = user_preferences.get('pref-lang') if pref_language in dark_lang_config.beta_languages_list: beta_language['code'] = pref_language beta_language['name'] = settings.LANGUAGE_DICT.get(pref_language) context = { 'auth': {}, 'duplicate_provider': None, 'nav_hidden': True, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': all_languages(), }, 'time_zone': { 'options': TIME_ZONE_CHOICES, } }, 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, 'order_history': user_orders, 'disable_order_history_tab': should_redirect_to_order_history_microfrontend(), 'enable_account_deletion': configuration_helpers.get_value( 'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False) ), 'extended_profile_fields': _get_extended_profile_fields(), 'beta_language': beta_language, } enterprise_customer = get_enterprise_customer_for_learner(user=request.user) update_account_settings_context_for_enterprise(context, enterprise_customer) if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [{ 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), # We only want to include providers if they are either currently available to be logged # in with, or if the user is already authenticated with them. } for state in auth_states if state.provider.display_for_login or state.has_account] return context
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard # Note: We check for the existence of login-related cookies in addition to is_authenticated # since Django's SessionAuthentication middleware auto-updates session cookies but not # the other login-related cookies. See ARCH-282. if request.user.is_authenticated and are_logged_in_cookies_set(request): return redirect(redirect_to) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = six.moves.urllib.parse.parse_qs(six.moves.urllib.parse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get(provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. if initial_mode == "register": auth_entry = pipeline.AUTH_ENTRY_REGISTER else: auth_entry = pipeline.AUTH_ENTRY_LOGIN return redirect( pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to) ) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError) as ex: log.exception(u"Unknown tpa_hint provider: %s", ex) # We are defaulting to true because all themes should now be using the newer page. if is_request_in_themed_site() and not configuration_helpers.get_value('ENABLE_COMBINED_LOGIN_REGISTRATION', True): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Account activation message account_activation_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags ] account_recovery_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-recovery' in message.tags ] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, 'account_recovery_messages': account_recovery_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)), 'is_account_recovery_feature_enabled': is_secondary_email_feature_enabled() }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER'] ), } enterprise_customer = enterprise_customer_for_request(request) update_logistration_context_for_enterprise(request, context, enterprise_customer) response = render_to_response('student_account/login_and_register.html', context) handle_enterprise_cookies_for_logistration(request, response, context) return response
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user country_options = [ (country_code, _(country_name)) # pylint: disable=translation-of-non-string for country_code, country_name in sorted(countries.countries, key=lambda (__, name): unicode(name)) ] year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] context = { "auth": {}, "duplicate_provider": None, "fields": { "country": {"options": country_options}, "gender": { "options": [ (choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES ] # pylint: disable=translation-of-non-string }, "language": {"options": released_languages()}, "level_of_education": { "options": [ (choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES ] # pylint: disable=translation-of-non-string }, "password": {"url": reverse("password_reset")}, "year_of_birth": {"options": year_of_birth_options}, "preferred_language": {"options": settings.ALL_LANGUAGES}, }, "platform_name": settings.PLATFORM_NAME, "user_accounts_api_url": reverse("accounts_api", kwargs={"username": user.username}), "user_preferences_api_url": reverse("preferences_api", kwargs={"username": user.username}), } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context["duplicate_provider"] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context["auth"]["providers"] = [ { "name": state.provider.NAME, # The name of the provider e.g. Facebook "connected": state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider. "connect_url": pipeline.get_login_url( state.provider.NAME, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse("account_settings"), ), # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. "disconnect_url": pipeline.get_disconnect_url(state.provider.NAME), } for state in auth_states ] try: external_auth_map = ExternalAuthMap.objects.get(user=user) except: external_auth_map = None context["is_shib_auth"] = "shib" in external_auth_map.external_domain if external_auth_map else False return context