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 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 _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 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 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, '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 assert_json_success_response_looks_correct(self, response): """Asserts the json response indicates success and redirection.""" self.assertEqual(200, response.status_code) payload = json.loads(response.content) self.assertTrue(payload.get('success')) self.assertEqual(pipeline.get_complete_url(self.provider.backend_name), payload.get('redirect_url'))
def login_user(request): """ AJAX request to log in the user. """ 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 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 except AuthFailedError as e: return HttpResponse(e.value, content_type="text/plain", status=403) else: user = _get_user_by_email(request) _check_shib_redirect(user) _check_excessive_login_attempts(user) possibly_authenticated_user = user if not is_user_third_party_authenticated: possibly_authenticated_user = _authenticate_first_party(request, user) 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) if not is_edly_user_allowed_to_login(request, possibly_authenticated_user): raise AuthFailedError(_('You are not allowed to login on this site.')) _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']) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. return set_logged_in_cookies(request, response, possibly_authenticated_user) except AuthFailedError as error: log.exception(error.get_response()) return JsonResponse(error.get_response())
def get_request_and_strategy(self, auth_entry=None, redirect_uri=None): """Gets a fully-configured request and strategy. These two objects contain circular references, so we create them together. The references themselves are a mixture of normal __init__ stuff and monkey-patching done by python-social-auth. See, for example, social.apps.django_apps.utils.strategy(). """ request = self.request_factory.get( pipeline.get_complete_url(self.backend_name) + '?redirect_state=redirect_state_value&code=code_value&state=state_value' ) request.user = auth_models.AnonymousUser() request.session = cache.SessionStore() request.session[self.backend_name + '_state'] = 'state_value' if auth_entry: request.session[pipeline.AUTH_ENTRY_KEY] = auth_entry strategy = social_utils.load_strategy(request=request) request.social_strategy = strategy request.backend = social_utils.load_backend(strategy, self.backend_name, redirect_uri) return request, strategy
def assert_logged_in_cookie_redirect(self, response): """Verify that the user was redirected in order to set the logged in cookie. """ self.assertEqual(response.status_code, 302) self.assertEqual( response["Location"], pipeline.get_complete_url(self.PROVIDER_CLASS.BACKEND_CLASS.name) ) self.assertEqual(response.cookies[django_settings.EDXMKTG_COOKIE_NAME].value, '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 login_user(request): """ AJAX request to log in the user. """ third_party_auth_requested = third_party_auth.is_enabled() and pipeline.running(request) trumped_by_first_party_auth = bool(request.POST.get('email')) or bool(request.POST.get('password')) was_authenticated_third_party = False try: if third_party_auth_requested and not trumped_by_first_party_auth: # 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: email_user = _do_third_party_auth(request) was_authenticated_third_party = True except AuthFailedError as e: return HttpResponse(e.value, content_type="text/plain", status=403) else: email_user = _get_user_by_email(request) _check_shib_redirect(email_user) _check_excessive_login_attempts(email_user) _check_forced_password_reset(email_user) possibly_authenticated_user = email_user if not was_authenticated_third_party: possibly_authenticated_user = _authenticate_first_party(request, email_user) 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(email_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 was_authenticated_third_party: running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url(backend_name=running_pipeline['backend']) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. return set_logged_in_cookies(request, response, possibly_authenticated_user) except AuthFailedError as error: log.exception(error.get_response()) return JsonResponse(error.get_response())
def _fake_strategy(self): """Simulate the strategy passed to the pipeline step. """ request = RequestFactory().get( pipeline.get_complete_url(self.BACKEND_NAME)) request.user = self.user request.session = cache.SessionStore() return social_utils.load_strategy(backend=self.BACKEND_NAME, request=request)
def assert_logged_in_cookie_redirect(self, response): """Verify that the user was redirected in order to set the logged in cookie. """ self.assertEqual(response.status_code, 302) self.assertEqual( response["Location"], pipeline.get_complete_url(self.provider.backend_name) ) self.assertEqual(response.cookies[django_settings.EDXMKTG_LOGGED_IN_COOKIE_NAME].value, 'true') self.assertIn(django_settings.EDXMKTG_USER_INFO_COOKIE_NAME, response.cookies)
def _fake_strategy(self): """Simulate the strategy passed to the pipeline step. """ request = RequestFactory().get(pipeline.get_complete_url(self.BACKEND_NAME)) request.user = self.user request.session = cache.SessionStore() return social_utils.load_strategy( backend=self.BACKEND_NAME, request=request )
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 course(request): response_format = request.GET.get('format', 'html') if request.method == 'GET' and response_format == 'html' and 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'): 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: redirect_to = pipeline.get_complete_url(current_provider.backend_name) return redirect(redirect_to) return course_handler(request)
def login_user(request): """ AJAX request to log in the user. """ third_party_auth_requested = third_party_auth.is_enabled() and pipeline.running(request) trumped_by_first_party_auth = bool(request.POST.get('email')) or bool(request.POST.get('password')) was_authenticated_third_party = False try: if third_party_auth_requested and not trumped_by_first_party_auth: # 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: email_user = _do_third_party_auth(request) was_authenticated_third_party = True except AuthFailedError as e: return HttpResponse(e.value, content_type="text/plain", status=403) else: email_user = _get_user_by_email(request) _check_shib_redirect(email_user) _check_excessive_login_attempts(email_user) _check_forced_password_reset(email_user) possibly_authenticated_user = email_user if not was_authenticated_third_party: possibly_authenticated_user = _authenticate_first_party(request, email_user) if possibly_authenticated_user is None or not possibly_authenticated_user.is_active: _handle_failed_authentication(email_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 was_authenticated_third_party: running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url(backend_name=running_pipeline['backend']) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. return set_logged_in_cookies(request, response, possibly_authenticated_user) except AuthFailedError as error: return JsonResponse(error.get_response())
def create_account(request, post_override=None): """ Deprecated. Use RegistrationView instead. JSON call to create new edX account. Used by form in signup_modal.html, which is included into header.html """ # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation if not configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)): return HttpResponseForbidden(_("Account creation not allowed.")) if waffle().is_enabled(PREVENT_AUTH_USER_WRITES): return HttpResponseForbidden(SYSTEM_MAINTENANCE_MSG) warnings.warn("Please use RegistrationView instead.", DeprecationWarning) try: user = create_account_with_params(request, post_override or request.POST) except AccountValidationError as exc: return JsonResponse( { 'success': False, 'value': text_type(exc), 'field': exc.field }, status=400) except ValidationError as exc: field, error_list = next(iteritems(exc.message_dict)) return JsonResponse( { "success": False, "field": field, "value": ' '.join(error_list), }, status=400) redirect_url = None # The AJAX method calling should know the default destination upon success # Resume the third-party-auth pipeline if necessary. if third_party_auth.is_enabled() and pipeline.running(request): running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url(running_pipeline['backend']) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) set_logged_in_cookies(request, response, user) return response
def post_account_consent(self, request, consent_provided): """ Interpret the account-wide form above, and save it to a UserDataSharingConsentAudit object for later retrieval. """ self.lift_quarantine(request) # Load the linked EnterpriseCustomer for this request. customer = get_enterprise_customer_for_request(request) if customer is None: # If we can't get an EnterpriseCustomer from the pipeline, then we don't really # have enough state to do anything meaningful. Just send the user to the login # screen; if they want to sign in with an Enterprise-linked SSO, they can do # so, and the pipeline will get them back here if they need to be. return redirect('signin_user') # Attempt to retrieve a user being manipulated by the third-party auth # pipeline. Return a 404 if no such user exists. social_auth = get_real_social_auth_object(request) user = getattr(social_auth, 'user', None) if user is None: raise Http404 if not consent_provided and active_provider_enforces_data_sharing( request, EnterpriseCustomer.AT_LOGIN): # Flush the session to avoid the possibility of accidental login and to abort the pipeline. # pipeline is flushed only if data sharing is enforced, in other cases let the user to login. request.session.flush() failure_url = request.POST.get('failure_url') or reverse( 'dashboard') return redirect(failure_url) ec_user, __ = EnterpriseCustomerUser.objects.get_or_create( user_id=user.id, enterprise_customer=customer, ) UserDataSharingConsentAudit.objects.update_or_create( user=ec_user, defaults={ 'state': (UserDataSharingConsentAudit.ENABLED if consent_provided else UserDataSharingConsentAudit.DISABLED) }) # Resume auth pipeline backend_name = request.session.get('partial_pipeline', {}).get('backend') return redirect(get_complete_url(backend_name))
def create_account(request, post_override=None): """ Deprecated. Use RegistrationView instead. JSON call to create new edX account. Used by form in signup_modal.html, which is included into header.html """ # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation if not configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True) ): return HttpResponseForbidden(_("Account creation not allowed.")) if waffle().is_enabled(PREVENT_AUTH_USER_WRITES): return HttpResponseForbidden(SYSTEM_MAINTENANCE_MSG) warnings.warn("Please use RegistrationView instead.", DeprecationWarning) try: user = create_account_with_params(request, post_override or request.POST) except AccountValidationError as exc: return JsonResponse({'success': False, 'value': text_type(exc), 'field': exc.field}, status=400) except ValidationError as exc: field, error_list = next(iteritems(exc.message_dict)) return JsonResponse( { "success": False, "field": field, "value": ' '.join(error_list), }, status=400 ) redirect_url = None # The AJAX method calling should know the default destination upon success # Resume the third-party-auth pipeline if necessary. if third_party_auth.is_enabled() and pipeline.running(request): running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url(running_pipeline['backend']) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) set_logged_in_cookies(request, response, user) return response
def post_account_consent(self, request, consent_provided): """ Interpret the account-wide form above, and save it to a UserDataSharingConsentAudit object for later retrieval. """ self.lift_quarantine(request) # Load the linked EnterpriseCustomer for this request. Return a 404 if no such EnterpriseCustomer exists customer = get_enterprise_customer_for_request(request) if customer is None: raise Http404 # Attempt to retrieve a user being manipulated by the third-party auth # pipeline. Return a 404 if no such user exists. social_auth = get_real_social_auth_object(request) user = getattr(social_auth, 'user', None) if user is None: raise Http404 if not consent_provided and active_provider_enforces_data_sharing( request, EnterpriseCustomer.AT_LOGIN): # Flush the session to avoid the possibility of accidental login and to abort the pipeline. # pipeline is flushed only if data sharing is enforced, in other cases let the user to login. request.session.flush() return redirect(reverse('dashboard')) ec_user, __ = EnterpriseCustomerUser.objects.get_or_create( user_id=user.id, enterprise_customer=customer, ) UserDataSharingConsentAudit.objects.update_or_create( user=ec_user, defaults={ 'state': (UserDataSharingConsentAudit.ENABLED if consent_provided else UserDataSharingConsentAudit.DISABLED) }) # Resume auth pipeline backend_name = request.session.get('partial_pipeline', {}).get('backend') return redirect(get_complete_url(backend_name))
def get_request_and_strategy(self, auth_entry=None, redirect_uri=None): """Gets a fully-configured request and strategy. These two objects contain circular references, so we create them together. The references themselves are a mixture of normal __init__ stuff and monkey-patching done by python-social-auth. See, for example, social.apps.django_apps.utils.strategy(). """ request = self.request_factory.get( pipeline.get_complete_url(self.backend_name) + '?redirect_state=redirect_state_value&code=code_value&state=state_value') request.user = auth_models.AnonymousUser() request.session = cache.SessionStore() request.session[self.backend_name + '_state'] = 'state_value' if auth_entry: request.session[pipeline.AUTH_ENTRY_KEY] = auth_entry strategy = social_utils.load_strategy(backend=self.backend_name, redirect_uri=redirect_uri, request=request) request.social_strategy = strategy return request, strategy
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 post(self, request): """ Process the above form. """ # Verify that all necessary resources are present verify_edx_resources() self.lift_quarantine(request) customer = get_enterprise_customer_for_request(request) if customer is None: raise Http404 consent_provided = request.POST.get('data_sharing_consent', False) # If the checkbox is unchecked, no value will be sent user = get_real_social_auth_object(request).user ec_user, __ = EnterpriseCustomerUser.objects.get_or_create( user_id=user.id, enterprise_customer=customer, ) UserDataSharingConsentAudit.objects.update_or_create( user=ec_user, defaults={ 'state': (UserDataSharingConsentAudit.ENABLED if consent_provided else UserDataSharingConsentAudit.DISABLED) }) if not consent_provided: # Flush the session to avoid the possibility of accidental login and to abort the pipeline. # pipeline is flushed only if data sharing is enforced, in other cases let the user to login. if active_provider_enforces_data_sharing( request, EnterpriseCustomer.AT_LOGIN): request.session.flush() return redirect(reverse('dashboard')) # Resume auth pipeline backend_name = request.session.get('partial_pipeline', {}).get('backend') return redirect(get_complete_url(backend_name))
def login_user(request): """ AJAX request to log in the user. """ _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_metric('login_user_enrollment_action', request.POST.get('enrollment_action')) set_custom_metric('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_metric('login_user_tpa_success', True) except AuthFailedError as e: set_custom_metric('login_user_tpa_success', False) set_custom_metric('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() response_content[ 'error_code'] = 'third-party-auth-with-no-linked-account' 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) 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']) 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_metric('login_user_auth_failed_error', False) set_custom_metric('login_user_response_status', response.status_code) return response except AuthFailedError as error: log.exception(error.get_response()) # original code returned a 200 status code with status=False for errors. This flag # is used for rolling out a transition to using a 400 status code for errors, which # is a breaking-change, but will hopefully be a tolerable breaking-change. status = 400 if UPDATE_LOGIN_USER_ERROR_STATUS_CODE.is_enabled( ) else 200 response = JsonResponse(error.get_response(), status=status) set_custom_metric('login_user_auth_failed_error', True) set_custom_metric('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, "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 login_user_custom(request, error=""): # pylint: disable=too-many-statements,unused-argument """AJAX request to log in the user.""" backend_name = None email = None password = None redirect_url = None response = None running_pipeline = None third_party_auth_requested = third_party_auth.is_enabled( ) and pipeline.running(request) third_party_auth_successful = False trumped_by_first_party_auth = bool(request.POST.get('email')) or bool( request.POST.get('password')) user = None platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) if third_party_auth_requested and not trumped_by_first_party_auth: # 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. running_pipeline = pipeline.get(request) username = running_pipeline['kwargs'].get('username') backend_name = running_pipeline['backend'] third_party_uid = running_pipeline['kwargs']['uid'] requested_provider = provider.Registry.get_from_pipeline( running_pipeline) try: user = pipeline.get_authenticated_user(requested_provider, username, third_party_uid) third_party_auth_successful = True except User.DoesNotExist: AUDIT_LOG.warning( u"Login failed - user with username {username} has no social auth " "with backend_name {backend_name}".format( username=username, backend_name=backend_name)) message = _( "You've successfully logged into your {provider_name} account, " "but this account isn't linked with an {platform_name} account yet." ).format( platform_name=platform_name, provider_name=requested_provider.name, ) message += "<br/><br/>" message += _( "Use your {platform_name} username and password to log into {platform_name} below, " "and then link your {platform_name} account with {provider_name} from your dashboard." ).format( platform_name=platform_name, provider_name=requested_provider.name, ) message += "<br/><br/>" message += _( "If you don't have an {platform_name} account yet, " "click <strong>Register</strong> at the top of the page." ).format(platform_name=platform_name) return HttpResponse(message, content_type="text/plain", status=403) else: if 'email' not in request.POST or 'password' not in request.POST: return JsonResponse({ "success": False, # TODO: User error message "value": _('There was an error receiving your login information. Please email us.' ), }) # TODO: this should be status code 400 email = request.POST['email'] password = request.POST['password'] try: user = User.objects.get(email=email) except User.DoesNotExist: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u"Login failed - Unknown user email") else: AUDIT_LOG.warning( u"Login failed - Unknown user email: {0}".format(email)) # check if the user has a linked shibboleth account, if so, redirect the user to shib-login # This behavior is pretty much like what gmail does for shibboleth. Try entering some @stanford.edu # address into the Gmail login. if settings.FEATURES.get('AUTH_USE_SHIB') and user: try: eamap = ExternalAuthMap.objects.get(user=user) if eamap.external_domain.startswith( openedx.core.djangoapps.external_auth.views. SHIBBOLETH_DOMAIN_PREFIX): return JsonResponse({ "success": False, "redirect": reverse('shib-login'), }) # TODO: this should be status code 301 # pylint: disable=fixme except ExternalAuthMap.DoesNotExist: # This is actually the common case, logging in user without external linked login AUDIT_LOG.info(u"User %s w/o external auth attempting login", user) # see if account has been locked out due to excessive login failures user_found_by_email_lookup = user if user_found_by_email_lookup and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user_found_by_email_lookup): lockout_message = _( 'This account has been temporarily locked due ' 'to excessive login failures. Try again later.') return JsonResponse({ "success": False, "value": lockout_message, }) # TODO: this should be status code 429 # pylint: disable=fixme # see if the user must reset his/her password due to any policy settings if user_found_by_email_lookup and PasswordHistory.should_user_reset_password_now( user_found_by_email_lookup): return JsonResponse({ "success": False, "value": _('Your password has expired due to password policy on this account. You must ' 'reset your password before you can log in again. Please click the ' '"Forgot Password" link on this page to reset your password before logging in again.' ), }) # TODO: this should be status code 403 # pylint: disable=fixme # if the user doesn't exist, we want to set the username to an invalid # username so that authentication is guaranteed to fail and we can take # advantage of the ratelimited backend username = user.username if user else "" if not third_party_auth_successful: try: user = authenticate(username=username, password=password, request=request) # this occurs when there are too many attempts from the same IP address except RateLimitException: return JsonResponse({ "success": False, "value": _('Too many failed login attempts. Try again later.'), }) # TODO: this should be status code 429 # pylint: disable=fixme if user is None: # tick the failed login counters if the user exists in the database if user_found_by_email_lookup and LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user_found_by_email_lookup) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password if username != "": if settings.FEATURES['SQUELCH_PII_IN_LOGS']: loggable_id = user_found_by_email_lookup.id if user_found_by_email_lookup else "<unknown>" AUDIT_LOG.warning( u"Login failed - password for user.id: {0} is invalid". format(loggable_id)) else: AUDIT_LOG.warning( u"Login failed - password for {0} is invalid".format( email)) return JsonResponse({ "success": False, "value": _('Email or password is incorrect.'), }) # TODO: this should be status code 400 # pylint: disable=fixme # successful login, clear failed login attempts counters, if applicable if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) # Track the user's sign in if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.identify( user.id, { 'email': email, 'username': username }, { # Disable MailChimp because we don't want to update the user's email # and username in MailChimp on every page load. We only need to capture # this data on registration/activation. 'MailChimp': False }) analytics.track(user.id, "edx.bi.user.account.authenticated", { 'category': "conversion", 'label': request.POST.get('course_id'), 'provider': None }, context={ 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } }) if user is not None and user.is_active: try: # We do not log here, because we have a handler registered # to perform logging on successful logins. login(request, user) if request.POST.get('remember') == 'true': request.session.set_expiry(604800) log.debug("Setting user session to never expire") else: request.session.set_expiry(0) except Exception as exc: # pylint: disable=broad-except AUDIT_LOG.critical( "Login failed - Could not create session. Is memcached running?" ) log.critical( "Login failed - Could not create session. Is memcached running?" ) log.exception(exc) raise redirect_url = None # The AJAX method calling should know the default destination upon success if third_party_auth_successful: redirect_url = pipeline.get_complete_url(backend_name) response = JsonResponse({ "success": True, "redirect_url": redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. return set_logged_in_cookies(request, response, user) if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u"Login failed - Account not active for user.id: {0}, resending activation" .format(user.id)) else: AUDIT_LOG.warning( u"Login failed - Account not active for user {0}, resending activation" .format(username)) reactivation_email_for_user_custom(request, user) not_activated_msg = _( "Before you sign in, you need to activate your account. We have sent you an " "email message with instructions for activating your account.") return JsonResponse({ "success": False, "value": not_activated_msg, }) # TODO: this should be status code 400 # pylint: disable=fixme
def test_complete_url_returns_expected_format(self): complete_url = pipeline.get_complete_url(self.enabled_provider.backend_name) self.assertTrue(complete_url.startswith('/auth/complete')) self.assertIn(self.enabled_provider.backend_name, complete_url)
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
if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): log.info('bypassing activation email') login_user.is_active = True login_user.save() AUDIT_LOG.info( u"Login activated on extauth account - {0} ({1})".format( login_user.username, login_user.email)) dog_stats_api.increment("common.student.account_created") redirect_url = try_change_enrollment(request) # Resume the third-party-auth pipeline if necessary. if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running( request): running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url(running_pipeline['backend']) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # set the login cookie for the edx marketing site # we want this cookie to be accessed via javascript # so httponly is set to None if request.session.get_expire_at_browser_close(): max_age = None expires = None else: max_age = request.session.get_expiry_age()
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 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_metric('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_metric('login_user_tpa_success', True) except AuthFailedError as e: set_custom_metric('login_user_tpa_success', False) set_custom_metric('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() response_content[ 'error_code'] = 'third-party-auth-with-no-linked-account' 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']) 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_metric('login_user_auth_failed_error', False) set_custom_metric('login_user_response_status', response.status_code) set_custom_metric('login_user_redirect_url', redirect_url) return response except AuthFailedError as error: log.exception(error.get_response()) response = JsonResponse(error.get_response(), status=400) set_custom_metric('login_user_auth_failed_error', True) set_custom_metric('login_user_response_status', response.status_code) return response
def post_account_consent(self, request, consent_provided): """ Interpret the account-wide form above, and save it to a UserDataSharingConsentAudit object for later retrieval. """ self.lift_quarantine(request) # Load the linked EnterpriseCustomer for this request. customer = get_enterprise_customer_for_request(request) if customer is None: # If we can't get an EnterpriseCustomer from the pipeline, then we don't really # have enough state to do anything meaningful. Just send the user to the login # screen; if they want to sign in with an Enterprise-linked SSO, they can do # so, and the pipeline will get them back here if they need to be. return redirect('signin_user') # Attempt to retrieve a user being manipulated by the third-party auth # pipeline. Return a 404 if no such user exists. social_auth = get_real_social_auth_object(request) user = getattr(social_auth, 'user', None) if user is None: raise Http404 if not consent_provided and active_provider_enforces_data_sharing( request, EnterpriseCustomer.AT_LOGIN): # Flush the session to avoid the possibility of accidental login and to abort the pipeline. # pipeline is flushed only if data sharing is enforced, in other cases let the user to login. request.session.flush() failure_url = request.POST.get('failure_url') or reverse( 'dashboard') return redirect(failure_url) enterprise_customer_user, __ = EnterpriseCustomerUser.objects.get_or_create( user_id=user.id, enterprise_customer=customer, ) platform_name = configuration_helpers.get_value( 'PLATFORM_NAME', settings.PLATFORM_NAME) messages.success( request, _('{span_start}Account created{span_end} Thank you for creating an account with {platform_name}.' ).format( platform_name=platform_name, span_start='<span>', span_end='</span>', )) if not user.is_active: messages.info( request, _('{span_start}Activate your account{span_end} Check your inbox for an activation email. ' 'You will not be able to log back into your account until you have activated it.' ).format(span_start='<span>', span_end='</span>')) UserDataSharingConsentAudit.objects.update_or_create( user=enterprise_customer_user, defaults={ 'state': (UserDataSharingConsentAudit.ENABLED if consent_provided else UserDataSharingConsentAudit.DISABLED) }) # Resume auth pipeline backend_name = get_partial_pipeline(request).get('backend') return redirect(get_complete_url(backend_name))
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 test_complete_url_returns_expected_format(self): complete_url = pipeline.get_complete_url( self.enabled_provider.backend_name) self.assertTrue(complete_url.startswith('/auth/complete')) self.assertIn(self.enabled_provider.backend_name, complete_url)
def login_user(request): """ AJAX request to log in the user. """ post_data = request.POST.copy() # Decrypt form data if it is encrypted if 'data_token' in request.POST: data_token = request.POST.get('data_token') try: decoded_data = jwt.decode( data_token, settings.EDRAAK_LOGISTRATION_SECRET_KEY, verify=False, algorithms=[settings.EDRAAK_LOGISTRATION_SIGNING_ALGORITHM]) post_data.update(decoded_data) except jwt.ExpiredSignatureError: err_msg = u"The provided data_token has been expired" AUDIT_LOG.warning(err_msg) return JsonResponse({ "success": False, "value": err_msg, }, status=400) except jwt.DecodeError: err_msg = u"Signature verification failed" AUDIT_LOG.warning(err_msg) return JsonResponse({ "success": False, "value": err_msg, }, status=400) except (jwt.InvalidTokenError, ValueError): err_msg = u"Invalid token" AUDIT_LOG.warning(err_msg) return JsonResponse({ "success": False, "value": err_msg, }, status=400) third_party_auth_requested = third_party_auth.is_enabled( ) and pipeline.running(request) trumped_by_first_party_auth = bool(post_data.get('email')) or bool( post_data.get('password')) was_authenticated_third_party = False parent_user = None child_user = None try: if third_party_auth_requested and not trumped_by_first_party_auth: # 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: email_user = _do_third_party_auth(request) was_authenticated_third_party = True except AuthFailedError as e: return HttpResponse(e.value, content_type="text/plain", status=403) elif 'child_user_id' in post_data: child_user_id = post_data['child_user_id'] try: child_user = User.objects.get(id=child_user_id) except User.DoesNotExist: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u"Child login failed - Unknown child user id") else: AUDIT_LOG.warning( u"Child login failed - Unknown child user id: {0}". format(child_user_id)) else: email_user = _get_user_by_email(request, post_data=post_data) if child_user: parent_user = request.user email_user = child_user _check_shib_redirect(email_user) _check_excessive_login_attempts(email_user) _check_forced_password_reset(email_user) # set the user object to child_user object if a child is being logged in possibly_authenticated_user = email_user if not was_authenticated_third_party: possibly_authenticated_user = _authenticate_first_party( request, post_data, email_user) 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, post_data, possibly_authenticated_user) if possibly_authenticated_user is None or not possibly_authenticated_user.is_active: _handle_failed_authentication(email_user) _handle_successful_authentication_and_login( possibly_authenticated_user, request, post_data) if parent_user: request.session['parent_user'] = json.dumps({ 'user_id': parent_user.id, 'username': parent_user.username, 'email': parent_user.email, 'name': parent_user.profile.name }) redirect_url = None # The AJAX method calling should know the default destination upon success if was_authenticated_third_party: running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url( backend_name=running_pipeline['backend']) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. return set_logged_in_cookies(request, response, possibly_authenticated_user) except AuthFailedError as error: return JsonResponse(error.get_response())
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)