<commit_before>"""
Views for login / logout and associated functionality

Much of this file was broken out from views.py, previous history can be found there.
"""

import hashlib
import json
import logging
import re
import urllib

from django.conf import settings
from django.contrib import admin
from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth import login as django_login
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_cookie
from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.http import require_http_methods
from edx_django_utils.monitoring import set_custom_attribute
from ratelimit.decorators import ratelimit
from rest_framework.views import APIView

from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED

from common.djangoapps import third_party_auth
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.helpers import get_next_url_for_login_page, get_redirect_url_with_host
from common.djangoapps.student.models import AllowedAuthUser, LoginFailures, UserProfile
from common.djangoapps.student.views import compose_and_send_activation_email
from common.djangoapps.third_party_auth import pipeline, provider
from common.djangoapps.track import segment
from common.djangoapps.util.json_request import JsonResponse
from common.djangoapps.util.password_policy_validators import normalize_password
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
from openedx.core.djangoapps.safe_sessions.middleware import mark_user_change_as_expected
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn.config.waffle import ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY
from openedx.core.djangoapps.user_authn.cookies import get_response_with_refreshed_jwt_cookies, set_logged_in_cookies
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
from openedx.core.djangoapps.user_authn.toggles import (
    is_require_third_party_auth_enabled,
    should_redirect_to_authn_microfrontend
)
from openedx.core.djangoapps.user_authn.views.login_form import get_login_session_form
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
from openedx.core.djangoapps.user_authn.views.utils import API_V1, ENTERPRISE_ENROLLMENT_URL_REGEX, UUID4_REGEX
from openedx.core.djangoapps.user_authn.tasks import check_pwned_password_and_send_track_event
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.api.view_utils import require_post_params  # lint-amnesty, pylint: disable=unused-import
from openedx.features.enterprise_support.api import activate_learner_enterprise, get_enterprise_learner_data_from_api

log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
USER_MODEL = get_user_model()


def _do_third_party_auth(request):
    """
    User is already authenticated via 3rd party, now try to find and return their associated Django user.
    """
    running_pipeline = pipeline.get(request)
    username = running_pipeline['kwargs'].get('username')
    backend_name = running_pipeline['backend']
    third_party_uid = running_pipeline['kwargs']['uid']
    requested_provider = provider.Registry.get_from_pipeline(running_pipeline)
    platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)

    try:
        return pipeline.get_authenticated_user(requested_provider, username, third_party_uid)
    except USER_MODEL.DoesNotExist:
        AUDIT_LOG.info(
            "Login failed - user with username {username} has no social auth "
            "with backend_name {backend_name}".format(
                username=username, backend_name=backend_name)
        )
        message = Text(_(
            "You've successfully signed in to your {provider_name} account, "
            "but this account isn't linked with your {platform_name} account yet. {blank_lines}"
            "Use your {platform_name} username and password to sign in to {platform_name} below, "
            "and then link your {platform_name} account with {provider_name} from your dashboard. {blank_lines}"
            "If you don't have an account on {platform_name} yet, "
            "click {register_label_strong} at the top of the page."
        )).format(
            blank_lines=HTML('<br/><br/>'),
            platform_name=platform_name,
            provider_name=requested_provider.name,
            register_label_strong=HTML('<strong>{register_text}</strong>').format(
                register_text=_('Register')
            )
        )

        raise AuthFailedError(message, error_code='third-party-auth-with-no-linked-account')  # lint-amnesty, pylint: disable=raise-missing-from


def _get_user_by_email(email):
    """
    Finds a user object in the database based on the given email, ignores all fields except for email.
    """
    try:
        return USER_MODEL.objects.get(email=email)
    except USER_MODEL.DoesNotExist:
        return None


def _get_user_by_username(username):
    """
    Finds a user object in the database based on the given username.
    """
    try:
        return USER_MODEL.objects.get(username=username)
    except USER_MODEL.DoesNotExist:
        return None


def _get_user_by_email_or_username(request, api_version):
    """
    Finds a user object in the database based on the given request, ignores all fields except for email and username.
    """
    is_api_v2 = api_version != API_V1
    login_fields = ['email', 'password']
    if is_api_v2:
        login_fields = ['email_or_username', 'password']

    if any(f not in request.POST.keys() for f in login_fields):
        raise AuthFailedError(_('There was an error receiving your login information. Please email us.'))

    email_or_username = request.POST.get('email', None)
    user = _get_user_by_email(email_or_username)

    if not user and is_api_v2:
        # If user not found with email and API_V2, try username lookup
        email_or_username = request.POST.get('email_or_username', None)
        user = _get_user_by_username(email_or_username)

    if not user:
        digest = hashlib.shake_128(email_or_username.encode('utf-8')).hexdigest(16)  # pylint: disable=too-many-function-args
        AUDIT_LOG.warning(f"Login failed - Unknown user email or username {digest}")

    return user


