Example #1
0
    def test_complete_url_raises_value_error_if_provider_not_enabled(self):
        provider_name = 'oa2-not-enabled'

        assert provider.Registry.get(provider_name) is None

        with pytest.raises(ValueError):
            pipeline.get_complete_url(provider_name)
Example #2
0
    def test_complete_url_raises_value_error_if_provider_not_enabled(self):
        provider_name = 'oa2-not-enabled'

        self.assertIsNone(provider.Registry.get(provider_name))

        with self.assertRaises(ValueError):
            pipeline.get_complete_url(provider_name)
Example #3
0
    def test_for_value_error_if_provider_id_invalid(self):
        provider_id = 'invalid'  # Format is normally "{prefix}-{identifier}"

        with pytest.raises(ValueError):
            provider.Registry.get(provider_id)

        with pytest.raises(ValueError):
            pipeline.get_login_url(provider_id, pipeline.AUTH_ENTRY_LOGIN)

        with pytest.raises(ValueError):
            pipeline.get_disconnect_url(provider_id, 1000)

        with pytest.raises(ValueError):
            pipeline.get_complete_url(provider_id)
Example #4
0
 def get_context(self,
                 params=None,
                 current_provider=None,
                 backend_name=None,
                 add_user_details=False):
     """
     Returns the third party auth context
     """
     return {
         'currentProvider':
         current_provider,
         'platformName':
         settings.PLATFORM_NAME,
         'providers':
         self.get_provider_data(params) if params else [],
         'secondaryProviders': [],
         'finishAuthUrl':
         pipeline.get_complete_url(backend_name) if backend_name else None,
         'errorMessage':
         None,
         'registerFormSubmitButtonText':
         'Create Account',
         'syncLearnerProfileData':
         False,
         'pipeline_user_details': {
             'email': '*****@*****.**'
         } if add_user_details else {}
     }
Example #5
0
    def test_custom_form_error(self):
        """
        Use the Google provider to test the custom login/register failure redirects.
        """
        # The pipeline starts by a user GETting /auth/login/google-oauth2/?auth_entry=custom1
        # Synthesize that request and check that it redirects to the correct
        # provider page.
        auth_entry = 'custom1'  # See definition in lms/envs/test.py
        login_url = pipeline.get_login_url(self.provider.provider_id,
                                           auth_entry)
        login_url += "&next=/misc/final-destination"
        self.assert_redirect_to_provider_looks_correct(
            self.client.get(login_url))

        def fake_auth_complete_error(_inst, *_args, **_kwargs):
            """ Mock the backend's auth_complete() method """
            raise AuthException("Mock login failed")

        # Next, the provider makes a request against /auth/complete/<provider>.
        complete_url = pipeline.get_complete_url(self.provider.backend_name)
        with patch.object(self.provider.backend_class, 'auth_complete',
                          fake_auth_complete_error):
            response = self.client.get(complete_url)
        # This should redirect to the custom error URL
        assert response.status_code == 302
        assert response['Location'] == '/misc/my-custom-sso-error-page'
Example #6
0
    def test_custom_form(self):
        """
        Use the Google provider to test the custom login/register form feature.
        """
        # The pipeline starts by a user GETting /auth/login/google-oauth2/?auth_entry=custom1
        # Synthesize that request and check that it redirects to the correct
        # provider page.
        auth_entry = 'custom1'  # See definition in lms/envs/test.py
        login_url = pipeline.get_login_url(self.provider.provider_id, auth_entry)
        login_url += "&next=/misc/final-destination"
        self.assert_redirect_to_provider_looks_correct(self.client.get(login_url))

        def fake_auth_complete(inst, *args, **kwargs):
            """ Mock the backend's auth_complete() method """
            kwargs.update({'response': self.get_response_data(), 'backend': inst})
            return inst.strategy.authenticate(*args, **kwargs)

        # Next, the provider makes a request against /auth/complete/<provider>.
        complete_url = pipeline.get_complete_url(self.provider.backend_name)
        with patch.object(self.provider.backend_class, 'auth_complete', fake_auth_complete):
            response = self.client.get(complete_url)
        # This should redirect to the custom login/register form:
        assert response.status_code == 302
        assert response['Location'] == '/auth/custom_auth_entry'

        response = self.client.get(response['Location'])
        assert response.status_code == 200
        assert 'action="/misc/my-custom-registration-form" method="post"' in response.content.decode('utf-8')
        data_decoded = base64.b64decode(response.context['data']).decode('utf-8')
        data_parsed = json.loads(data_decoded)
        # The user's details get passed to the custom page as a base64 encoded query parameter:
        assert data_parsed == {'auth_entry': 'custom1', 'backend_name': 'google-oauth2',
                               'provider_id': 'oa2-google-oauth2',
                               'user_details': {'username': '******', 'email': '*****@*****.**',
                                                'fullname': 'name_value', 'first_name': 'given_name_value',
                                                'last_name': 'family_name_value'}}
        # Check the hash that is used to confirm the user's data in the GET parameter is correct
        secret_key = settings.THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS['custom1']['secret_key']
        hmac_expected = hmac.new(
            secret_key.encode('utf-8'),
            msg=data_decoded.encode('utf-8'),
            digestmod=hashlib.sha256
        ).digest()
        assert base64.b64decode(response.context['hmac']) == hmac_expected

        # Now our custom registration form creates or logs in the user:
        email, password = data_parsed['user_details']['email'], 'random_password'
        created_user = UserFactory(email=email, password=password)
        login_response = self.client.post(reverse('login_api'), {'email': email, 'password': password})
        assert login_response.status_code == 200

        # Now our custom login/registration page must resume the pipeline:
        response = self.client.get(complete_url)
        assert response.status_code == 302
        assert response['Location'] == '/misc/final-destination'

        _, strategy = self.get_request_and_strategy()
        self.assert_social_auth_exists_for_user(created_user, strategy)
