def test_next_failures(self, log_level, log_name, unsafe_url, http_accept, user_agent, expected_log): """ Test unsafe next parameter """ with LogCapture(LOGGER_NAME, level=log_level) as logger: req = self.request.get(settings.LOGIN_URL + f"?next={unsafe_url}") req.META["HTTP_ACCEPT"] = http_accept req.META["HTTP_USER_AGENT"] = user_agent get_next_url_for_login_page(req) logger.check((LOGGER_NAME, log_name, expected_log))
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 """ 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_url = get_next_url_for_login_page(request, include_host=True) response = self._create_response(request, {}, status_code=200, redirect_url=redirect_url) set_logged_in_cookies(request, response, user) return response
def test_safe_next(self, next_url, http_accept, host): """ Test safe next parameter """ req = self.request.get(settings.LOGIN_URL + f"?next={next_url}", HTTP_HOST=host) req.META["HTTP_ACCEPT"] = http_accept next_page = get_next_url_for_login_page(req) assert next_page == next_url
def get(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused-argument """ Return either a redirect to the login page of an identity provider that corresponds to the provider_slug keyword argument or a 404 if the provider_slug does not correspond to an identity provider. Args: request (HttpRequest) Keyword Args: provider_slug (str): a slug corresponding to a configured identity provider Returns: HttpResponse: 302 to a provider's login url if the provider_slug kwarg matches an identity provider HttpResponse: 404 if the provider_slug kwarg does not match an identity provider """ # this gets the url to redirect to after login/registration/third_party_auth # it also handles checking the safety of the redirect url (next query parameter) # it checks against settings.LOGIN_REDIRECT_WHITELIST, so be sure to add the url # to this setting next_destination_url = get_next_url_for_login_page(request) try: url = pipeline.get_login_url(kwargs['provider_slug'], pipeline.AUTH_ENTRY_LOGIN, next_destination_url) return redirect(url) except ValueError: return HttpResponseNotFound()
def test_safe_next(self, next_url, http_accept, host): """ Test safe next parameter """ req = self.request.get(settings.LOGIN_URL + "?next={url}".format(url=next_url), HTTP_HOST=host) req.META["HTTP_ACCEPT"] = http_accept next_page = get_next_url_for_login_page(req) self.assertEqual(next_page, next_url)
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 test_custom_redirect_url(self, redirect, expected_url): """ Test custom redirect after login """ configuration_values = {"DEFAULT_REDIRECT_AFTER_LOGIN": redirect} req = self.request.get(settings.LOGIN_URL) req.META["HTTP_ACCEPT"] = "text/html" with with_site_configuration_context(configuration=configuration_values): next_page = get_next_url_for_login_page(req) assert next_page == expected_url
def validate_login(): """ Assert that get_next_url_for_login_page returns as expected. """ if method == 'GET': req = self.request.get(settings.LOGIN_URL + "?next={url}".format(url=next_url)) elif method == 'POST': req = self.request.post(settings.LOGIN_URL, {'next': next_url}) req.META["HTTP_ACCEPT"] = "text/html" self._add_session(req) next_page = get_next_url_for_login_page(req) assert next_page == expected_url
def get_provider_login_url(request, provider_id, redirect_url=None): """ Return the given provider's login URL. This method is here to avoid the importing of pipeline and student app in enterprise. """ provider_login_url = third_party_auth.pipeline.get_login_url( provider_id, third_party_auth.pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_url if redirect_url else get_next_url_for_login_page(request)) return provider_login_url
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 process_exception(self, request, exception): """Handles specific exception raised by Python Social Auth eg HTTPError.""" referer_url = request.META.get('HTTP_REFERER', '') if (referer_url and isinstance(exception, HTTPError) and exception.response.status_code == 502): referer_url = six.moves.urllib.parse.urlparse(referer_url).path if referer_url == reverse('signin_user'): messages.error(request, _('Unable to connect with the external provider, please try again'), extra_tags='social-auth') redirect_url = get_next_url_for_login_page(request) return redirect('/login?next=' + redirect_url) return super().process_exception(request, exception)
def get(self, request, **kwargs): # lint-amnesty, pylint: disable=unused-argument """ Returns the 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. tpa_hint (string): An override flag that will return a matching provider as long as its configuration has been enabled """ request_params = request.GET redirect_to = get_next_url_for_login_page(request) third_party_auth_hint = request_params.get('tpa_hint') context = third_party_auth_context(request, redirect_to, third_party_auth_hint) return Response( status=status.HTTP_200_OK, data=context )
def test_http_exception_redirection(self): """ Test ExceptionMiddleware is correctly redirected to login page when PSA raises HttpError exception. """ request = RequestFactory().get("dummy_url") next_url = get_next_url_for_login_page(request) login_url = '/login?next=' + next_url request.META['HTTP_REFERER'] = 'http://example.com:8000/login' exception = HTTPError() exception.response = HttpResponse(status=502) # Add error message for error in auth pipeline MessageMiddleware().process_request(request) response = ExceptionMiddleware().process_exception(request, exception) target_url = response.url self.assertEqual(response.status_code, 302) self.assertTrue(target_url.endswith(login_url))
def activate_account(request, key): """ When link in activation e-mail is clicked """ # If request is in Studio call the appropriate view if theming_helpers.get_project_root_name().lower() == 'cms': monitoring_utils.set_custom_attribute('student_activate_account', 'cms') return activate_account_studio(request, key) # TODO: Use custom attribute to determine if there are any `activate_account` calls for cms in Production. # If not, the templates wouldn't be needed for cms, but we still need a way to activate for cms tests. monitoring_utils.set_custom_attribute('student_activate_account', 'lms') activation_message_type = None activated_or_confirmed = 'confirmed' if settings.MARKETING_EMAILS_OPT_IN else 'activated' account_or_email = 'email' if settings.MARKETING_EMAILS_OPT_IN else 'account' invalid_message = HTML( _('{html_start}Your {account_or_email} could not be {activated_or_confirmed}{html_end}' 'Something went wrong, please <a href="{support_url}">contact support</a> to resolve this issue.' )).format( account_or_email=account_or_email, activated_or_confirmed=activated_or_confirmed, support_url=configuration_helpers.get_value( 'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK, html_start=HTML('<p class="message-title">'), html_end=HTML('</p>'), ) show_account_activation_popup = None try: registration = Registration.objects.get(activation_key=key) except (Registration.DoesNotExist, Registration.MultipleObjectsReturned): activation_message_type = 'error' messages.error(request, invalid_message, extra_tags='account-activation aa-icon') else: if request.user.is_authenticated and request.user.id != registration.user.id: activation_message_type = 'error' messages.error(request, invalid_message, extra_tags='account-activation aa-icon') elif registration.user.is_active: activation_message_type = 'info' messages.info( request, HTML( _('{html_start}This {account_or_email} has already been {activated_or_confirmed}.{html_end}' )).format( account_or_email=account_or_email, activated_or_confirmed=activated_or_confirmed, html_start=HTML('<p class="message-title">'), html_end=HTML('</p>'), ), extra_tags='account-activation aa-icon', ) else: registration.activate() # Success message for logged in users. message = _( '{html_start}Success{html_end} You have {activated_or_confirmed} your {account_or_email}.' ) tracker.emit( USER_ACCOUNT_ACTIVATED, { "user_id": registration.user.id, "activation_timestamp": registration.activation_timestamp }) if not request.user.is_authenticated: # Success message for logged out users message = _( '{html_start}Success! You have {activated_or_confirmed} your {account_or_email}.{html_end}' 'You will now receive email updates and alerts from us related to' ' the courses you are enrolled in. Sign In to continue.') # Add message for later use. activation_message_type = 'success' messages.success( request, HTML(message).format( account_or_email=account_or_email, activated_or_confirmed=activated_or_confirmed, html_start=HTML('<p class="message-title">'), html_end=HTML('</p>'), ), extra_tags='account-activation aa-icon', ) show_account_activation_popup = request.COOKIES.get( settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME, None) # If a safe `next` parameter is provided in the request # and it's not the same as the dashboard, redirect there. # The `get_next_url_for_login_page()` function will only return a safe redirect URL. # If the provided `next` URL is not safe, that function will fill `redirect_to` # with a value of `reverse('dashboard')`. redirect_url = None if request.GET.get('next'): redirect_to, root_login_url = get_next_url_for_login_page( request, include_host=True) # Don't automatically redirect authenticated users to the redirect_url # if the `next` value is either: # 1. "/dashboard" or # 2. "https://{LMS_ROOT_URL}/dashboard" (which we might provide as a value from the AuthN MFE) if redirect_to not in (root_login_url + reverse('dashboard'), reverse('dashboard')): redirect_url = get_redirect_url_with_host(root_login_url, redirect_to) if should_redirect_to_authn_microfrontend( ) and not request.user.is_authenticated: params = {'account_activation_status': activation_message_type} if redirect_url: params['next'] = redirect_url url_path = '/login?{}'.format(urllib.parse.urlencode(params)) return redirect(settings.AUTHN_MICROFRONTEND_URL + url_path) response = redirect( redirect_url) if redirect_url and is_enterprise_learner( request.user) else redirect('dashboard') if show_account_activation_popup: response.delete_cookie( settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME, domain=settings.SESSION_COOKIE_DOMAIN, path='/', ) return response
def activate_account(request, key): """ When link in activation e-mail is clicked """ # If request is in Studio call the appropriate view if theming_helpers.get_project_root_name().lower() == 'cms': monitoring_utils.set_custom_attribute('student_activate_account', 'cms') return activate_account_studio(request, key) # TODO: Use custom attribute to determine if there are any `activate_account` calls for cms in Production. # If not, the templates wouldn't be needed for cms, but we still need a way to activate for cms tests. monitoring_utils.set_custom_attribute('student_activate_account', 'lms') activation_message_type = None invalid_message = HTML(_( '{html_start}Your account could not be activated{html_end}' 'Something went wrong, please <a href="{support_url}">contact support</a> to resolve this issue.' )).format( support_url=configuration_helpers.get_value( 'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK, html_start=HTML('<p class="message-title">'), html_end=HTML('</p>'), ) try: registration = Registration.objects.get(activation_key=key) except (Registration.DoesNotExist, Registration.MultipleObjectsReturned): activation_message_type = 'error' messages.error( request, invalid_message, extra_tags='account-activation aa-icon' ) else: if request.user.is_authenticated and request.user.id != registration.user.id: activation_message_type = 'error' messages.error( request, invalid_message, extra_tags='account-activation aa-icon' ) elif registration.user.is_active: activation_message_type = 'info' messages.info( request, HTML(_('{html_start}This account has already been activated.{html_end}')).format( html_start=HTML('<p class="message-title">'), html_end=HTML('</p>'), ), extra_tags='account-activation aa-icon', ) else: registration.activate() # Success message for logged in users. message = _('{html_start}Success{html_end} You have activated your account.') tracker.emit( USER_ACCOUNT_ACTIVATED, { "user_id": registration.user.id, "activation_timestamp": registration.activation_timestamp } ) if not request.user.is_authenticated: # Success message for logged out users message = _( '{html_start}Success! You have activated your account.{html_end}' 'You will now receive email updates and alerts from us related to' ' the courses you are enrolled in. Sign In to continue.' ) # Add message for later use. activation_message_type = 'success' messages.success( request, HTML(message).format( html_start=HTML('<p class="message-title">'), html_end=HTML('</p>'), ), extra_tags='account-activation aa-icon', ) # If a (safe) `next` parameter is provided in the request # and it's not the same as the dashboard, redirect there. # The `get_next_url_for_login_page()` function will only return a safe redirect URL. # If the provided `next` URL is not safe, that function will fill `redirect_to` # with a value of `reverse('dashboard')`. if request.GET.get('next'): redirect_to, root_url = get_next_url_for_login_page(request, include_host=True) if redirect_to != reverse('dashboard'): redirect_url = get_redirect_url_with_host(root_url, redirect_to) return redirect(redirect_url) if should_redirect_to_authn_microfrontend() and not request.user.is_authenticated: url_path = f'/login?account_activation_status={activation_message_type}' return redirect(settings.AUTHN_MICROFRONTEND_URL + url_path) return redirect('dashboard')
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 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 # Note: We check for the existence of login-related cookies in addition to is_authenticated # since Django's SessionAuthentication middleware auto-updates session cookies but not # the other login-related cookies. See ARCH-282. if request.user.is_authenticated and are_logged_in_cookies_set(request): return redirect(redirect_to) # 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: # lint-amnesty, pylint: disable=too-many-nested-blocks try: next_args = six.moves.urllib.parse.parse_qs(six.moves.urllib.parse.urlparse(redirect_to).query) if 'tpa_hint' in next_args: 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.exception(u"Unknown tpa_hint provider: %s", ex) enterprise_customer = enterprise_customer_for_request(request) # Redirect to authn MFE if it is enabled if should_redirect_to_authn_microfrontend() and not enterprise_customer: # This is to handle a case where a logged-in cookie is not present but the user is authenticated. # Note: If we don't handle this learner is redirected to authn MFE and then back to dashboard # instead of the desired redirect URL (e.g. finish_auth) resulting in learners not enrolling # into the courses. if request.user.is_authenticated and redirect_to: return redirect(redirect_to) query_params = request.GET.urlencode() url_path = '/{}{}'.format( initial_mode, '?' + query_params if query_params else '' ) return redirect(settings.AUTHN_MICROFRONTEND_URL + url_path) # 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 ] account_recovery_messages = [ { 'message': message.message, 'tags': message.tags } for message in messages.get_messages(request) if 'account-recovery' 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, 'account_recovery_messages': account_recovery_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)), 'is_account_recovery_feature_enabled': is_secondary_email_feature_enabled(), 'is_multiple_user_enterprises_feature_enabled': is_multiple_user_enterprises_feature_enabled(), 'enterprise_slug_login_url': get_enterprise_slug_login_url(), 'is_require_third_party_auth_enabled': is_require_third_party_auth_enabled(), }, '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'] ), } update_logistration_context_for_enterprise(request, context, enterprise_customer) response = render_to_response('student_account/login_and_register.html', context) handle_enterprise_cookies_for_logistration(request, response, context) 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