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.