def _check_excessive_login_attempts(user):
    """
    See if account has been locked out due to excessive login failures
    """<commit_msg>fix: login by email (#29727)<commit_after>"""
Views for login / logout and associated functionality

Much of this file was broken out from views.py, previous history can be found there.
"""

import hashlib
import json
import logging
import re
import urllib

from django.conf import settings
from django.contrib import admin
from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth import login as django_login
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_cookie
from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.http import require_http_methods
from edx_django_utils.monitoring import set_custom_attribute
from ratelimit.decorators import ratelimit
from rest_framework.views import APIView

from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED

from common.djangoapps import third_party_auth
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.helpers import get_next_url_for_login_page, get_redirect_url_with_host
from common.djangoapps.student.models import AllowedAuthUser, LoginFailures, UserProfile
from common.djangoapps.student.views import compose_and_send_activation_email
from common.djangoapps.third_party_auth import pipeline, provider
from common.djangoapps.track import segment
from common.djangoapps.util.json_request import JsonResponse
from common.djangoapps.util.password_policy_validators import normalize_password
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
from openedx.core.djangoapps.safe_sessions.middleware import mark_user_change_as_expected
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn.config.waffle import ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY
from openedx.core.djangoapps.user_authn.cookies import get_response_with_refreshed_jwt_cookies, set_logged_in_cookies
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
from openedx.core.djangoapps.user_authn.toggles import (
    is_require_third_party_auth_enabled,
    should_redirect_to_authn_microfrontend
)
from openedx.core.djangoapps.user_authn.views.login_form import get_login_session_form
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
from openedx.core.djangoapps.user_authn.views.utils import API_V1, ENTERPRISE_ENROLLMENT_URL_REGEX, UUID4_REGEX
from openedx.core.djangoapps.user_authn.tasks import check_pwned_password_and_send_track_event
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.api.view_utils import require_post_params  # lint-amnesty, pylint: disable=unused-import
from openedx.features.enterprise_support.api import activate_learner_enterprise, get_enterprise_learner_data_from_api

log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
USER_MODEL = get_user_model()


def _do_third_party_auth(request):
    """
    User is already authenticated via 3rd party, now try to find and return their associated Django user.
    """
    running_pipeline = pipeline.get(request)
    username = running_pipeline['kwargs'].get('username')
    backend_name = running_pipeline['backend']
    third_party_uid = running_pipeline['kwargs']['uid']
    requested_provider = provider.Registry.get_from_pipeline(running_pipeline)
    platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)

    try:
        return pipeline.get_authenticated_user(requested_provider, username, third_party_uid)
    except USER_MODEL.DoesNotExist:
        AUDIT_LOG.info(
            "Login failed - user with username {username} has no social auth "
            "with backend_name {backend_name}".format(
                username=username, backend_name=backend_name)
        )
        message = Text(_(
            "You've successfully signed in to your {provider_name} account, "
            "but this account isn't linked with your {platform_name} account yet. {blank_lines}"
            "Use your {platform_name} username and password to sign in to {platform_name} below, "
            "and then link your {platform_name} account with {provider_name} from your dashboard. {blank_lines}"
            "If you don't have an account on {platform_name} yet, "
            "click {register_label_strong} at the top of the page."
        )).format(
            blank_lines=HTML('<br/><br/>'),
            platform_name=platform_name,
            provider_name=requested_provider.name,
            register_label_strong=HTML('<strong>{register_text}</strong>').format(
                register_text=_('Register')
            )
        )

        raise AuthFailedError(message, error_code='third-party-auth-with-no-linked-account')  # lint-amnesty, pylint: disable=raise-missing-from


def _get_user_by_email(email):
    """
    Finds a user object in the database based on the given email, ignores all fields except for email.
    """
    try:
        return USER_MODEL.objects.get(email=email)
    except USER_MODEL.DoesNotExist:
        return None


def _get_user_by_username(username):
    """
    Finds a user object in the database based on the given username.
    """
    try:
        return USER_MODEL.objects.get(username=username)
    except USER_MODEL.DoesNotExist:
        return None


def _get_user_by_email_or_username(request, api_version):
    """
    Finds a user object in the database based on the given request, ignores all fields except for email and username.
    """
    is_api_v2 = api_version != API_V1
    login_fields = ['email', 'password']
    if is_api_v2:
        login_fields = ['email_or_username', 'password']

    if any(f not in request.POST.keys() for f in login_fields):
        raise AuthFailedError(_('There was an error receiving your login information. Please email us.'))

    email_or_username = request.POST.get('email', None) or request.POST.get('email_or_username', None)
    user = _get_user_by_email(email_or_username)

    if not user and is_api_v2:
        # If user not found with email and API_V2, try username lookup
        user = _get_user_by_username(email_or_username)

    if not user:
        digest = hashlib.shake_128(email_or_username.encode('utf-8')).hexdigest(16)  # pylint: disable=too-many-function-args
        AUDIT_LOG.warning(f"Login failed - Unknown user email or username {digest}")

    return user


def _check_excessive_login_attempts(user):
    """
    See if account has been locked out due to excessive login failures
    """
{'fraction_non_alphanumeric': 0.07039966106482136, 'fraction_numerical': 0.0023301793531987007, 'mean_word_length': 4.6426294820717136, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5929713', 'n_tokens_mistral': 4154, 'n_tokens_neox': 4080, 'n_words': 1048}