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 auth_pipeline_urls(auth_entry, redirect_url=None): """Retrieve URLs for each enabled third-party auth provider. These URLs are used on the "sign up" and "sign in" buttons on the login/registration forms to allow users to begin authentication with a third-party provider. Optionally, we can redirect the user to an arbitrary url after auth completes successfully. We use this to redirect the user to a page that required login, or to send users to the payment flow when enrolling in a course. Args: auth_entry (string): Either `pipeline.AUTH_ENTRY_LOGIN` or `pipeline.AUTH_ENTRY_REGISTER` Keyword Args: redirect_url (unicode): If provided, send users to this URL after they successfully authenticate. Returns: dict mapping provider IDs to URLs """ if not third_party_auth.is_enabled(): return {} return { provider.provider_id: third_party_auth.pipeline.get_login_url( provider.provider_id, auth_entry, redirect_url=redirect_url ) for provider in third_party_auth.provider.Registry.displayed_for_login() }
def _apply_third_party_auth_overrides(request, form_desc): """Modify the login form if the user has authenticated with a third-party provider. If a user has successfully authenticated with a third-party provider, and an email is associated with it then we fill in the email field with readonly property. Arguments: request (HttpRequest): The request for the registration form, used to determine if the user has successfully authenticated with a third-party provider. form_desc (FormDescription): The registration form description """ if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get(request) if running_pipeline: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) if current_provider and enterprise_customer_for_request(request): pipeline_kwargs = running_pipeline.get('kwargs') # Details about the user sent back from the provider. details = pipeline_kwargs.get('details') email = details.get('email', '') # override the email field. form_desc.override_field_properties( "email", default=email, restrictions={"readonly": "readonly"} if email else { "min_length": accounts.EMAIL_MIN_LENGTH, "max_length": accounts.EMAIL_MAX_LENGTH, } )
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 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 _apply_third_party_auth_overrides(self, request, form_desc): """Modify the registration form if the user has authenticated with a third-party provider. If a user has successfully authenticated with a third-party provider, but does not yet have an account with EdX, we want to fill in the registration form with any info that we get from the provider. This will also hide the password field, since we assign users a default (random) password on the assumption that they will be using third-party auth to log in. Arguments: request (HttpRequest): The request for the registration form, used to determine if the user has successfully authenticated with a third-party provider. form_desc (FormDescription): The registration form description """ if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get(request) if running_pipeline: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) if current_provider: # Override username / email / full name field_overrides = current_provider.get_register_form_data( running_pipeline.get('kwargs') ) for field_name in self.DEFAULT_FIELDS: if field_name in field_overrides: form_desc.override_field_properties( field_name, default=field_overrides[field_name] ) # Hide the password field form_desc.override_field_properties( "password", default="", field_type="hidden", required=False, label="", instructions="", restrictions={} ) form_desc.override_field_properties( "show_password", default=False, field_type="hidden", required=False, label="", instructions="", restrictions={} )
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 _third_party_auth_context(request): """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. Returns: dict """ context = { "currentProvider": None, "providers": [] } course_id = request.GET.get("course_id") email_opt_in = request.GET.get('email_opt_in') login_urls = auth_pipeline_urls( third_party_auth.pipeline.AUTH_ENTRY_LOGIN_2, course_id=course_id, email_opt_in=email_opt_in ) register_urls = auth_pipeline_urls( third_party_auth.pipeline.AUTH_ENTRY_REGISTER_2, course_id=course_id, email_opt_in=email_opt_in ) if third_party_auth.is_enabled(): context["providers"] = [ { "name": enabled.NAME, "iconClass": enabled.ICON_CLASS, "loginUrl": login_urls[enabled.NAME], "registerUrl": register_urls[enabled.NAME] } for enabled in third_party_auth.provider.Registry.enabled() ] running_pipeline = third_party_auth.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 return context
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 _get_profile(request): """Retrieve the user's profile information, including an HTML form that students can use to update the information. Args: request (HttpRequest) Returns: HttpResponse """ user = request.user context = { 'disable_courseware_js': True } if third_party_auth.is_enabled(): context['provider_user_states'] = third_party_auth.pipeline.get_provider_user_states(user) return render_to_response('student_profile/index.html', context)
def login(request): """Allow external auth to intercept and handle a login request. Arguments: request (Request): A request for the login page. Returns: Response or None """ # Default to a `None` response, indicating that external auth # is not handling the request. response = None if settings.IONISX_AUTH and third_party_auth.is_enabled(): # IONISx Authentication is enabled so we directly redirect # to IONISx portal redirect_to = request.GET.get('next') redirect_uri = get_login_url('oa2-portal-oauth2', AUTH_ENTRY_LOGIN, redirect_to) response = redirect(redirect_uri) elif settings.FEATURES['AUTH_USE_CERTIFICATES'] and external_auth.views.ssl_get_cert_from_request(request): # SSL login doesn't require a view, so redirect # branding and allow that to process the login if it # is enabled and the header is in the request. response = external_auth.views.redirect_with_get('root', request.GET) elif settings.FEATURES.get('AUTH_USE_CAS'): # If CAS is enabled, redirect auth handling to there response = redirect(reverse('cas-login')) elif settings.FEATURES.get('AUTH_USE_SHIB'): redirect_to = request.GET.get('next') if redirect_to: course_id = _parse_course_id_from_string(redirect_to) if course_id and _get_course_enrollment_domain(course_id): response = external_auth.views.course_specific_login(request, course_id.to_deprecated_string()) return response
def register(request): """Allow external auth to intercept and handle a registration request. Arguments: request (Request): A request for the registration page. Returns: Response or None """ response = None if settings.IONISX_AUTH and third_party_auth.is_enabled(): # IONISx Authentication is enabled so we directly redirect # to IONISx portal redirect_to = request.GET.get('next') redirect_uri = get_login_url('oa2-portal-oauth2', AUTH_ENTRY_REGISTER, redirect_to) response = redirect(redirect_uri) elif settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'): # Redirect to branding to process their certificate if SSL is enabled # and registration is disabled. response = external_auth.views.redirect_with_get('root', request.GET) return response
def _third_party_auth_context(request): """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. Returns: dict """ context = {"currentProvider": None, "providers": []} course_id = request.GET.get("course_id") email_opt_in = request.GET.get("email_opt_in") redirect_to = request.GET.get("next") # Check if the user is trying to enroll in a course # that they don't have access to based on country # access rules. # # If so, set the redirect URL to the blocked page. # We need to set it here, rather than redirecting # from within the pipeline, because a redirect # from the pipeline can prevent users # from completing the authentication process. # # Note that we can't check the user's country # profile at this point, since the user hasn't # authenticated. If the user ends up being blocked # by their country preference, we let them enroll; # they'll still be blocked when they try to access # the courseware. if course_id: try: course_key = CourseKey.from_string(course_id) redirect_url = embargo_api.redirect_if_blocked(course_key, ip_address=get_ip(request), url=request.path) if redirect_url: redirect_to = embargo_api.message_url_path(course_key, "enrollment") except InvalidKeyError: pass login_urls = auth_pipeline_urls( third_party_auth.pipeline.AUTH_ENTRY_LOGIN, course_id=course_id, email_opt_in=email_opt_in, redirect_url=redirect_to, ) register_urls = auth_pipeline_urls( third_party_auth.pipeline.AUTH_ENTRY_REGISTER, course_id=course_id, email_opt_in=email_opt_in, redirect_url=redirect_to, ) if third_party_auth.is_enabled(): context["providers"] = [ { "name": enabled.NAME, "iconClass": enabled.ICON_CLASS, "loginUrl": login_urls[enabled.NAME], "registerUrl": register_urls[enabled.NAME], } for enabled in third_party_auth.provider.Registry.enabled() ] running_pipeline = third_party_auth.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 return context
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ third_party_auth_requested = third_party_auth.is_enabled() and pipeline.running(request) if third_party_auth_requested: login_user(request) # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard if request.user.is_authenticated(): return redirect(redirect_to) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # If this is a microsite, revert to the old login/registration pages. # We need to do this for now to support existing themes. # Microsites can use the new logistration page by setting # 'ENABLE_COMBINED_LOGIN_REGISTRATION' in their microsites configuration file. if microsite.is_request_in_microsite() and not microsite.get_value('ENABLE_COMBINED_LOGIN_REGISTRATION', False): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Allow external auth to intercept and handle the request ext_auth_response = _external_auth_intercept(request, initial_mode) if ext_auth_response is not None: return ext_auth_response # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] if third_party_auth.provider.Registry.get(provider_id=provider_id): third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError): pass # Otherwise, render the combined login/registration page faq_url = None if settings.FEATURES.get('ENABLE_MKTG_SITE'): faq_url = marketing_link('FAQ') context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': settings.PLATFORM_NAME, 'faq_url': faq_url or '', # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'disable_footer': True, } return render_to_response('student_account/login_and_register.html', context)
def email_marketing_user_field_changed(sender, user=None, table=None, setting=None, old_value=None, new_value=None, **kwargs): # pylint: disable=unused-argument """ Update a single user/profile field Args: sender: Not used user: The user object for the user being changed table: The name of the table being updated setting: The name of the setting being updated old_value: Prior value new_value: New value kwargs: Not used """ # ignore anonymous users if user.is_anonymous: return # ignore anything but User, Profile or UserPreference tables if table not in {'auth_user', 'auth_userprofile', 'user_api_userpreference'}: return # ignore anything not in list of fields to handle if setting in CHANGED_FIELDNAMES: # skip if not enabled # the check has to be here rather than at the start of the method to avoid # accessing the config during migration 0001_date__add_ecommerce_service_user email_config = EmailMarketingConfiguration.current() if not email_config.enabled: return # Is the status of the user account changing to active? is_activation = (setting == 'is_active') and new_value is True # Is this change in the context of an SSO-initiated registration? third_party_provider = None if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get(crum.get_current_request()) if running_pipeline: third_party_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) # Send a welcome email if the user account is being activated # and we are not in a SSO registration flow whose associated # identity provider is configured to allow for the sending # of a welcome email. send_welcome_email = is_activation and ( third_party_provider is None or third_party_provider.send_welcome_email ) # set the activation flag when the user is marked as activated update_user.delay(_create_sailthru_user_vars(user, user.profile), user.email, site=_get_current_site(), new_user=False, activation=send_welcome_email) elif setting == 'email': # email update is special case email_config = EmailMarketingConfiguration.current() if not email_config.enabled: return update_user_email.delay(user.email, old_value)
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_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard if UserProfile.has_registered(request.user): return redirect(redirect_to) if third_party_auth.is_enabled(): force_provider_id = settings.FORCED_TPA_PROVIDER_ID if force_provider_id: force_provider = third_party_auth.provider.Registry.get( provider_id=force_provider_id, ) if force_provider and force_provider.display_for_login: running_pipeline = third_party_auth.pipeline.get(request) if not running_pipeline: if initial_mode in [pipeline.AUTH_ENTRY_LOGIN, pipeline.AUTH_ENTRY_REGISTER]: tpa_url = pipeline.get_login_url( force_provider_id, initial_mode, redirect_url=redirect_to, ) return redirect(tpa_url) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get(provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. if initial_mode == "register": auth_entry = pipeline.AUTH_ENTRY_REGISTER else: auth_entry = pipeline.AUTH_ENTRY_LOGIN return redirect( pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to) ) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError) as ex: log.error("Unknown tpa_hint provider: %s", ex) # If this is a themed site, revert to the old login/registration pages. # We need to do this for now to support existing themes. # Themed sites can use the new logistration page by setting # 'ENABLE_COMBINED_LOGIN_REGISTRATION' in their # configuration settings. if is_request_in_themed_site() and not configuration_helpers.get_value('ENABLE_COMBINED_LOGIN_REGISTRATION', False): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Allow external auth to intercept and handle the request ext_auth_response = _external_auth_intercept(request, initial_mode) if ext_auth_response is not None: return ext_auth_response # Account activation message account_activation_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags ] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)) }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER'] ), } context = update_context_for_enterprise(request, context) return render_to_response('student_account/login_and_register.html', context)
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception('Error fetching order history from Otto.') # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] beta_language = {} dark_lang_config = DarkLangConfig.current() if dark_lang_config.enable_beta_languages: user_preferences = get_user_preferences(user) pref_language = user_preferences.get('pref-lang') if pref_language in dark_lang_config.beta_languages_list: beta_language['code'] = pref_language beta_language['name'] = settings.LANGUAGE_DICT.get(pref_language) context = { 'auth': {}, 'duplicate_provider': None, 'nav_hidden': True, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': all_languages(), }, 'time_zone': { 'options': TIME_ZONE_CHOICES, } }, 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, 'order_history': user_orders, 'disable_order_history_tab': should_redirect_to_order_history_microfrontend(), 'enable_account_deletion': configuration_helpers.get_value( 'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False) ), 'extended_profile_fields': _get_extended_profile_fields(), 'beta_language': beta_language, } enterprise_customer = get_enterprise_customer_for_learner(user=request.user) update_account_settings_context_for_enterprise(context, enterprise_customer) if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [{ 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), # We only want to include providers if they are either currently available to be logged # in with, or if the user is already authenticated with them. } for state in auth_states if state.provider.display_for_login or state.has_account] return context
def login_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) 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 _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, } if third_party_auth.is_enabled(): if not enterprise_customer_for_request(request): 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: # 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 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 account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] context = { 'auth': {}, 'duplicate_provider': None, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': settings.ALL_LANGUAGES, } }, 'platform_name': settings.PLATFORM_NAME, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [{ 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states] return context
def login_and_registration_form(request, initial_mode="login"): """Render the combined login/registration form, defaulting to login This relies on the JS to asynchronously load the actual form from the user_api. Keyword Args: initial_mode (string): Either "login" or "register". """ # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) # If we're already logged in, redirect to the dashboard if UserProfile.has_registered(request.user): return redirect(redirect_to) if third_party_auth.is_enabled(): force_provider_id = settings.FORCED_TPA_PROVIDER_ID if force_provider_id: force_provider = third_party_auth.provider.Registry.get( provider_id=force_provider_id, ) if force_provider and force_provider.display_for_login: running_pipeline = third_party_auth.pipeline.get(request) if not running_pipeline: if initial_mode in [ pipeline.AUTH_ENTRY_LOGIN, pipeline.AUTH_ENTRY_REGISTER ]: tpa_url = pipeline.get_login_url( force_provider_id, initial_mode, redirect_url=redirect_to, ) return redirect(tpa_url) # Retrieve the form descriptions from the user API form_descriptions = _get_form_descriptions(request) # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. # If present, we display a login page focused on third-party auth with that provider. third_party_auth_hint = None if '?' in redirect_to: try: next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) provider_id = next_args['tpa_hint'][0] tpa_hint_provider = third_party_auth.provider.Registry.get( provider_id=provider_id) if tpa_hint_provider: if tpa_hint_provider.skip_hinted_login_dialog: # Forward the user directly to the provider's login URL when the provider is configured # to skip the dialog. if initial_mode == "register": auth_entry = pipeline.AUTH_ENTRY_REGISTER else: auth_entry = pipeline.AUTH_ENTRY_LOGIN return redirect( pipeline.get_login_url(provider_id, auth_entry, redirect_url=redirect_to)) third_party_auth_hint = provider_id initial_mode = "hinted_login" except (KeyError, ValueError, IndexError) as ex: log.error("Unknown tpa_hint provider: %s", ex) # If this is a themed site, revert to the old login/registration pages. # We need to do this for now to support existing themes. # Themed sites can use the new logistration page by setting # 'ENABLE_COMBINED_LOGIN_REGISTRATION' in their # configuration settings. if is_request_in_themed_site() and not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION', False): if initial_mode == "login": return old_login_view(request) elif initial_mode == "register": return old_register_view(request) # Allow external auth to intercept and handle the request ext_auth_response = _external_auth_intercept(request, initial_mode) if ext_auth_response is not None: return ext_auth_response # Account activation message account_activation_messages = [{ 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-activation' in message.tags] # Otherwise, render the combined login/registration page context = { 'data': { 'login_redirect_url': redirect_to, 'initial_mode': initial_mode, 'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint), 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), 'password_reset_support_link': configuration_helpers.get_value( 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK, 'account_activation_messages': account_activation_messages, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, # but we include them in the initial page load to avoid # the additional round-trip to the server. 'login_form_desc': json.loads(form_descriptions['login']), 'registration_form_desc': json.loads(form_descriptions['registration']), 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), 'account_creation_allowed': configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)) }, 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, 'combined_login_and_register': True, 'disable_footer': not configuration_helpers.get_value( 'ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER', settings.FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION_FOOTER']), } context = update_context_for_enterprise(request, context) return render_to_response('student_account/login_and_register.html', context)
def email_marketing_user_field_changed(sender, user=None, table=None, setting=None, old_value=None, new_value=None, **kwargs): # pylint: disable=unused-argument """ Update a single user/profile field Args: sender: Not used user: The user object for the user being changed table: The name of the table being updated setting: The name of the setting being updated old_value: Prior value new_value: New value kwargs: Not used """ # ignore anonymous users if user.is_anonymous: return # ignore anything but User, Profile or UserPreference tables if table not in { 'auth_user', 'auth_userprofile', 'user_api_userpreference' }: return # ignore anything not in list of fields to handle if setting in CHANGED_FIELDNAMES: # skip if not enabled # the check has to be here rather than at the start of the method to avoid # accessing the config during migration 0001_date__add_ecommerce_service_user email_config = EmailMarketingConfiguration.current() if not email_config.enabled: return # Is the status of the user account changing to active? is_activation = (setting == 'is_active') and new_value is True # Is this change in the context of an SSO-initiated registration? third_party_provider = None if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get( crum.get_current_request()) if running_pipeline: third_party_provider = third_party_auth.provider.Registry.get_from_pipeline( running_pipeline) # Send a welcome email if the user account is being activated # and we are not in a SSO registration flow whose associated # identity provider is configured to allow for the sending # of a welcome email. send_welcome_email = is_activation and ( third_party_provider is None or third_party_provider.send_welcome_email) # set the activation flag when the user is marked as activated update_user.delay(_create_sailthru_user_vars(user, user.profile), user.email, site=_get_current_site(), new_user=False, activation=send_welcome_email) elif setting == 'email': # email update is special case email_config = EmailMarketingConfiguration.current() if not email_config.enabled: return update_user_email.delay(user.email, old_value)
def create_account_with_params(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 non-transactional except where explicitly wrapped in atomic to alleviate deadlocks and improve performance. This means failures at different places in registration can leave users in inconsistent states. * 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. * 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"). * Duplicate email raises a ValidationError (rather than the expected AccountValidationError). Duplicate username returns an inconsistent user message (i.e. "An account with the Public Username '{username}' already exists." rather than "It looks like {username} belongs to an existing account. Try again with a different username.") The two checks occur at different places in the code; as a result, registering with both a duplicate username and email raises only a ValidationError for email only. """ # 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(list(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', {})) # registration via third party (Google, Facebook) using mobile application # doesn't use social auth pipeline (no redirect uri(s) etc involved). # In this case all related info (required for account linking) # is sent in params. # `third_party_auth_credentials_in_api` essentially means 'request # is made from mobile application' third_party_auth_credentials_in_api = 'provider' in params is_third_party_auth_enabled = third_party_auth.is_enabled() if is_third_party_auth_enabled and (pipeline.running(request) or third_party_auth_credentials_in_api): params["password"] = generate_password() # in case user is registering via third party (Google, Facebook) and pipeline has expired, show appropriate # error message if is_third_party_auth_enabled and ('social_auth_provider' in params and not pipeline.running(request)): raise ValidationError({ 'session_expired': [ _(u"Registration using {provider} has timed out.").format( provider=params.get('social_auth_provider')) ] }) extended_profile_fields = configuration_helpers.get_value( 'extended_profile_fields', []) # 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') form = AccountCreationForm( data=params, extra_fields=extra_fields, extended_profile_fields=extended_profile_fields, do_third_party_auth=False, tos_required=tos_required, ) custom_form = get_registration_extension_form(data=params) # Perform operations within a transaction that are critical to account creation with outer_atomic(read_committed=True): # first, create the account (user, profile, registration) = do_create_account(form, custom_form) third_party_provider, running_pipeline = _link_user_to_third_party_provider( is_third_party_auth_enabled, third_party_auth_credentials_in_api, user, request, params, ) new_user = authenticate_new_user(request, user.username, form.cleaned_data['password']) django_login(request, new_user) request.session.set_expiry(0) # Check if system is configured to skip activation email for the current user. skip_email = _skip_activation_email( user, running_pipeline, third_party_provider, ) if skip_email: registration.activate() else: compose_and_send_activation_email(user, profile, registration) # Perform operations that are non-critical parts of account creation create_or_set_user_attribute_created_on_site(user, request.site) 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( u"Enable discussion notifications failed for user {id}.". format(id=user.id)) _track_user_registration(user, profile, params, third_party_provider) # Announce registration REGISTER_USER.send(sender=None, user=user, registration=registration) create_comments_service_user(user) 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)) return new_user
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] context = { "auth": {}, "duplicate_provider": None, "fields": { "country": {"options": list(countries)}, "gender": { "options": [ (choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES ] # pylint: disable=translation-of-non-string }, "language": {"options": released_languages()}, "level_of_education": { "options": [ (choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES ] # pylint: disable=translation-of-non-string }, "password": {"url": reverse("password_reset")}, "year_of_birth": {"options": year_of_birth_options}, "preferred_language": {"options": settings.ALL_LANGUAGES}, }, "platform_name": settings.PLATFORM_NAME, "user_accounts_api_url": reverse("accounts_api", kwargs={"username": user.username}), "user_preferences_api_url": reverse("preferences_api", kwargs={"username": user.username}), "disable_courseware_js": True, } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context["duplicate_provider"] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context["auth"]["providers"] = [ { "id": state.provider.provider_id, "name": state.provider.name, # The name of the provider e.g. Facebook "connected": state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. "connect_url": pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse("account_settings"), ), "accepts_logins": state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. "disconnect_url": pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states ] return context
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user country_options = [ (country_code, _(country_name)) # pylint: disable=translation-of-non-string for country_code, country_name in sorted(countries.countries, key=lambda (__, name): unicode(name)) ] year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] context = { "auth": {}, "duplicate_provider": None, "fields": { "country": {"options": country_options}, "gender": { "options": [ (choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES ] # pylint: disable=translation-of-non-string }, "language": {"options": released_languages()}, "level_of_education": { "options": [ (choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES ] # pylint: disable=translation-of-non-string }, "password": {"url": reverse("password_reset")}, "year_of_birth": {"options": year_of_birth_options}, "preferred_language": {"options": settings.ALL_LANGUAGES}, }, "platform_name": settings.PLATFORM_NAME, "user_accounts_api_url": reverse("accounts_api", kwargs={"username": user.username}), "user_preferences_api_url": reverse("preferences_api", kwargs={"username": user.username}), } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context["duplicate_provider"] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context["auth"]["providers"] = [ { "name": state.provider.NAME, # The name of the provider e.g. Facebook "connected": state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider. "connect_url": pipeline.get_login_url( state.provider.NAME, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse("account_settings"), ), # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. "disconnect_url": pipeline.get_disconnect_url(state.provider.NAME), } for state in auth_states ] try: external_auth_map = ExternalAuthMap.objects.get(user=user) except: external_auth_map = None context["is_shib_auth"] = "shib" in external_auth_map.external_domain if external_auth_map else False return context
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(six.text_type(year), six.text_type(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception('Error fetching order history from Otto.') # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] beta_language = {} dark_lang_config = DarkLangConfig.current() if dark_lang_config.enable_beta_languages: user_preferences = get_user_preferences(user) pref_language = user_preferences.get('pref-lang') if pref_language in dark_lang_config.beta_languages_list: beta_language['code'] = pref_language beta_language['name'] = settings.LANGUAGE_DICT.get(pref_language) context = { 'auth': {}, 'duplicate_provider': None, 'nav_hidden': True, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': all_languages(), }, 'time_zone': { 'options': TIME_ZONE_CHOICES, } }, 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'password_reset_support_link': configuration_helpers.get_value('PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, 'order_history': user_orders, 'disable_order_history_tab': should_redirect_to_order_history_microfrontend(), 'enable_account_deletion': configuration_helpers.get_value( 'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False)), 'extended_profile_fields': _get_extended_profile_fields(), 'beta_language': beta_language, } enterprise_customer = enterprise_customer_for_request(request) update_account_settings_context_for_enterprise(context, enterprise_customer, user) if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider( messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [ { 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state. has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), # We only want to include providers if they are either currently available to be logged # in with, or if the user is already authenticated with them. } for state in auth_states if state.provider.display_for_login or state.has_account ] # Append/Override the existing view context values with plugin defined values run_extension_point( 'NAU_STUDENT_ACCOUNT_CONTEXT_EXTENSION', context=context, request=request, user=user, ) return context
def _third_party_auth_context(request, redirect_to, tpa_hint=None): """Context for third party auth providers and the currently running pipeline. Arguments: request (HttpRequest): The request, used to determine if a pipeline is currently running. redirect_to: The URL to send the user to following successful authentication. tpa_hint (string): An override flag that will return a matching provider as long as its configuration has been enabled Returns: dict """ context = { "currentProvider": None, "providers": [], "secondaryProviders": [], "finishAuthUrl": None, "errorMessage": None, "registerFormSubmitButtonText": _("Create Account"), "syncLearnerProfileData": False, "pipeline_user_details": {} } if third_party_auth.is_enabled(): for enabled in third_party_auth.provider.Registry.displayed_for_login(tpa_hint=tpa_hint): info = { "id": enabled.provider_id, "name": enabled.name, "iconClass": enabled.icon_class or None, "iconImage": enabled.icon_image.url if enabled.icon_image else None, "loginUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } context["providers" if not enabled.secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) user_details = running_pipeline['kwargs']['details'] if user_details: context['pipeline_user_details'] = user_details if current_provider is not None: context["currentProvider"] = current_provider.name context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name) context["syncLearnerProfileData"] = current_provider.sync_learner_profile_data if current_provider.skip_registration_form: # As a reliable way of "skipping" the registration form, we just submit it automatically context["autoSubmitRegForm"] = True # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": # msg may or may not be translated. Try translating [again] in case we are able to: context['errorMessage'] = _(unicode(msg)) break return context
def _apply_third_party_auth_overrides(self, request, form_desc): """Modify the registration form if the user has authenticated with a third-party provider. If a user has successfully authenticated with a third-party provider, but does not yet have an account with EdX, we want to fill in the registration form with any info that we get from the provider. This will also hide the password field, since we assign users a default (random) password on the assumption that they will be using third-party auth to log in. Arguments: request (HttpRequest): The request for the registration form, used to determine if the user has successfully authenticated with a third-party provider. form_desc (FormDescription): The registration form description """ if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get(request) if running_pipeline: current_provider = third_party_auth.provider.Registry.get_from_pipeline( running_pipeline) if current_provider: # Override username / email / full name field_overrides = current_provider.get_register_form_data( running_pipeline.get('kwargs')) # When the TPA Provider is configured to skip the registration form and we are in an # enterprise context, we need to hide all fields except for terms of service and # ensure that the user explicitly checks that field. hide_registration_fields_except_tos = ( (current_provider.skip_registration_form and enterprise_customer_for_request(request)) or current_provider.sync_learner_profile_data) for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS: if field_name in field_overrides: form_desc.override_field_properties( field_name, default=field_overrides[field_name]) if (field_name not in ['terms_of_service', 'honor_code'] and field_overrides[field_name] and hide_registration_fields_except_tos): form_desc.override_field_properties( field_name, field_type="hidden", label="", instructions="", ) # Hide the password field form_desc.override_field_properties("password", default="", field_type="hidden", required=False, label="", instructions="", restrictions={}) # used to identify that request is running third party social auth form_desc.add_field( "social_auth_provider", field_type="hidden", label="", default=current_provider.name if current_provider.name else "Third Party", required=False, )
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_excessive_login_attempts(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, 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 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 account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception('Error fetching order history from Otto.') # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] context = { 'auth': {}, 'duplicate_provider': None, 'nav_hidden': True, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': all_languages(), }, 'time_zone': { 'options': TIME_ZONE_CHOICES, } }, 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'password_reset_support_link': configuration_helpers.get_value('PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK, 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'order_history': user_orders } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider( messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [ { 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state. has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), # We only want to include providers if they are either currently available to be logged # in with, or if the user is already authenticated with them. } for state in auth_states if state.provider.display_for_login or state.has_account ] return context
def account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception("Error fetching order history from Otto.") # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] context = { "auth": {}, "duplicate_provider": None, "fields": { "country": {"options": list(countries)}, "gender": { "options": [ (choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES ] # pylint: disable=translation-of-non-string }, "language": {"options": released_languages()}, "level_of_education": { "options": [ (choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES ] # pylint: disable=translation-of-non-string }, "password": {"url": reverse("password_reset")}, "year_of_birth": {"options": year_of_birth_options}, "preferred_language": {"options": all_languages()}, "time_zone": { "options": TIME_ZONE_CHOICES, "enabled": settings.FEATURES.get("ENABLE_TIME_ZONE_PREFERENCE"), }, }, "platform_name": get_themed_value("PLATFORM_NAME", settings.PLATFORM_NAME), "user_accounts_api_url": reverse("accounts_api", kwargs={"username": user.username}), "user_preferences_api_url": reverse("preferences_api", kwargs={"username": user.username}), "disable_courseware_js": True, "show_program_listing": ProgramsApiConfig.current().show_program_listing, "order_history": user_orders, } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context["duplicate_provider"] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context["auth"]["providers"] = [ { "id": state.provider.provider_id, "name": state.provider.name, # The name of the provider e.g. Facebook "connected": state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. "connect_url": pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse("account_settings"), ), "accepts_logins": state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. "disconnect_url": pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states ] return context
def create_account_with_params(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 non-transactional except where explicitly wrapped in atomic to alleviate deadlocks and improve performance. This means failures at different places in registration can leave users in inconsistent states. * 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. * 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"). * Duplicate email raises a ValidationError (rather than the expected AccountValidationError). Duplicate username returns an inconsistent user message (i.e. "An account with the Public Username '{username}' already exists." rather than "It looks like {username} belongs to an existing account. Try again with a different username.") The two checks occur at different places in the code; as a result, registering with both a duplicate username and email raises only a ValidationError for email only. """ # 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', {}) ) # registration via third party (Google, Facebook) using mobile application # doesn't use social auth pipeline (no redirect uri(s) etc involved). # In this case all related info (required for account linking) # is sent in params. # `third_party_auth_credentials_in_api` essentially means 'request # is made from mobile application' third_party_auth_credentials_in_api = 'provider' in params is_third_party_auth_enabled = third_party_auth.is_enabled() if is_third_party_auth_enabled and (pipeline.running(request) or third_party_auth_credentials_in_api): params["password"] = generate_password() # in case user is registering via third party (Google, Facebook) and pipeline has expired, show appropriate # error message if is_third_party_auth_enabled and ('social_auth_provider' in params and not pipeline.running(request)): raise ValidationError( {'session_expired': [ _(u"Registration using {provider} has timed out.").format( provider=params.get('social_auth_provider')) ]} ) do_external_auth, eamap = pre_account_creation_external_auth(request, params) extended_profile_fields = configuration_helpers.get_value('extended_profile_fields', []) # 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(settings.SHIBBOLETH_DOMAIN_PREFIX) ) form = AccountCreationForm( data=params, extra_fields=extra_fields, extended_profile_fields=extended_profile_fields, do_third_party_auth=do_external_auth, tos_required=tos_required, ) custom_form = get_registration_extension_form(data=params) # Perform operations within a transaction that are critical to account creation with outer_atomic(read_committed=True): # first, create the account (user, profile, registration) = do_create_account(form, custom_form) third_party_provider, running_pipeline = _link_user_to_third_party_provider( is_third_party_auth_enabled, third_party_auth_credentials_in_api, user, request, params, ) new_user = authenticate_new_user(request, user.username, params['password']) django_login(request, new_user) request.session.set_expiry(0) post_account_creation_external_auth(do_external_auth, eamap, new_user) # Check if system is configured to skip activation email for the current user. skip_email = _skip_activation_email( user, do_external_auth, running_pipeline, third_party_provider, ) if skip_email: registration.activate() else: compose_and_send_activation_email(user, profile, registration) # Perform operations that are non-critical parts of account creation create_or_set_user_attribute_created_on_site(user, request.site) 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") _track_user_registration(user, profile, params, third_party_provider) # Announce registration REGISTER_USER.send(sender=None, user=user, registration=registration) create_comments_service_user(user) 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)) return new_user
def _apply_third_party_auth_overrides(self, request, form_desc): """Modify the registration form if the user has authenticated with a third-party provider. If a user has successfully authenticated with a third-party provider, but does not yet have an account with EdX, we want to fill in the registration form with any info that we get from the provider. This will also hide the password field, since we assign users a default (random) password on the assumption that they will be using third-party auth to log in. Arguments: request (HttpRequest): The request for the registration form, used to determine if the user has successfully authenticated with a third-party provider. form_desc (FormDescription): The registration form description """ if third_party_auth.is_enabled(): running_pipeline = third_party_auth.pipeline.get(request) if running_pipeline: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) if current_provider: # Override username / email / full name field_overrides = current_provider.get_register_form_data( running_pipeline.get('kwargs') ) # When the TPA Provider is configured to skip the registration form and we are in an # enterprise context, we need to hide all fields except for terms of service and # ensure that the user explicitly checks that field. hide_registration_fields_except_tos = ( ( current_provider.skip_registration_form and enterprise_customer_for_request(request) ) or current_provider.sync_learner_profile_data ) for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS: if field_name in field_overrides: form_desc.override_field_properties( field_name, default=field_overrides[field_name] ) if (field_name not in ['terms_of_service', 'honor_code'] and field_overrides[field_name] and hide_registration_fields_except_tos): form_desc.override_field_properties( field_name, field_type="hidden", label="", instructions="", ) # Hide the password field form_desc.override_field_properties( "password", default="", field_type="hidden", required=False, label="", instructions="", restrictions={} ) # used to identify that request is running third party social auth form_desc.add_field( "social_auth_provider", field_type="hidden", label="", default=current_provider.name if current_provider.name else "Third Party", required=False, )
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']) elif settings.FEATURES.get('ENABLE_LOGIN_MICROFRONTEND'): redirect_url = get_next_url_for_login_page(request) 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 account_settings_context(request): """ Context for the account settings page. Args: request: The request object. Returns: dict """ user = request.user year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] try: user_orders = get_user_orders(user) except: # pylint: disable=bare-except log.exception('Error fetching order history from Otto.') # Return empty order list as account settings page expect a list and # it will be broken if exception raised user_orders = [] context = { 'auth': {}, 'duplicate_provider': None, 'fields': { 'country': { 'options': list(countries), }, 'gender': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string }, 'language': { 'options': released_languages(), }, 'level_of_education': { 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string }, 'password': { 'url': reverse('password_reset'), }, 'year_of_birth': { 'options': year_of_birth_options, }, 'preferred_language': { 'options': all_languages(), }, 'time_zone': { 'options': UserPreference.TIME_ZONE_CHOICES, 'enabled': settings.FEATURES.get('ENABLE_TIME_ZONE_PREFERENCE'), } }, 'platform_name': get_themed_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.current().show_program_listing, 'order_history': user_orders } if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, # we display a message to the user. context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request)) auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [{ 'id': state.provider.provider_id, 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states] return context
def 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())