Exemplo n.º 1
0
    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
Exemplo n.º 2
0
    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
Exemplo n.º 3
0
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')
Exemplo n.º 4
0
def login_user(request, api_version='v1'):
    """
    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

        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)
                check_pwned_password_and_send_track_event.delay(
                    user.id, request.POST.get('password'), user.is_staff)

        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)

        _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))

        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(response, 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
        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
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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