diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index 590f85442c..73dd3bbfc4 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -59,22 +59,33 @@ def _get_user_session_key(request):
     return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])
 
 
-def authenticate(request=None, **credentials):
+def authenticate_with_error_code(request=None, **credentials):
     """
     If the given credentials are valid, return a User object.
     """
+    failed_reasons = []
     for backend, backend_path in _get_backends(return_tuples=True):
         try:
-            inspect.getcallargs(backend.authenticate, request, **credentials)
+            inspect.getcallargs(backend.authenticate_with_error_code, request, **credentials)
+            use_authenticate_with_error_code = True
         except TypeError:
-            # This backend doesn't accept these credentials as arguments. Try the next one.
-            continue
+            try:
+                inspect.getcallargs(backend.authenticate, request, **credentials)
+                use_authenticate_with_error_code = False
+            except TypeError:
+                # This backend doesn't accept these credentials as arguments. Try the next one.
+                continue
         try:
-            user = backend.authenticate(request, **credentials)
+            if use_authenticate_with_error_code:
+                user, failed_reason = backend.authenticate_with_error_code(request, **credentials)
+            else:
+                user, failed_reason = backend.authenticate(request, **credentials), None
         except PermissionDenied:
             # This backend says to stop in our tracks - this user should not be allowed in at all.
             break
         if user is None:
+            if failed_reason:
+                failed_reasons.append(failed_reason)
             continue
         # Annotate the user object with the path of the backend.
         user.backend = backend_path
@@ -83,6 +94,10 @@ def authenticate(request=None, **credentials):
     # The credentials supplied are invalid to all backends, fire signal
     user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
 
+    return None, failed_reasons[0] if failed_reasons else None
+
+def authenticate(*args, **kwargs):
+    return authenticate_with_error_code(*args, **kwargs)[0]
 
 def login(request, user, backend=None):
     """
diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py
index 64937753ed..1f1a446d33 100644
--- a/django/contrib/auth/backends.py
+++ b/django/contrib/auth/backends.py
@@ -9,7 +9,7 @@ class ModelBackend:
     Authenticates against settings.AUTH_USER_MODEL.
     """
 
-    def authenticate(self, request, username=None, password=None, **kwargs):
+    def authenticate_with_error_code(self, request, username=None, password=None, **kwargs):
         if username is None:
             username = kwargs.get(UserModel.USERNAME_FIELD)
         try:
@@ -19,8 +19,15 @@ class ModelBackend:
             # difference between an existing and a nonexistent user (#20760).
             UserModel().set_password(password)
         else:
-            if user.check_password(password) and self.user_can_authenticate(user):
-                return user
+            if user.check_password(password):
+                if self.user_can_authenticate(user):
+                    return user, None
+                else:
+                    return None, "inactive"
+        return None, None
+
+    def authenticate(self, *args, **kwargs):
+        return self.authenticate_with_error_code(*args, **kwargs)[0]
 
     def user_can_authenticate(self, user):
         """
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index dfceccb2ec..9b7b2aa274 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -2,7 +2,7 @@ import unicodedata
 
 from django import forms
 from django.contrib.auth import (
-    authenticate, get_user_model, password_validation,
+    authenticate_with_error_code, get_user_model, password_validation,
 )
 from django.contrib.auth.hashers import (
     UNUSABLE_PASSWORD_PREFIX, identify_hasher,
@@ -189,17 +189,14 @@ class AuthenticationForm(forms.Form):
         password = self.cleaned_data.get('password')
 
         if username is not None and password:
-            self.user_cache = authenticate(self.request, username=username, password=password)
+            self.user_cache, failed_reason = authenticate_with_error_code(self.request, username=username, password=password)
             if self.user_cache is None:
-                # An authentication backend may reject inactive users. Check
-                # if the user exists and is inactive, and raise the 'inactive'
-                # error if so.
-                try:
-                    self.user_cache = UserModel._default_manager.get_by_natural_key(username)
-                except UserModel.DoesNotExist:
-                    pass
-                else:
-                    self.confirm_login_allowed(self.user_cache)
+                # An authentication backend may specify a custom rejection reason.
+                if failed_reason and failed_reason in self.error_messages:
+                    raise forms.ValidationError(
+                        self.error_messages[failed_reason],
+                        code=failed_reason,
+                    )
                 raise self.get_invalid_login_error()
             else:
                 self.confirm_login_allowed(self.user_cache)
