@@ -29,7 +29,7 @@ class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
returned json
|
||||
"""
|
||||
resp = self.client.post(
|
||||
reverse('user_api_login_session'),
|
||||
reverse('user_api_login_session', kwargs={'api_version': 'v1'}),
|
||||
{'email': email, 'password': password}
|
||||
)
|
||||
return resp
|
||||
|
||||
@@ -448,7 +448,7 @@ class IntegrationTestMixin(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
# Now the user enters their username and password.
|
||||
# The AJAX on the page will log them in:
|
||||
ajax_login_response = self.client.post(
|
||||
reverse('user_api_login_session'),
|
||||
reverse('user_api_login_session', kwargs={'api_version': 'v1'}),
|
||||
{'email': self.user.email, 'password': 'test'}
|
||||
)
|
||||
assert ajax_login_response.status_code == 200
|
||||
|
||||
@@ -199,7 +199,7 @@ class LoginEnrollmentTestCase(TestCase):
|
||||
"""
|
||||
Login, check that the corresponding view's response has a 200 status code.
|
||||
"""
|
||||
resp = self.client.post(reverse('user_api_login_session'),
|
||||
resp = self.client.post(reverse('user_api_login_session', kwargs={'api_version': 'v1'}),
|
||||
{'email': email, 'password': password})
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ class TestUserPreferenceMiddleware(CacheIsolationTestCase):
|
||||
# Use an actual call to the login endpoint, to validate that the middleware
|
||||
# stack does the right thing
|
||||
response = self.client.post(
|
||||
reverse('user_api_login_session'),
|
||||
reverse('user_api_login_session', kwargs={'api_version': 'v1'}),
|
||||
data={
|
||||
'email': self.user.email,
|
||||
'password': UserFactory._DEFAULT_PASSWORD, # pylint: disable=protected-access
|
||||
|
||||
@@ -54,7 +54,7 @@ urlpatterns = [
|
||||
|
||||
# Moved from user_api/legacy_urls.py
|
||||
url(
|
||||
r'^api/user/v1/account/login_session/$',
|
||||
r'^api/user/(?P<api_version>v(1|2))/account/login_session/$',
|
||||
login.LoginSessionView.as_view(),
|
||||
name="user_api_login_session"
|
||||
),
|
||||
|
||||
@@ -13,9 +13,11 @@ import urllib
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth import login as django_login
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.contrib import admin
|
||||
from django.db.models import Q
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
@@ -37,7 +39,9 @@ from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
|
||||
from openedx.core.djangoapps.user_authn.toggles import should_redirect_to_authn_microfrontend
|
||||
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
|
||||
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 ENTERPRISE_ENROLLMENT_URL_REGEX, UUID4_REGEX
|
||||
from openedx.core.djangoapps.user_authn.views.utils import (
|
||||
ENTERPRISE_ENROLLMENT_URL_REGEX, UUID4_REGEX, API_V1
|
||||
)
|
||||
from openedx.core.djangoapps.user_authn.toggles import is_require_third_party_auth_enabled
|
||||
from openedx.core.djangoapps.user_authn.config.waffle import ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
@@ -54,6 +58,7 @@ from common.djangoapps.util.password_policy_validators import normalize_password
|
||||
|
||||
log = logging.getLogger("edx.student")
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
USER_MODEL = get_user_model()
|
||||
|
||||
|
||||
def _do_third_party_auth(request):
|
||||
@@ -104,12 +109,34 @@ def _get_user_by_email(request):
|
||||
email = request.POST['email']
|
||||
|
||||
try:
|
||||
return User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
return USER_MODEL.objects.get(email=email)
|
||||
except USER_MODEL.DoesNotExist:
|
||||
digest = hashlib.shake_128(email.encode('utf-8')).hexdigest(16) # pylint: disable=too-many-function-args
|
||||
AUDIT_LOG.warning(f"Login failed - Unknown user email {digest}")
|
||||
|
||||
|
||||
def _get_user_by_email_or_username(request):
|
||||
"""
|
||||
Finds a user object in the database based on the given request, ignores all fields except for email and username.
|
||||
"""
|
||||
if not (
|
||||
'email' in request.POST or 'username' in request.POST
|
||||
) or 'password' not in request.POST:
|
||||
raise AuthFailedError(_('There was an error receiving your login information. Please email us.'))
|
||||
|
||||
email = request.POST.get('email', None)
|
||||
username = request.POST.get('username', None)
|
||||
|
||||
try:
|
||||
return USER_MODEL.objects.get(
|
||||
Q(username=username) | Q(email=email)
|
||||
)
|
||||
except USER_MODEL.DoesNotExist:
|
||||
username_or_email = email or username
|
||||
digest = hashlib.shake_128(username_or_email.encode('utf-8')).hexdigest(16) # pylint: disable=too-many-function-args
|
||||
AUDIT_LOG.warning(f"Login failed - Unknown user username/email {digest}")
|
||||
|
||||
|
||||
def _check_excessive_login_attempts(user):
|
||||
"""
|
||||
See if account has been locked out due to excessive login failures
|
||||
@@ -428,7 +455,7 @@ def enterprise_selection_page(request, user, next_url):
|
||||
rate=settings.LOGISTRATION_RATELIMIT_RATE,
|
||||
method='POST',
|
||||
) # lint-amnesty, pylint: disable=too-many-statements
|
||||
def login_user(request):
|
||||
def login_user(request, api_version='v1'):
|
||||
"""
|
||||
AJAX request to log in the user.
|
||||
|
||||
@@ -494,8 +521,10 @@ def login_user(request):
|
||||
response_content = e.get_response()
|
||||
return JsonResponse(response_content, status=403)
|
||||
else:
|
||||
user = _get_user_by_email(request)
|
||||
|
||||
if api_version == API_V1:
|
||||
user = _get_user_by_email(request)
|
||||
else:
|
||||
user = _get_user_by_email_or_username(request)
|
||||
_check_excessive_login_attempts(user)
|
||||
|
||||
possibly_authenticated_user = user
|
||||
@@ -592,12 +621,11 @@ class LoginSessionView(APIView):
|
||||
authentication_classes = []
|
||||
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
def get(self, request):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse(get_login_session_form(request).to_json(), content_type="application/json") # lint-amnesty, pylint: disable=http-response-with-content-type-json
|
||||
|
||||
@method_decorator(require_post_params(["email", "password"]))
|
||||
@method_decorator(csrf_protect)
|
||||
def post(self, request):
|
||||
def post(self, request, api_version):
|
||||
"""Log in a user.
|
||||
|
||||
See `login_user` for details.
|
||||
@@ -610,7 +638,7 @@ class LoginSessionView(APIView):
|
||||
200 {'success': true}
|
||||
|
||||
"""
|
||||
return login_user(request)
|
||||
return login_user(request, api_version)
|
||||
|
||||
@method_decorator(sensitive_post_parameters("password"))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
@@ -89,7 +89,7 @@ def get_login_session_form(request):
|
||||
HttpResponse
|
||||
|
||||
"""
|
||||
form_desc = FormDescription("post", reverse("user_api_login_session"))
|
||||
form_desc = FormDescription("post", reverse("user_api_login_session", kwargs={'api_version': 'v1'}))
|
||||
_apply_third_party_auth_overrides(request, form_desc)
|
||||
|
||||
# Translators: This label appears above a field on the login form
|
||||
|
||||
@@ -957,7 +957,7 @@ class LoginSessionViewTest(ApiTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url = reverse("user_api_login_session")
|
||||
self.url = reverse("user_api_login_session", kwargs={'api_version': 'v1'})
|
||||
|
||||
@ddt.data("get", "post")
|
||||
def test_auth_disabled(self, method):
|
||||
@@ -986,7 +986,7 @@ class LoginSessionViewTest(ApiTestCase):
|
||||
# Verify that the form description matches what we expect
|
||||
form_desc = json.loads(response.content.decode('utf-8'))
|
||||
assert form_desc['method'] == 'post'
|
||||
assert form_desc['submit_url'] == reverse('user_api_login_session')
|
||||
assert form_desc['submit_url'] == reverse('user_api_login_session', kwargs={'api_version': 'v1'})
|
||||
assert form_desc['fields'] == [{'name': 'email', 'defaultValue': '', 'type': 'email', 'required': True,
|
||||
'label': 'Email', 'placeholder': '',
|
||||
'instructions': 'The email address you used to register with {platform_name}'
|
||||
@@ -1050,6 +1050,16 @@ class LoginSessionViewTest(ApiTestCase):
|
||||
{'category': 'conversion', 'provider': None, 'label': track_label}
|
||||
)
|
||||
|
||||
def test_login_with_username(self):
|
||||
UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
data = {
|
||||
"username": self.USERNAME,
|
||||
"password": self.PASSWORD,
|
||||
}
|
||||
self.url = reverse("user_api_login_session", kwargs={'api_version': 'v2'})
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertHttpOK(response)
|
||||
|
||||
def test_session_cookie_expiry(self):
|
||||
# Create a test user
|
||||
UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
|
||||
@@ -9,7 +9,7 @@ from common.djangoapps import third_party_auth
|
||||
from common.djangoapps.third_party_auth import pipeline
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
|
||||
|
||||
API_V1 = 'v1'
|
||||
UUID4_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
|
||||
ENTERPRISE_ENROLLMENT_URL_REGEX = fr'/enterprise/{UUID4_REGEX}/course/{settings.COURSE_KEY_REGEX}/enroll'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user