diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index 590f85442c..73dd3bbfc4 100644
a
|
b
|
def _get_user_session_key(request):
|
59 | 59 | return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY]) |
60 | 60 | |
61 | 61 | |
62 | | def authenticate(request=None, **credentials): |
| 62 | def authenticate_with_error_code(request=None, **credentials): |
63 | 63 | """ |
64 | 64 | If the given credentials are valid, return a User object. |
65 | 65 | """ |
| 66 | failed_reasons = [] |
66 | 67 | for backend, backend_path in _get_backends(return_tuples=True): |
67 | 68 | try: |
68 | | inspect.getcallargs(backend.authenticate, request, **credentials) |
| 69 | inspect.getcallargs(backend.authenticate_with_error_code, request, **credentials) |
| 70 | use_authenticate_with_error_code = True |
69 | 71 | except TypeError: |
70 | | # This backend doesn't accept these credentials as arguments. Try the next one. |
71 | | continue |
| 72 | try: |
| 73 | inspect.getcallargs(backend.authenticate, request, **credentials) |
| 74 | use_authenticate_with_error_code = False |
| 75 | except TypeError: |
| 76 | # This backend doesn't accept these credentials as arguments. Try the next one. |
| 77 | continue |
72 | 78 | try: |
73 | | user = backend.authenticate(request, **credentials) |
| 79 | if use_authenticate_with_error_code: |
| 80 | user, failed_reason = backend.authenticate_with_error_code(request, **credentials) |
| 81 | else: |
| 82 | user, failed_reason = backend.authenticate(request, **credentials), None |
74 | 83 | except PermissionDenied: |
75 | 84 | # This backend says to stop in our tracks - this user should not be allowed in at all. |
76 | 85 | break |
77 | 86 | if user is None: |
| 87 | if failed_reason: |
| 88 | failed_reasons.append(failed_reason) |
78 | 89 | continue |
79 | 90 | # Annotate the user object with the path of the backend. |
80 | 91 | user.backend = backend_path |
… |
… |
def authenticate(request=None, **credentials):
|
83 | 94 | # The credentials supplied are invalid to all backends, fire signal |
84 | 95 | user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request) |
85 | 96 | |
| 97 | return None, failed_reasons[0] if failed_reasons else None |
| 98 | |
| 99 | def authenticate(*args, **kwargs): |
| 100 | return authenticate_with_error_code(*args, **kwargs)[0] |
86 | 101 | |
87 | 102 | def login(request, user, backend=None): |
88 | 103 | """ |
diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py
index 64937753ed..1f1a446d33 100644
a
|
b
|
class ModelBackend:
|
9 | 9 | Authenticates against settings.AUTH_USER_MODEL. |
10 | 10 | """ |
11 | 11 | |
12 | | def authenticate(self, request, username=None, password=None, **kwargs): |
| 12 | def authenticate_with_error_code(self, request, username=None, password=None, **kwargs): |
13 | 13 | if username is None: |
14 | 14 | username = kwargs.get(UserModel.USERNAME_FIELD) |
15 | 15 | try: |
… |
… |
class ModelBackend:
|
19 | 19 | # difference between an existing and a nonexistent user (#20760). |
20 | 20 | UserModel().set_password(password) |
21 | 21 | else: |
22 | | if user.check_password(password) and self.user_can_authenticate(user): |
23 | | return user |
| 22 | if user.check_password(password): |
| 23 | if self.user_can_authenticate(user): |
| 24 | return user, None |
| 25 | else: |
| 26 | return None, "inactive" |
| 27 | return None, None |
| 28 | |
| 29 | def authenticate(self, *args, **kwargs): |
| 30 | return self.authenticate_with_error_code(*args, **kwargs)[0] |
24 | 31 | |
25 | 32 | def user_can_authenticate(self, user): |
26 | 33 | """ |
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index dfceccb2ec..9b7b2aa274 100644
a
|
b
|
import unicodedata
|
2 | 2 | |
3 | 3 | from django import forms |
4 | 4 | from django.contrib.auth import ( |
5 | | authenticate, get_user_model, password_validation, |
| 5 | authenticate_with_error_code, get_user_model, password_validation, |
6 | 6 | ) |
7 | 7 | from django.contrib.auth.hashers import ( |
8 | 8 | UNUSABLE_PASSWORD_PREFIX, identify_hasher, |
… |
… |
class AuthenticationForm(forms.Form):
|
189 | 189 | password = self.cleaned_data.get('password') |
190 | 190 | |
191 | 191 | if username is not None and password: |
192 | | self.user_cache = authenticate(self.request, username=username, password=password) |
| 192 | self.user_cache, failed_reason = authenticate_with_error_code(self.request, username=username, password=password) |
193 | 193 | if self.user_cache is None: |
194 | | # An authentication backend may reject inactive users. Check |
195 | | # if the user exists and is inactive, and raise the 'inactive' |
196 | | # error if so. |
197 | | try: |
198 | | self.user_cache = UserModel._default_manager.get_by_natural_key(username) |
199 | | except UserModel.DoesNotExist: |
200 | | pass |
201 | | else: |
202 | | self.confirm_login_allowed(self.user_cache) |
| 194 | # An authentication backend may specify a custom rejection reason. |
| 195 | if failed_reason and failed_reason in self.error_messages: |
| 196 | raise forms.ValidationError( |
| 197 | self.error_messages[failed_reason], |
| 198 | code=failed_reason, |
| 199 | ) |
203 | 200 | raise self.get_invalid_login_error() |
204 | 201 | else: |
205 | 202 | self.confirm_login_allowed(self.user_cache) |