From 28ab627a50bf0b3d4f6936ee51e9c591a25d461d Mon Sep 17 00:00:00 2001
From: "J. Clifford Dyer" <cdyer@edx.org>
Date: Wed, 13 Apr 2016 20:57:46 +0000
Subject: [PATCH] Fix authentication issues with django-oauth-toolkit

MA-2271
    Allow inactive users to authenticate.
MA-2273
    Provide custom authenticate method that allows users to provide email.
---
 .../oauth_dispatch/dot_overrides.py           | 43 ++++++++++++
 .../tests/test_dot_overrides.py               | 66 +++++++++++++++++++
 lms/envs/common.py                            |  6 ++
 3 files changed, 115 insertions(+)
 create mode 100644 lms/djangoapps/oauth_dispatch/dot_overrides.py
 create mode 100644 lms/djangoapps/oauth_dispatch/tests/test_dot_overrides.py

diff --git a/lms/djangoapps/oauth_dispatch/dot_overrides.py b/lms/djangoapps/oauth_dispatch/dot_overrides.py
new file mode 100644
index 00000000000..98203d6cde4
--- /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 00000000000..bca14f94712
--- /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 6ad37a6ca95..e3e92e9b7e5 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.
-- 
GitLab