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: send_account_activation_email(user, profile) return redirect(request.GET.get('next', 'dashboard'))
def post(self, request): """Create the user's account. You must send all required form fields with the request. You can optionally send a "course_id" param to indicate in analytics events that the user registered while enrolling in a particular course. Arguments: request (HTTPRequest) Returns: HttpResponse: 200 on success HttpResponse: 400 if the request is not valid. HttpResponse: 409 if an account with the given username or email address already exists HttpResponse: 403 operation not allowed """ should_be_rate_limited = getattr(request, 'limited', False) if should_be_rate_limited: return JsonResponse({'error_code': 'forbidden-request'}, status=403) if is_require_third_party_auth_enabled( ) and not pipeline.running(request): # if request is not running a third-party auth pipeline return HttpResponseForbidden( "Third party authentication is required to register. Username and password were received instead." ) data = request.POST.copy() self._handle_terms_of_service(data) response = self._handle_duplicate_email_username(request, data) if response: return response response, user = self._create_account(request, data) if response: return response redirect_to, root_url = get_next_url_for_login_page(request, include_host=True) redirect_url = get_redirect_url_with_host(root_url, redirect_to) response = self._create_response(request, {}, status_code=200, redirect_url=redirect_url) set_logged_in_cookies(request, response, user) if not user.is_active and settings.SHOW_ACCOUNT_ACTIVATION_CTA and not settings.MARKETING_EMAILS_OPT_IN: response.set_cookie( settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME, True, domain=settings.SESSION_COOKIE_DOMAIN, path='/', secure=request.is_secure() ) # setting the cookie to show account activation dialogue in platform and learning MFE mark_user_change_as_expected(response, user.id) return response
def _link_user_to_third_party_provider( is_third_party_auth_enabled, third_party_auth_credentials_in_api, user, request, params, ): """ If a 3rd party auth provider and credentials were provided in the API, link the account with social auth (If the user is using the normal register page, the social auth pipeline does the linking, not this code) Note: this is orthogonal to the 3rd party authentication pipeline that occurs when the account is created via the browser and redirect URLs. """ third_party_provider, running_pipeline = None, None if is_third_party_auth_enabled and third_party_auth_credentials_in_api: backend_name = params['provider'] request.social_strategy = social_utils.load_strategy(request) redirect_uri = reverse('social:complete', args=(backend_name, )) request.backend = social_utils.load_backend(request.social_strategy, backend_name, redirect_uri) social_access_token = params.get('access_token') if not social_access_token: raise ValidationError({ 'access_token': [ _(u"An access_token is required when passing value ({}) for provider." ).format(params['provider']) ] }) request.session[ pipeline.AUTH_ENTRY_KEY] = pipeline.AUTH_ENTRY_REGISTER_API pipeline_user = None error_message = "" try: pipeline_user = request.backend.do_auth(social_access_token, user=user) except AuthAlreadyAssociated: error_message = _( "The provided access_token is already associated with another user." ) except (HTTPError, AuthException): error_message = _("The provided access_token is not valid.") if not pipeline_user or not isinstance(pipeline_user, User): # Ensure user does not re-enter the pipeline request.social_strategy.clean_partial_pipeline(social_access_token) raise ValidationError({'access_token': [error_message]}) # If the user is registering via 3rd party auth, track which provider they use if is_third_party_auth_enabled and pipeline.running(request): running_pipeline = pipeline.get(request) third_party_provider = provider.Registry.get_from_pipeline( running_pipeline) return third_party_provider, running_pipeline
def post(self, request): """Create the user's account. You must send all required form fields with the request. You can optionally send a "course_id" param to indicate in analytics events that the user registered while enrolling in a particular course. Arguments: request (HTTPRequest) Returns: HttpResponse: 200 on success HttpResponse: 400 if the request is not valid. HttpResponse: 409 if an account with the given username or email address already exists HttpResponse: 403 operation not allowed """ should_be_rate_limited = getattr(request, 'limited', False) if should_be_rate_limited: return JsonResponse({'error_code': 'forbidden-request'}, status=403) if is_require_third_party_auth_enabled( ) and not pipeline.running(request): # if request is not running a third-party auth pipeline return HttpResponseForbidden( "Third party authentication is required to register. Username and password were received instead." ) data = request.POST.copy() self._handle_terms_of_service(data) response = self._handle_duplicate_email_username(request, data) if response: return response response, user = self._create_account(request, data) if response: return response redirect_to, root_url = get_next_url_for_login_page(request, include_host=True) redirect_url = get_redirect_url_with_host(root_url, redirect_to) response = self._create_response(request, {}, status_code=200, redirect_url=redirect_url) set_logged_in_cookies(request, response, user) return response
def login_user(request): """ AJAX request to log in the user. Arguments: request (HttpRequest) Required params: email, password Optional params: analytics: a JSON-encoded object with additional info to include in the login analytics event. The only supported field is "enroll_course_id" to indicate that the user logged in while enrolling in a particular course. Returns: HttpResponse: 200 if successful. Ex. {'success': true} HttpResponse: 400 if the request failed. Ex. {'success': false, 'value': '{'success': false, 'value: 'Email or password is incorrect.'} HttpResponse: 403 if successful authentication with a third party provider but does not have a linked account. Ex. {'success': false, 'error_code': 'third-party-auth-with-no-linked-account'} Example Usage: POST /login_ajax with POST params `email`, `password` 200 {'success': true} """ _parse_analytics_param_for_course_id(request) third_party_auth_requested = third_party_auth.is_enabled( ) and pipeline.running(request) first_party_auth_requested = bool(request.POST.get('email')) or bool( request.POST.get('password')) is_user_third_party_authenticated = False set_custom_attribute('login_user_course_id', request.POST.get('course_id')) try: if third_party_auth_requested and not first_party_auth_requested: # The user has already authenticated via third-party auth and has not # asked to do first party auth by supplying a username or password. We # now want to put them through the same logging and cookie calculation # logic as with first-party auth. # This nested try is due to us only returning an HttpResponse in this # one case vs. JsonResponse everywhere else. try: user = _do_third_party_auth(request) is_user_third_party_authenticated = True set_custom_attribute('login_user_tpa_success', True) except AuthFailedError as e: set_custom_attribute('login_user_tpa_success', False) set_custom_attribute('login_user_tpa_failure_msg', e.value) # user successfully authenticated with a third party provider, but has no linked Open edX account response_content = e.get_response() return JsonResponse(response_content, status=403) else: user = _get_user_by_email(request) _check_excessive_login_attempts(user) possibly_authenticated_user = user if not is_user_third_party_authenticated: possibly_authenticated_user = _authenticate_first_party( request, user, third_party_auth_requested) if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login( ): # Important: This call must be made AFTER the user was successfully authenticated. _enforce_password_policy_compliance( request, possibly_authenticated_user) if possibly_authenticated_user is None or not possibly_authenticated_user.is_active: _handle_failed_authentication(user, possibly_authenticated_user) _handle_successful_authentication_and_login( possibly_authenticated_user, request) redirect_url = None # The AJAX method calling should know the default destination upon success if is_user_third_party_authenticated: running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url( backend_name=running_pipeline['backend']) elif should_redirect_to_logistration_mircrofrontend(): redirect_url = get_next_url_for_login_page(request, include_host=True) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. response = set_logged_in_cookies(request, response, possibly_authenticated_user) set_custom_attribute('login_user_auth_failed_error', False) set_custom_attribute('login_user_response_status', response.status_code) set_custom_attribute('login_user_redirect_url', redirect_url) return response except AuthFailedError as error: response_content = error.get_response() log.exception(response_content) if response_content.get('error_code') == 'inactive-user': response_content['email'] = user.email response = JsonResponse(response_content, status=400) set_custom_attribute('login_user_auth_failed_error', True) set_custom_attribute('login_user_response_status', response.status_code) return response
def 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', {}) ) if is_registration_api_v1(request): if 'confirm_email' in extra_fields: del extra_fields['confirm_email'] # 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': [ _("Registration using {provider} has timed out.").format( provider=params.get('social_auth_provider')) ], 'error_code': 'tpa-session-expired', } ) if is_third_party_auth_enabled: set_custom_attribute('register_user_tpa', pipeline.running(request)) 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(): # 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) # Sites using multiple languages need to record the language used during registration. # If not, compose_and_send_activation_email will be sent in site's default language only. create_or_set_user_attribute_created_on_site(user, request.site) # Only add a default user preference if user does not already has one. if not preferences_api.has_user_preference(user, LANGUAGE_KEY): preferences_api.set_user_preference(user, LANGUAGE_KEY, get_language()) # 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: redirect_to, root_url = get_next_url_for_login_page(request, include_host=True) redirect_url = get_redirect_url_with_host(root_url, redirect_to) compose_and_send_activation_email(user, profile, registration, redirect_url) if settings.FEATURES.get('ENABLE_DISCUSSION_EMAIL_DIGEST'): try: enable_notifications(user) except Exception: # pylint: disable=broad-except log.exception(f"Enable discussion notifications failed for user {user.id}.") _track_user_registration(user, profile, params, third_party_provider, registration) # Announce registration REGISTER_USER.send(sender=None, user=user, registration=registration) STUDENT_REGISTRATION_COMPLETED.send_event( user=UserData( pii=UserPersonalData( username=user.username, email=user.email, name=user.profile.name, ), id=user.id, is_active=user.is_active, ), ) create_comments_service_user(user) try: _record_registration_attributions(request, new_user) _record_marketing_emails_opt_in_attribute(params.get('marketing_emails_opt_in'), 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. is_new_user(request, new_user) return new_user
def login_user(request, api_version='v1'): # pylint: disable=too-many-statements """ AJAX request to log in the user. Arguments: request (HttpRequest) Required params: email, password Optional params: analytics: a JSON-encoded object with additional info to include in the login analytics event. The only supported field is "enroll_course_id" to indicate that the user logged in while enrolling in a particular course. Returns: HttpResponse: 200 if successful. Ex. {'success': true} HttpResponse: 400 if the request failed. Ex. {'success': false, 'value': '{'success': false, 'value: 'Email or password is incorrect.'} HttpResponse: 403 if successful authentication with a third party provider but does not have a linked account. Ex. {'success': false, 'error_code': 'third-party-auth-with-no-linked-account'} Example Usage: POST /login_ajax with POST params `email`, `password` 200 {'success': true} """ _parse_analytics_param_for_course_id(request) third_party_auth_requested = third_party_auth.is_enabled( ) and pipeline.running(request) first_party_auth_requested = bool(request.POST.get('email')) or bool( request.POST.get('password')) is_user_third_party_authenticated = False set_custom_attribute('login_user_course_id', request.POST.get('course_id')) if is_require_third_party_auth_enabled( ) and not third_party_auth_requested: return HttpResponseForbidden( "Third party authentication is required to login. Username and password were received instead." ) possibly_authenticated_user = None try: if third_party_auth_requested and not first_party_auth_requested: # The user has already authenticated via third-party auth and has not # asked to do first party auth by supplying a username or password. We # now want to put them through the same logging and cookie calculation # logic as with first-party auth. # This nested try is due to us only returning an HttpResponse in this # one case vs. JsonResponse everywhere else. try: user = _do_third_party_auth(request) is_user_third_party_authenticated = True set_custom_attribute('login_user_tpa_success', True) except AuthFailedError as e: set_custom_attribute('login_user_tpa_success', False) set_custom_attribute('login_user_tpa_failure_msg', e.value) if e.error_code: set_custom_attribute('login_error_code', e.error_code) # user successfully authenticated with a third party provider, but has no linked Open edX account response_content = e.get_response() return JsonResponse(response_content, status=403) else: user = _get_user_by_email_or_username(request, api_version) _check_excessive_login_attempts(user) possibly_authenticated_user = user try: possibly_authenticated_user = StudentLoginRequested.run_filter( user=possibly_authenticated_user) except StudentLoginRequested.PreventLogin as exc: raise AuthFailedError( str(exc), redirect_url=exc.redirect_to, error_code=exc.error_code, context=exc.context, ) from exc if not is_user_third_party_authenticated: possibly_authenticated_user = _authenticate_first_party( request, user, third_party_auth_requested) if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login( ): # Important: This call must be made AFTER the user was successfully authenticated. _enforce_password_policy_compliance( request, possibly_authenticated_user) if possibly_authenticated_user is None or not ( possibly_authenticated_user.is_active or settings.MARKETING_EMAILS_OPT_IN): _handle_failed_authentication(user, possibly_authenticated_user) pwned_properties = check_pwned_password_and_send_track_event( user.id, request.POST.get('password'), user.is_staff) if not is_user_third_party_authenticated else {} # Set default for third party login password_frequency = pwned_properties.get('frequency', -1) if (settings.ENABLE_AUTHN_LOGIN_BLOCK_HIBP_POLICY and password_frequency >= settings.HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD): raise VulnerablePasswordError( accounts.AUTHN_LOGIN_BLOCK_HIBP_POLICY_MSG, 'require-password-change') _handle_successful_authentication_and_login( possibly_authenticated_user, request) # The AJAX method calling should know the default destination upon success redirect_url, finish_auth_url = None, '' if third_party_auth_requested: running_pipeline = pipeline.get(request) finish_auth_url = pipeline.get_complete_url( backend_name=running_pipeline['backend']) if is_user_third_party_authenticated: redirect_url = finish_auth_url elif should_redirect_to_authn_microfrontend(): next_url, root_url = get_next_url_for_login_page(request, include_host=True) redirect_url = get_redirect_url_with_host( root_url, enterprise_selection_page(request, possibly_authenticated_user, finish_auth_url or next_url)) if (settings.ENABLE_AUTHN_LOGIN_NUDGE_HIBP_POLICY and 0 <= password_frequency <= settings.HIBP_LOGIN_NUDGE_PASSWORD_FREQUENCY_THRESHOLD): raise VulnerablePasswordError( accounts.AUTHN_LOGIN_NUDGE_HIBP_POLICY_MSG, 'nudge-password-change', redirect_url) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. response = set_logged_in_cookies(request, response, possibly_authenticated_user) set_custom_attribute('login_user_auth_failed_error', False) set_custom_attribute('login_user_response_status', response.status_code) set_custom_attribute('login_user_redirect_url', redirect_url) mark_user_change_as_expected(user.id) return response except AuthFailedError as error: response_content = error.get_response() log.exception(response_content) error_code = response_content.get('error_code') if error_code: set_custom_attribute('login_error_code', error_code) email_or_username_key = 'email' if api_version == API_V1 else 'email_or_username' email_or_username = request.POST.get(email_or_username_key, None) email_or_username = possibly_authenticated_user.email if possibly_authenticated_user else email_or_username response_content['email'] = email_or_username except VulnerablePasswordError as error: response_content = error.get_response() log.exception(response_content) response = JsonResponse(response_content, status=400) set_custom_attribute('login_user_auth_failed_error', True) set_custom_attribute('login_user_response_status', response.status_code) return response