Merge pull request #27338 from edx/update-login-api

update login api
This commit is contained in:
Uzair Rasheed
2021-04-23 18:05:58 +05:00
committed by GitHub
9 changed files with 57 additions and 19 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
),

View File

@@ -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):

View File

@@ -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

View File

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

View File

@@ -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'