def test_complete_url_raises_value_error_if_provider_not_enabled(self): provider_name = 'oa2-not-enabled' assert provider.Registry.get(provider_name) is None with pytest.raises(ValueError): pipeline.get_complete_url(provider_name)
def test_complete_url_raises_value_error_if_provider_not_enabled(self): provider_name = 'oa2-not-enabled' self.assertIsNone(provider.Registry.get(provider_name)) with self.assertRaises(ValueError): pipeline.get_complete_url(provider_name)
def test_for_value_error_if_provider_id_invalid(self): provider_id = 'invalid' # Format is normally "{prefix}-{identifier}" with pytest.raises(ValueError): provider.Registry.get(provider_id) with pytest.raises(ValueError): pipeline.get_login_url(provider_id, pipeline.AUTH_ENTRY_LOGIN) with pytest.raises(ValueError): pipeline.get_disconnect_url(provider_id, 1000) with pytest.raises(ValueError): pipeline.get_complete_url(provider_id)
def get_context(self, params=None, current_provider=None, backend_name=None, add_user_details=False): """ Returns the third party auth context """ return { 'currentProvider': current_provider, 'platformName': settings.PLATFORM_NAME, 'providers': self.get_provider_data(params) if params else [], 'secondaryProviders': [], 'finishAuthUrl': pipeline.get_complete_url(backend_name) if backend_name else None, 'errorMessage': None, 'registerFormSubmitButtonText': 'Create Account', 'syncLearnerProfileData': False, 'pipeline_user_details': { 'email': '*****@*****.**' } if add_user_details else {} }
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 assert response.status_code == 302 assert response['Location'] == '/misc/my-custom-sso-error-page'
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: assert response.status_code == 302 assert response['Location'] == '/auth/custom_auth_entry' response = self.client.get(response['Location']) assert response.status_code == 200 assert 'action="/misc/my-custom-registration-form" method="post"' in response.content.decode('utf-8') data_decoded = base64.b64decode(response.context['data']).decode('utf-8') data_parsed = json.loads(data_decoded) # The user's details get passed to the custom page as a base64 encoded query parameter: assert 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.encode('utf-8'), msg=data_decoded.encode('utf-8'), digestmod=hashlib.sha256 ).digest() assert base64.b64decode(response.context['hmac']) == hmac_expected # 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_api'), {'email': email, 'password': password}) assert login_response.status_code == 200 # Now our custom login/registration page must resume the pipeline: response = self.client.get(complete_url) assert response.status_code == 302 assert response['Location'] == '/misc/final-destination' _, strategy = self.get_request_and_strategy() self.assert_social_auth_exists_for_user(created_user, strategy)
def login_user(request): """ AJAX request to log in the user. Arguments: request (HttpRequest) Required params: email, password Optional params: analytics: a JSON-encoded object with additional info to include in the login analytics event. The only supported field is "enroll_course_id" to indicate that the user logged in while enrolling in a particular course. Returns: HttpResponse: 200 if successful. Ex. {'success': true} HttpResponse: 400 if the request failed. Ex. {'success': false, 'value': '{'success': false, 'value: 'Email or password is incorrect.'} HttpResponse: 403 if successful authentication with a third party provider but does not have a linked account. Ex. {'success': false, 'error_code': 'third-party-auth-with-no-linked-account'} Example Usage: POST /login_ajax with POST params `email`, `password` 200 {'success': true} """ _parse_analytics_param_for_course_id(request) third_party_auth_requested = third_party_auth.is_enabled( ) and pipeline.running(request) first_party_auth_requested = bool(request.POST.get('email')) or bool( request.POST.get('password')) is_user_third_party_authenticated = False set_custom_attribute('login_user_course_id', request.POST.get('course_id')) try: if third_party_auth_requested and not first_party_auth_requested: # The user has already authenticated via third-party auth and has not # asked to do first party auth by supplying a username or password. We # now want to put them through the same logging and cookie calculation # logic as with first-party auth. # This nested try is due to us only returning an HttpResponse in this # one case vs. JsonResponse everywhere else. try: user = _do_third_party_auth(request) is_user_third_party_authenticated = True set_custom_attribute('login_user_tpa_success', True) except AuthFailedError as e: set_custom_attribute('login_user_tpa_success', False) set_custom_attribute('login_user_tpa_failure_msg', e.value) # user successfully authenticated with a third party provider, but has no linked Open edX account response_content = e.get_response() return JsonResponse(response_content, status=403) else: user = _get_user_by_email(request) _check_excessive_login_attempts(user) possibly_authenticated_user = user if not is_user_third_party_authenticated: possibly_authenticated_user = _authenticate_first_party( request, user, third_party_auth_requested) if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login( ): # Important: This call must be made AFTER the user was successfully authenticated. _enforce_password_policy_compliance( request, possibly_authenticated_user) if possibly_authenticated_user is None or not possibly_authenticated_user.is_active: _handle_failed_authentication(user, possibly_authenticated_user) _handle_successful_authentication_and_login( possibly_authenticated_user, request) redirect_url = None # The AJAX method calling should know the default destination upon success if is_user_third_party_authenticated: running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url( backend_name=running_pipeline['backend']) elif should_redirect_to_logistration_mircrofrontend(): redirect_url = get_next_url_for_login_page(request, include_host=True) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. response = set_logged_in_cookies(request, response, possibly_authenticated_user) set_custom_attribute('login_user_auth_failed_error', False) set_custom_attribute('login_user_response_status', response.status_code) set_custom_attribute('login_user_redirect_url', redirect_url) return response except AuthFailedError as error: response_content = error.get_response() log.exception(response_content) if response_content.get('error_code') == 'inactive-user': response_content['email'] = user.email response = JsonResponse(response_content, status=400) set_custom_attribute('login_user_auth_failed_error', True) set_custom_attribute('login_user_response_status', response.status_code) 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, "platformName": configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), "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, "skipHintedLogin": enabled.skip_hinted_login_dialog, "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: username = running_pipeline['kwargs'].get( 'username') or user_details.get('username') if username: user_details['username'] = clean_username(username) 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"] = _(str(msg)) # pylint: disable=E7610 break return context
def test_complete_url_returns_expected_format(self): complete_url = pipeline.get_complete_url( self.enabled_provider.backend_name) assert complete_url.startswith('/auth/complete') assert self.enabled_provider.backend_name in complete_url
def login_user(request, api_version='v1'): # pylint: disable=too-many-statements """ AJAX request to log in the user. Arguments: request (HttpRequest) Required params: email, password Optional params: analytics: a JSON-encoded object with additional info to include in the login analytics event. The only supported field is "enroll_course_id" to indicate that the user logged in while enrolling in a particular course. Returns: HttpResponse: 200 if successful. Ex. {'success': true} HttpResponse: 400 if the request failed. Ex. {'success': false, 'value': '{'success': false, 'value: 'Email or password is incorrect.'} HttpResponse: 403 if successful authentication with a third party provider but does not have a linked account. Ex. {'success': false, 'error_code': 'third-party-auth-with-no-linked-account'} Example Usage: POST /login_ajax with POST params `email`, `password` 200 {'success': true} """ _parse_analytics_param_for_course_id(request) third_party_auth_requested = third_party_auth.is_enabled( ) and pipeline.running(request) first_party_auth_requested = bool(request.POST.get('email')) or bool( request.POST.get('password')) is_user_third_party_authenticated = False set_custom_attribute('login_user_course_id', request.POST.get('course_id')) if is_require_third_party_auth_enabled( ) and not third_party_auth_requested: return HttpResponseForbidden( "Third party authentication is required to login. Username and password were received instead." ) possibly_authenticated_user = None try: if third_party_auth_requested and not first_party_auth_requested: # The user has already authenticated via third-party auth and has not # asked to do first party auth by supplying a username or password. We # now want to put them through the same logging and cookie calculation # logic as with first-party auth. # This nested try is due to us only returning an HttpResponse in this # one case vs. JsonResponse everywhere else. try: user = _do_third_party_auth(request) is_user_third_party_authenticated = True set_custom_attribute('login_user_tpa_success', True) except AuthFailedError as e: set_custom_attribute('login_user_tpa_success', False) set_custom_attribute('login_user_tpa_failure_msg', e.value) if e.error_code: set_custom_attribute('login_error_code', e.error_code) # user successfully authenticated with a third party provider, but has no linked Open edX account response_content = e.get_response() return JsonResponse(response_content, status=403) else: user = _get_user_by_email_or_username(request, api_version) _check_excessive_login_attempts(user) possibly_authenticated_user = user try: possibly_authenticated_user = StudentLoginRequested.run_filter( user=possibly_authenticated_user) except StudentLoginRequested.PreventLogin as exc: raise AuthFailedError( str(exc), redirect_url=exc.redirect_to, error_code=exc.error_code, context=exc.context, ) from exc if not is_user_third_party_authenticated: possibly_authenticated_user = _authenticate_first_party( request, user, third_party_auth_requested) if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login( ): # Important: This call must be made AFTER the user was successfully authenticated. _enforce_password_policy_compliance( request, possibly_authenticated_user) if possibly_authenticated_user is None or not ( possibly_authenticated_user.is_active or settings.MARKETING_EMAILS_OPT_IN): _handle_failed_authentication(user, possibly_authenticated_user) pwned_properties = check_pwned_password_and_send_track_event( user.id, request.POST.get('password'), user.is_staff) if not is_user_third_party_authenticated else {} # Set default for third party login password_frequency = pwned_properties.get('frequency', -1) if (settings.ENABLE_AUTHN_LOGIN_BLOCK_HIBP_POLICY and password_frequency >= settings.HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD): raise VulnerablePasswordError( accounts.AUTHN_LOGIN_BLOCK_HIBP_POLICY_MSG, 'require-password-change') _handle_successful_authentication_and_login( possibly_authenticated_user, request) # The AJAX method calling should know the default destination upon success redirect_url, finish_auth_url = None, '' if third_party_auth_requested: running_pipeline = pipeline.get(request) finish_auth_url = pipeline.get_complete_url( backend_name=running_pipeline['backend']) if is_user_third_party_authenticated: redirect_url = finish_auth_url elif should_redirect_to_authn_microfrontend(): next_url, root_url = get_next_url_for_login_page(request, include_host=True) redirect_url = get_redirect_url_with_host( root_url, enterprise_selection_page(request, possibly_authenticated_user, finish_auth_url or next_url)) if (settings.ENABLE_AUTHN_LOGIN_NUDGE_HIBP_POLICY and 0 <= password_frequency <= settings.HIBP_LOGIN_NUDGE_PASSWORD_FREQUENCY_THRESHOLD): raise VulnerablePasswordError( accounts.AUTHN_LOGIN_NUDGE_HIBP_POLICY_MSG, 'nudge-password-change', redirect_url) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. response = set_logged_in_cookies(request, response, possibly_authenticated_user) set_custom_attribute('login_user_auth_failed_error', False) set_custom_attribute('login_user_response_status', response.status_code) set_custom_attribute('login_user_redirect_url', redirect_url) mark_user_change_as_expected(user.id) return response except AuthFailedError as error: response_content = error.get_response() log.exception(response_content) error_code = response_content.get('error_code') if error_code: set_custom_attribute('login_error_code', error_code) email_or_username_key = 'email' if api_version == API_V1 else 'email_or_username' email_or_username = request.POST.get(email_or_username_key, None) email_or_username = possibly_authenticated_user.email if possibly_authenticated_user else email_or_username response_content['email'] = email_or_username except VulnerablePasswordError as error: response_content = error.get_response() log.exception(response_content) response = JsonResponse(response_content, status=400) set_custom_attribute('login_user_auth_failed_error', True) set_custom_attribute('login_user_response_status', response.status_code) return response