Example #7
0
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
Example #8
0
def third_party_auth_context(request, redirect_to, tpa_hint=None):
    """
    Context for third party auth providers and the currently running pipeline.

    Arguments:
        request (HttpRequest): The request, used to determine if a pipeline
            is currently running.
        redirect_to: The URL to send the user to following successful
            authentication.
        tpa_hint (string): An override flag that will return a matching provider
            as long as its configuration has been enabled

    Returns:
        dict

    """
    context = {
        "currentProvider":
        None,
        "platformName":
        configuration_helpers.get_value('PLATFORM_NAME',
                                        settings.PLATFORM_NAME),
        "providers": [],
        "secondaryProviders": [],
        "finishAuthUrl":
        None,
        "errorMessage":
        None,
        "registerFormSubmitButtonText":
        _("Create Account"),
        "syncLearnerProfileData":
        False,
        "pipeline_user_details": {}
    }

    if third_party_auth.is_enabled():
        for enabled in third_party_auth.provider.Registry.displayed_for_login(
                tpa_hint=tpa_hint):
            info = {
                "id":
                enabled.provider_id,
                "name":
                enabled.name,
                "iconClass":
                enabled.icon_class or None,
                "iconImage":
                enabled.icon_image.url if enabled.icon_image else None,
                "skipHintedLogin":
                enabled.skip_hinted_login_dialog,
                "loginUrl":
                pipeline.get_login_url(
                    enabled.provider_id,
                    pipeline.AUTH_ENTRY_LOGIN,
                    redirect_url=redirect_to,
                ),
                "registerUrl":
                pipeline.get_login_url(
                    enabled.provider_id,
                    pipeline.AUTH_ENTRY_REGISTER,
                    redirect_url=redirect_to,
                ),
            }
            context["providers" if not enabled.
                    secondary else "secondaryProviders"].append(info)

        running_pipeline = pipeline.get(request)
        if running_pipeline is not None:
            current_provider = third_party_auth.provider.Registry.get_from_pipeline(
                running_pipeline)
            user_details = running_pipeline['kwargs']['details']
            if user_details:
                username = running_pipeline['kwargs'].get(
                    'username') or user_details.get('username')
                if username:
                    user_details['username'] = clean_username(username)
                context['pipeline_user_details'] = user_details

            if current_provider is not None:
                context["currentProvider"] = current_provider.name
                context["finishAuthUrl"] = pipeline.get_complete_url(
                    current_provider.backend_name)
                context[
                    "syncLearnerProfileData"] = current_provider.sync_learner_profile_data

                if current_provider.skip_registration_form:
                    # As a reliable way of "skipping" the registration form, we just submit it automatically
                    context["autoSubmitRegForm"] = True

        # Check for any error messages we may want to display:
        for msg in messages.get_messages(request):
            if msg.extra_tags.split()[0] == "social-auth":
                # msg may or may not be translated. Try translating [again] in case we are able to:
                context["errorMessage"] = _(str(msg))  # pylint: disable=E7610
                break

    return context
Example #9
0
    def test_complete_url_returns_expected_format(self):
        complete_url = pipeline.get_complete_url(
            self.enabled_provider.backend_name)

        assert complete_url.startswith('/auth/complete')
        assert self.enabled_provider.backend_name in complete_url
Example #10
0
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