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 inactive_user_view(request): """ A newly or recently registered user has completed the social auth pipeline. Their account is not yet activated, but we let them login since the third party auth provider is trusted to vouch for them. See details in pipeline.py. The reason this view exists is that if we don't define this as the SOCIAL_AUTH_INACTIVE_USER_URL, inactive users will get sent to LOGIN_ERROR_URL, which we don't want. If the third_party_provider.skip_email_verification is set then the user is activated and verification email is not sent """ # 'next' may be set to '/account/finish_auth/.../' if this user needs to be auto-enrolled # in a course. Otherwise, just redirect them to the dashboard, which displays a message # about activating their account. user = request.user profile = UserProfile.objects.get(user=user) activated = user.is_active # If the user is registering via 3rd party auth, track which provider they use if third_party_auth.is_enabled() and pipeline.running(request): running_pipeline = pipeline.get(request) third_party_provider = provider.Registry.get_from_pipeline(running_pipeline) if third_party_provider.skip_email_verification and not activated: user.is_active = True user.save() activated = True if not activated: compose_and_send_activation_email(user, profile) return redirect(request.GET.get('next', 'dashboard'))
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 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 _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_new_account_registration_fails_if_email_exists(self): 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)) # pylint:disable-msg=protected-access self.assert_redirect_to_register_looks_correct(actions.do_complete(strategy, social_views._do_login)) self.assert_register_response_in_pipeline_looks_correct( student_views.register_user(strategy.request), pipeline.get(request)['kwargs']) strategy.request.POST = self.get_registration_post_vars() # Create twice: once successfully, and once causing a collision. student_views.create_account(strategy.request) self.assert_json_failure_response_is_username_collision(student_views.create_account(strategy.request))
def test_new_account_registration_assigns_distinct_username_on_collision(self): original_username = self.get_username() request, strategy = self.get_request_and_strategy( auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri='social:complete') # Create a colliding username in the backend, then proceed with # assignment via pipeline to make sure a distinct username is created. strategy.storage.user.create_user(username=self.get_username(), email='*****@*****.**', password='******') strategy.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) # pylint: disable-msg=protected-access self.assert_redirect_to_register_looks_correct(actions.do_complete(strategy, social_views._do_login)) distinct_username = pipeline.get(request)['kwargs']['username'] self.assertNotEqual(original_username, distinct_username)
def _link_user_to_third_party_provider( is_third_party_auth_enabled, third_party_auth_credentials_in_api, user, request, params, ): """ If a 3rd party auth provider and credentials were provided in the API, link the account with social auth (If the user is using the normal register page, the social auth pipeline does the linking, not this code) Note: this is orthogonal to the 3rd party authentication pipeline that occurs when the account is created via the browser and redirect URLs. """ third_party_provider, running_pipeline = None, None if is_third_party_auth_enabled and third_party_auth_credentials_in_api: backend_name = params['provider'] request.social_strategy = social_utils.load_strategy(request) redirect_uri = reverse('social:complete', args=(backend_name, )) request.backend = social_utils.load_backend(request.social_strategy, backend_name, redirect_uri) social_access_token = params.get('access_token') if not social_access_token: raise ValidationError({ 'access_token': [ _("An access_token is required when passing value ({}) for provider.").format( params['provider'] ) ] }) request.session[pipeline.AUTH_ENTRY_KEY] = pipeline.AUTH_ENTRY_REGISTER_API pipeline_user = None error_message = "" try: pipeline_user = request.backend.do_auth(social_access_token, user=user) except AuthAlreadyAssociated: error_message = _("The provided access_token is already associated with another user.") except (HTTPError, AuthException): error_message = _("The provided access_token is not valid.") if not pipeline_user or not isinstance(pipeline_user, User): # Ensure user does not re-enter the pipeline request.social_strategy.clean_partial_pipeline(social_access_token) raise ValidationError({'access_token': [error_message]}) # If the user is registering via 3rd party auth, track which provider they use if is_third_party_auth_enabled and pipeline.running(request): running_pipeline = pipeline.get(request) third_party_provider = provider.Registry.get_from_pipeline(running_pipeline) return third_party_provider, running_pipeline
def _do_third_party_auth(request): """ User is already authenticated via 3rd party, now try to find and return their associated Django user. """ 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) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) try: return pipeline.get_authenticated_user(requested_provider, username, third_party_uid) except User.DoesNotExist: AUDIT_LOG.info( u"Login failed - user with username {username} has no social auth " u"with backend_name {backend_name}".format( username=username, backend_name=backend_name) ) message = _( u"You've successfully logged into your {provider_name} account, " u"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 += _( u"Use your {platform_name} username and password to log into {platform_name} below, " u"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 += Text(_( u"If you don't have an {platform_name} account yet, " u"click {register_label_strong} at the top of the page." )).format( platform_name=platform_name, register_label_strong=HTML('<strong>{register_text}</strong>').format( register_text=_('Register') ) ) raise AuthFailedError(message)
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 register_user(request, extra_context=None): """ Deprecated. To be replaced by :class:`user_authn.views.login_form.login_and_registration_form`. """ # Determine the URL to redirect to following login: redirect_to = get_next_url_for_login_page(request) if request.user.is_authenticated: return redirect(redirect_to) external_auth_response = external_auth_register(request) if external_auth_response is not None: return external_auth_response context = { 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in the header 'email': '', 'name': '', 'running_pipeline': None, 'pipeline_urls': auth_pipeline_urls(pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to), 'platform_name': configuration_helpers.get_value( 'platform_name', settings.PLATFORM_NAME ), 'selected_provider': '', 'username': '', } if extra_context is not None: context.update(extra_context) if context.get("extauth_domain", '').startswith(settings.SHIBBOLETH_DOMAIN_PREFIX): return render_to_response('register-shib.html', context) # If third-party auth is enabled, prepopulate the form with data from the # selected provider. if third_party_auth.is_enabled() and pipeline.running(request): running_pipeline = pipeline.get(request) current_provider = provider.Registry.get_from_pipeline(running_pipeline) if current_provider is not None: overrides = current_provider.get_register_form_data(running_pipeline.get('kwargs')) overrides['running_pipeline'] = running_pipeline overrides['selected_provider'] = current_provider.name context.update(overrides) return render_to_response('register.html', context)
def update_third_party_auth_context_for_enterprise(request, context, enterprise_customer=None): """ Return updated context of third party auth with modified for enterprise. Arguments: request (HttpRequest): The request for the logistration page. context (dict): Context for third party auth providers and auth pipeline. enterprise_customer (dict): data for enterprise customer Returns: context (dict): Updated context of third party auth with modified `errorMessage`. """ if context['data']['third_party_auth']['errorMessage']: context['data']['third_party_auth']['errorMessage'] = Text(_( u'We are sorry, you are not authorized to access {platform_name} via this channel. ' u'Please contact your learning administrator or manager in order to access {platform_name}.' u'{line_break}{line_break}' u'Error Details:{line_break}{error_message}') ).format( platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), error_message=context['data']['third_party_auth']['errorMessage'], line_break=HTML('<br/>') ) if enterprise_customer: context['data']['third_party_auth']['providers'] = [] context['data']['third_party_auth']['secondaryProviders'] = [] 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 and current_provider.skip_registration_form and enterprise_customer: # 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. context['data']['third_party_auth']['autoSubmitRegForm'] = False context['data']['third_party_auth']['autoRegisterWelcomeMessage'] = Text(_( 'Thank you for joining {platform_name}. ' 'Just a couple steps before you start learning!') ).format( platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) ) context['data']['third_party_auth']['registerFormSubmitButtonText'] = _('Continue') 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 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) _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 _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_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
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:
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 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)) 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) # 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(strategy.request.backend, social_views._do_login, user=created_user)) # 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), created_user, linked=True)
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 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 create_account_with_params_custom(request, params): """ Given a request and a dict of parameters (which may or may not have come from the request), create an account for the requesting user, including creating a comments service user object and sending an activation email. This also takes external/third-party auth into account, updates that as necessary, and authenticates the user for the request's session. Does not return anything. Raises AccountValidationError if an account with the username or email specified by params already exists, or ValidationError if any of the given parameters is invalid for any other reason. Issues with this code: * It is not transactional. If there is a failure part-way, an incomplete account will be created and left in the database. * Third-party auth passwords are not verified. There is a comment that they are unused, but it would be helpful to have a sanity check that they are sane. * It is over 300 lines long (!) and includes disprate functionality, from registration e-mails to all sorts of other things. It should be broken up into semantically meaningful functions. * The user-facing text is rather unfriendly (e.g. "Username must be a minimum of two characters long" rather than "Please use a username of at least two characters"). """ # Copy params so we can modify it; we can't just do dict(params) because if # params is request.POST, that results in a dict containing lists of values params = dict(params.items()) # allow to define custom set of required/optional/hidden fields via configuration extra_fields = configuration_helpers.get_value( 'REGISTRATION_EXTRA_FIELDS', getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {})) # Boolean of whether a 3rd party auth provider and credentials were provided in # the API so the newly created account can link with the 3rd party account. # # Note: this is orthogonal to the 3rd party authentication pipeline that occurs # when the account is created via the browser and redirect URLs. should_link_with_social_auth = third_party_auth.is_enabled( ) and 'provider' in params if should_link_with_social_auth or (third_party_auth.is_enabled() and pipeline.running(request)): params["password"] = pipeline.make_random_password() # Add a form requirement for data sharing consent if the EnterpriseCustomer # for the request requires it at login extra_fields[ 'data_sharing_consent'] = data_sharing_consent_requirement_at_login( request) # if doing signup for an external authorization, then get email, password, name from the eamap # don't use the ones from the form, since the user could have hacked those # unless originally we didn't get a valid email or name from the external auth # TODO: We do not check whether these values meet all necessary criteria, such as email length do_external_auth = 'ExternalAuthMap' in request.session if do_external_auth: eamap = request.session['ExternalAuthMap'] try: validate_email(eamap.external_email) params["email"] = eamap.external_email except ValidationError: pass if eamap.external_name.strip() != '': params["name"] = eamap.external_name params["password"] = eamap.internal_password log.debug(u'In create_account with external_auth: user = %s, email=%s', params["name"], params["email"]) extended_profile_fields = configuration_helpers.get_value( 'extended_profile_fields', []) enforce_password_policy = (settings.FEATURES.get( "ENFORCE_PASSWORD_POLICY", False) and not do_external_auth) # Can't have terms of service for certain SHIB users, like at Stanford registration_fields = getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {}) tos_required = (registration_fields.get('terms_of_service') != 'hidden' or registration_fields.get('honor_code') != 'hidden') and ( not settings.FEATURES.get("AUTH_USE_SHIB") or not settings.FEATURES.get("SHIB_DISABLE_TOS") or not do_external_auth or not eamap.external_domain.startswith( openedx.core.djangoapps.external_auth.views. SHIBBOLETH_DOMAIN_PREFIX)) params['name'] = "{} {}".format(params.get('first_name'), params.get('last_name')) form = AccountCreationForm( data=params, extra_fields=extra_fields, extended_profile_fields=extended_profile_fields, enforce_username_neq_password=True, enforce_password_policy=enforce_password_policy, tos_required=tos_required, ) custom_form = get_registration_extension_form(data=params) # Perform operations within a transaction that are critical to account creation with transaction.atomic(): # first, create the account (user, profile, registration) = _do_create_account_custom(form, custom_form) # next, link the account with social auth, if provided via the API. # (If the user is using the normal register page, the social auth pipeline does the linking, not this code) if should_link_with_social_auth: backend_name = params['provider'] request.social_strategy = social_utils.load_strategy(request) redirect_uri = reverse('social:complete', args=(backend_name, )) request.backend = social_utils.load_backend( request.social_strategy, backend_name, redirect_uri) social_access_token = params.get('access_token') if not social_access_token: raise ValidationError({ 'access_token': [ _("An access_token is required when passing value ({}) for provider." ).format(params['provider']) ] }) request.session[ pipeline.AUTH_ENTRY_KEY] = pipeline.AUTH_ENTRY_REGISTER_API pipeline_user = None error_message = "" try: pipeline_user = request.backend.do_auth(social_access_token, user=user) except AuthAlreadyAssociated: error_message = _( "The provided access_token is already associated with another user." ) except (HTTPError, AuthException): error_message = _("The provided access_token is not valid.") if not pipeline_user or not isinstance(pipeline_user, User): # Ensure user does not re-enter the pipeline request.social_strategy.clean_partial_pipeline() raise ValidationError({'access_token': [error_message]}) # Perform operations that are non-critical parts of account creation preferences_api.set_user_preference(user, LANGUAGE_KEY, get_language()) if settings.FEATURES.get('ENABLE_DISCUSSION_EMAIL_DIGEST'): try: enable_notifications(user) except Exception: # pylint: disable=broad-except log.exception( "Enable discussion notifications failed for user {id}.".format( id=user.id)) dog_stats_api.increment("common.student.account_created") # If the user is registering via 3rd party auth, track which provider they use third_party_provider = None running_pipeline = None if third_party_auth.is_enabled() and pipeline.running(request): running_pipeline = pipeline.get(request) third_party_provider = provider.Registry.get_from_pipeline( running_pipeline) # Store received data sharing consent field values in the pipeline for use # by any downstream pipeline elements which require them. running_pipeline['kwargs'][ 'data_sharing_consent'] = form.cleaned_data.get( 'data_sharing_consent', None) # Track the user's registration if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY: tracking_context = tracker.get_tracker().resolve_context() identity_args = [ user.id, # pylint: disable=no-member { 'email': user.email, 'username': user.username, 'name': profile.name, # Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey. 'age': profile.age or -1, 'yearOfBirth': profile.year_of_birth or datetime.datetime.now(UTC).year, 'education': profile.level_of_education_display, 'address': profile.mailing_address, 'gender': profile.gender_display, 'country': unicode(profile.country), } ] if hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID'): identity_args.append( {"MailChimp": { "listId": settings.MAILCHIMP_NEW_USER_LIST_ID }}) analytics.identify(*identity_args) analytics.track( user.id, "edx.bi.user.account.registered", { 'category': 'conversion', 'label': params.get('course_id'), 'provider': third_party_provider.name if third_party_provider else None }, context={ 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } }) # Announce registration REGISTER_USER.send(sender=None, user=user, profile=profile) create_comments_service_user(user) # Don't send email if we are: # # 1. Doing load testing. # 2. Random user generation for other forms of testing. # 3. External auth bypassing activation. # 4. Have the platform configured to not require e-mail activation. # 5. Registering a new user using a trusted third party provider (with skip_email_verification=True) # # Note that this feature is only tested as a flag set one way or # the other for *new* systems. we need to be careful about # changing settings on a running system to make sure no users are # left in an inconsistent state (or doing a migration if they are). send_email = ( not settings.FEATURES.get('SKIP_EMAIL_VALIDATION', None) and not settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING') and not (do_external_auth and settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH')) and not (third_party_provider and third_party_provider.skip_email_verification and user.email == running_pipeline['kwargs'].get('details', {}).get('email'))) if send_email: send_account_activation_email(request, registration, user) else: registration.activate() _enroll_user_in_pending_courses( user) # Enroll student in any pending courses # Immediately after a user creates an account, we log them in. They are only # logged in until they close the browser. They can't log in again until they click # the activation link from the email. new_user = authenticate(username=user.username, password=params['password']) login(request, new_user) request.session.set_expiry(0) try: record_registration_attributions(request, new_user) # Don't prevent a user from registering due to attribution errors. except Exception: # pylint: disable=broad-except log.exception('Error while attributing cookies to user registration.') # TODO: there is no error checking here to see that the user actually logged in successfully, # and is not yet an active user. if new_user is not None: AUDIT_LOG.info(u"Login success on new account creation - {0}".format( new_user.username)) if do_external_auth: eamap.user = new_user eamap.dtsignup = datetime.datetime.now(UTC) eamap.save() AUDIT_LOG.info(u"User registered with external_auth %s", new_user.username) AUDIT_LOG.info(u'Updated ExternalAuthMap for %s to be %s', new_user.username, eamap) if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): log.info('bypassing activation email') new_user.is_active = True new_user.save() AUDIT_LOG.info( u"Login activated on extauth account - {0} ({1})".format( new_user.username, new_user.email)) return new_user
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): """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 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: # 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_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) # The user's account isn't created yet, so an attempt to complete the # pipeline will error out on /login: self.assert_redirect_to_login_looks_correct( actions.do_complete(strategy, social_views._do_login, user=created_user)) # So we activate the account in order to verify the redirect to /dashboard: created_user.is_active = True created_user.save() # Last step in the pipeline: we re-invoke the pipeline and expect to # end up on /dashboard, with the correct social auth object now in the # backend and the correct user's data on display. 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)
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 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())