diff --git a/lms/djangoapps/oauth_dispatch/dot_overrides.py b/lms/djangoapps/oauth_dispatch/dot_overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..98203d6cde46c93da2ac2e25df13ac528d6678a8 --- /dev/null +++ b/lms/djangoapps/oauth_dispatch/dot_overrides.py @@ -0,0 +1,43 @@ +""" +Classes that override default django-oauth-toolkit behavior +""" + +from django.contrib.auth import authenticate, get_user_model +from oauth2_provider.oauth2_validators import OAuth2Validator + + +class EdxOAuth2Validator(OAuth2Validator): + """ + Validator class that implements edX-specific custom behavior: + + * It allows users to log in with their email or username. + * It does not require users to be active before logging in. + """ + + def validate_user(self, username, password, client, request, *args, **kwargs): + """ + Authenticate users, but allow inactive users (with u.is_active == False) + to authenticate. + """ + user = self._authenticate(username=username, password=password) + if user is not None: + request.user = user + return True + return False + + def _authenticate(self, username, password): + """ + Authenticate the user, allowing the user to identify themself either by + username or email + """ + + authenticated_user = authenticate(username=username, password=password) + if authenticated_user is None: + UserModel = get_user_model() # pylint: disable=invalid-name + try: + email_user = UserModel.objects.get(email=username) + except UserModel.DoesNotExist: + authenticated_user = None + else: + authenticated_user = authenticate(username=email_user.username, password=password) + return authenticated_user diff --git a/lms/djangoapps/oauth_dispatch/tests/test_dot_overrides.py b/lms/djangoapps/oauth_dispatch/tests/test_dot_overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..bca14f94712da4150a4a74fc2f2ac7e27da1100a --- /dev/null +++ b/lms/djangoapps/oauth_dispatch/tests/test_dot_overrides.py @@ -0,0 +1,66 @@ +""" +Test of custom django-oauth-toolkit behavior +""" + +# pylint: disable=protected-access + +from django.contrib.auth.models import User +from django.test import TestCase, RequestFactory +from ..dot_overrides import EdxOAuth2Validator + + +class AuthenticateTestCase(TestCase): + """ + Test that users can authenticate with either username or email + """ + + def setUp(self): + super(AuthenticateTestCase, self).setUp() + self.user = User.objects.create_user( + username='darkhelmet', + password='12345', + email='darkhelmet@spaceball_one.org', + ) + self.validator = EdxOAuth2Validator() + + def test_authenticate_with_username(self): + user = self.validator._authenticate(username='darkhelmet', password='12345') + self.assertEqual( + self.user, + user + ) + + def test_authenticate_with_email(self): + user = self.validator._authenticate(username='darkhelmet@spaceball_one.org', password='12345') + self.assertEqual( + self.user, + user + ) + + +class CustomValidationTestCase(TestCase): + """ + Test custom user validation works. + + In particular, inactive users should be able to validate. + """ + def setUp(self): + super(CustomValidationTestCase, self).setUp() + self.user = User.objects.create_user( + username='darkhelmet', + password='12345', + email='darkhelmet@spaceball_one.org', + ) + self.validator = EdxOAuth2Validator() + self.request_factory = RequestFactory() + + def test_active_user_validates(self): + self.assertTrue(self.user.is_active) + request = self.request_factory.get('/') + self.assertTrue(self.validator.validate_user('darkhelmet', '12345', client=None, request=request)) + + def test_inactive_user_validates(self): + self.user.is_active = False + self.user.save() + request = self.request_factory.get('/') + self.assertTrue(self.validator.validate_user('darkhelmet', '12345', client=None, request=request)) diff --git a/lms/envs/common.py b/lms/envs/common.py index 6ad37a6ca955df0a4882fa72438b89efc01c735d..e3e92e9b7e57d743d17b64ccecc40090ce23c37c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -455,6 +455,12 @@ OAUTH_OIDC_USERINFO_HANDLERS = ( OAUTH_EXPIRE_CONFIDENTIAL_CLIENT_DAYS = 365 OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS = 30 +################################## DJANGO OAUTH TOOLKIT ####################################### + +OAUTH2_PROVIDER = { + 'OAUTH2_VALIDATOR_CLASS': 'lms.djangoapps.oauth_dispatch.dot_overrides.EdxOAuth2Validator' +} + ################################## TEMPLATE CONFIGURATION ##################################### # Mako templating # TODO: Move the Mako templating into a different engine in TEMPLATES below.