summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksej Manaev <aleksej.manaev@gmx.de>2016-07-11 16:40:39 +0200
committerTim Graham <timograham@gmail.com>2016-09-12 20:11:53 -0400
commit4b9330ccc04575f9e5126529ec355a450d12e77c (patch)
tree90d340a9d28bd448b3b709b8b605bd1009bbba0a
parent32c0d823e5316aa7d616a69996919b62748368cc (diff)
downloaddjango-4b9330ccc04575f9e5126529ec355a450d12e77c.tar.gz
Fixed #25187 -- Made request available in authentication backends.
-rw-r--r--django/contrib/auth/__init__.py24
-rw-r--r--django/contrib/auth/backends.py4
-rw-r--r--django/contrib/auth/forms.py2
-rw-r--r--django/contrib/auth/middleware.py2
-rw-r--r--docs/internals/deprecation.txt3
-rw-r--r--docs/ref/contrib/auth.txt16
-rw-r--r--docs/releases/1.11.txt8
-rw-r--r--docs/topics/auth/customizing.txt24
-rw-r--r--docs/topics/auth/default.txt11
-rw-r--r--tests/auth_tests/test_auth_backends.py4
-rw-r--r--tests/auth_tests/test_auth_backends_deprecation.py30
-rw-r--r--tests/test_client_regress/auth_backends.py2
12 files changed, 106 insertions, 24 deletions
diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index fa0ef39a32..77458e15f9 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -1,11 +1,13 @@
import inspect
import re
+import warnings
from django.apps import apps as django_apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
+from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.module_loading import import_string
from django.utils.translation import LANGUAGE_SESSION_KEY
@@ -59,19 +61,29 @@ def _get_user_session_key(request):
return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])
-def authenticate(**credentials):
+def authenticate(request=None, **credentials):
"""
If the given credentials are valid, return a User object.
"""
for backend, backend_path in _get_backends(return_tuples=True):
+ args = (request,)
try:
- inspect.getcallargs(backend.authenticate, **credentials)
+ inspect.getcallargs(backend.authenticate, request, **credentials)
except TypeError:
- # This backend doesn't accept these credentials as arguments. Try the next one.
- continue
-
+ try:
+ inspect.getcallargs(backend.authenticate, **credentials)
+ except TypeError:
+ # This backend doesn't accept these credentials as arguments. Try the next one.
+ continue
+ else:
+ args = ()
+ warnings.warn(
+ "Update authentication backend %s to accept a "
+ "positional `request` argument." % backend_path,
+ RemovedInDjango21Warning
+ )
try:
- user = backend.authenticate(**credentials)
+ user = backend.authenticate(*args, **credentials)
except PermissionDenied:
# This backend says to stop in our tracks - this user should not be allowed in at all.
break
diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py
index f145a1929a..379f1c4450 100644
--- a/django/contrib/auth/backends.py
+++ b/django/contrib/auth/backends.py
@@ -9,7 +9,7 @@ class ModelBackend(object):
Authenticates against settings.AUTH_USER_MODEL.
"""
- def authenticate(self, username=None, password=None, **kwargs):
+ def authenticate(self, request, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
@@ -125,7 +125,7 @@ class RemoteUserBackend(ModelBackend):
# Create a User object if not already in the database?
create_unknown_user = True
- def authenticate(self, remote_user):
+ def authenticate(self, request, remote_user):
"""
The username passed as ``remote_user`` is considered trusted. This
method simply returns the ``User`` object with the given username,
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index ddd915ae69..7a6a8d74aa 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -189,7 +189,7 @@ class AuthenticationForm(forms.Form):
password = self.cleaned_data.get('password')
if username is not None and password:
- self.user_cache = authenticate(username=username, password=password)
+ self.user_cache = authenticate(self.request, username=username, password=password)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py
index 8c56e645fe..f179894e73 100644
--- a/django/contrib/auth/middleware.py
+++ b/django/contrib/auth/middleware.py
@@ -88,7 +88,7 @@ class RemoteUserMiddleware(MiddlewareMixin):
# We are seeing this user for the first time in this session, attempt
# to authenticate the user.
- user = auth.authenticate(remote_user=username)
+ user = auth.authenticate(request, remote_user=username)
if user:
# User is valid. Set request.user and persist user in the session
# by logging the user in.
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 8be137e635..a1c55646d0 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -38,6 +38,9 @@ details on these changes.
* ``DatabaseIntrospection.get_indexes()`` will be removed.
+* The ``authenticate()`` method of authentication backends will require a
+ ``request`` argument.
+
.. _deprecation-removed-in-2.0:
2.0
diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt
index f12b16daa0..38f18b6dc8 100644
--- a/docs/ref/contrib/auth.txt
+++ b/docs/ref/contrib/auth.txt
@@ -518,7 +518,7 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
implement them other than returning an empty set of permissions if
``obj is not None``.
- .. method:: authenticate(username=None, password=None, **kwargs)
+ .. method:: authenticate(request, username=None, password=None, **kwargs)
Tries to authenticate ``username`` with ``password`` by calling
:meth:`User.check_password
@@ -528,6 +528,14 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
<django.contrib.auth.models.CustomUser.USERNAME_FIELD>`. Returns an
authenticated user or ``None``.
+ ``request`` is an :class:`~django.http.HttpRequest` and may be ``None``
+ if it wasn't provided to :func:`~django.contrib.auth.authenticate`
+ (which passes it on to the backend).
+
+ .. versionchanged:: 1.11
+
+ The ``request`` argument was added.
+
.. method:: get_user_permissions(user_obj, obj=None)
Returns the set of permission strings the ``user_obj`` has from their
@@ -603,7 +611,7 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
:class:`~django.contrib.auth.models.User` object is created if not already
in the database. Defaults to ``True``.
-.. method:: RemoteUserBackend.authenticate(remote_user)
+.. method:: RemoteUserBackend.authenticate(request, remote_user)
The username passed as ``remote_user`` is considered trusted. This method
simply returns the ``User`` object with the given username, creating a new
@@ -614,6 +622,10 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
``False`` and a ``User`` object with the given username is not found in the
database.
+ ``request`` is an :class:`~django.http.HttpRequest` and may be ``None`` if
+ it wasn't provided to :func:`~django.contrib.auth.authenticate` (which
+ passes it on to the backend).
+
.. method:: RemoteUserBackend.clean_username(username)
Performs any cleaning on the ``username`` (e.g. stripping LDAP DN
diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt
index 7b5bcc450f..a6edfcdf25 100644
--- a/docs/releases/1.11.txt
+++ b/docs/releases/1.11.txt
@@ -113,6 +113,10 @@ Minor features
allows using the authentication system :ref:`without any of the built-in
models <using-auth-without-models>`.
+* The ``HttpRequest`` is now passed to :func:`~django.contrib.auth.authenticate`
+ which in turn passes it to the authentication backend if it accepts a
+ ``request`` argument.
+
:mod:`django.contrib.contenttypes`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -556,3 +560,7 @@ Miscellaneous
* ``DatabaseIntrospection.get_indexes()`` is deprecated in favor of
``DatabaseIntrospection.get_constraints()``.
+
+* :func:`~django.contrib.auth.authenticate` now passes a ``request`` argument
+ to the ``authenticate()`` method of authentication backends. Support for
+ methods that don't accept ``request`` will be removed in Django 2.1.
diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt
index d102d9939b..085f74fcde 100644
--- a/docs/topics/auth/customizing.txt
+++ b/docs/topics/auth/customizing.txt
@@ -89,25 +89,26 @@ Writing an authentication backend
---------------------------------
An authentication backend is a class that implements two required methods:
-``get_user(user_id)`` and ``authenticate(**credentials)``, as well as a set of
-optional permission related :ref:`authorization methods <authorization_methods>`.
+``get_user(user_id)`` and ``authenticate(request, **credentials)``, as well as
+a set of optional permission related :ref:`authorization methods
+<authorization_methods>`.
The ``get_user`` method takes a ``user_id`` -- which could be a username,
database ID or whatever, but has to be the primary key of your ``User`` object
-- and returns a ``User`` object.
-The ``authenticate`` method takes credentials as keyword arguments. Most of
-the time, it'll just look like this::
+The ``authenticate`` method takes a ``request`` argument and credentials as
+keyword arguments. Most of the time, it'll just look like this::
class MyBackend(object):
- def authenticate(self, username=None, password=None):
+ def authenticate(self, request, username=None, password=None):
# Check the username/password and return a User.
...
But it could also authenticate a token, like so::
class MyBackend(object):
- def authenticate(self, token=None):
+ def authenticate(self, request, token=None):
# Check the token and return a User.
...
@@ -115,6 +116,10 @@ Either way, ``authenticate`` should check the credentials it gets, and it
should return a ``User`` object that matches those credentials, if the
credentials are valid. If they're not valid, it should return ``None``.
+``request`` is an :class:`~django.http.HttpRequest` and may be ``None`` if it
+wasn't provided to :func:`~django.contrib.auth.authenticate` (which passes it
+on to the backend).
+
The Django admin is tightly coupled to the Django :ref:`User object
<user-objects>`. The best way to deal with this is to create a Django ``User``
object for each user that exists for your backend (e.g., in your LDAP
@@ -140,7 +145,7 @@ object the first time a user authenticates::
ADMIN_PASSWORD = 'pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M='
"""
- def authenticate(self, username=None, password=None):
+ def authenticate(self, request, username=None, password=None):
login_valid = (settings.ADMIN_LOGIN == username)
pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
if login_valid and pwd_valid:
@@ -163,6 +168,11 @@ object the first time a user authenticates::
except User.DoesNotExist:
return None
+.. versionchanged:: 1.11
+
+ The ``request`` parameter was added to ``authenticate()`` and support for
+ backends that don't accept it will be removed in Django 2.1.
+
.. _authorization_methods:
Handling authorization in custom backends
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
index a484a89d6d..310f09e322 100644
--- a/docs/topics/auth/default.txt
+++ b/docs/topics/auth/default.txt
@@ -115,7 +115,7 @@ Changing a user's password will log out all their sessions. See
Authenticating users
--------------------
-.. function:: authenticate(\**credentials)
+.. function:: authenticate(request=None, \**credentials)
Use :func:`~django.contrib.auth.authenticate()` to verify a set of
credentials. It takes credentials as keyword arguments, ``username`` and
@@ -133,6 +133,13 @@ Authenticating users
else:
# No backend authenticated the credentials
+ ``request`` is an optional :class:`~django.http.HttpRequest` which is
+ passed on the ``authenticate()`` method of the authentication backends.
+
+ .. versionchanged:: 1.11
+
+ The optional ``request`` argument was added.
+
.. note::
This is a low level way to authenticate a set of credentials; for
@@ -342,7 +349,7 @@ If you have an authenticated user you want to attach to the current session
def my_view(request):
username = request.POST['username']
password = request.POST['password']
- user = authenticate(username=username, password=password)
+ user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Redirect to a success page.
diff --git a/tests/auth_tests/test_auth_backends.py b/tests/auth_tests/test_auth_backends.py
index e850772f35..ea40992a61 100644
--- a/tests/auth_tests/test_auth_backends.py
+++ b/tests/auth_tests/test_auth_backends.py
@@ -475,7 +475,7 @@ class PermissionDeniedBackend(object):
Always raises PermissionDenied in `authenticate`, `has_perm` and `has_module_perms`.
"""
- def authenticate(self, username=None, password=None):
+ def authenticate(self, request, username=None, password=None):
raise PermissionDenied
def has_perm(self, user_obj, perm, obj=None):
@@ -585,7 +585,7 @@ class TypeErrorBackend(object):
Always raises TypeError.
"""
- def authenticate(self, username=None, password=None):
+ def authenticate(self, request, username=None, password=None):
raise TypeError
diff --git a/tests/auth_tests/test_auth_backends_deprecation.py b/tests/auth_tests/test_auth_backends_deprecation.py
new file mode 100644
index 0000000000..6178936535
--- /dev/null
+++ b/tests/auth_tests/test_auth_backends_deprecation.py
@@ -0,0 +1,30 @@
+import warnings
+
+from django.contrib.auth import authenticate
+from django.test import SimpleTestCase, override_settings
+
+
+class NoRequestBackend(object):
+ def authenticate(self, username=None, password=None):
+ # Doesn't accept a request parameter.
+ pass
+
+
+class AcceptsRequestBackendTest(SimpleTestCase):
+ """
+ A deprecation warning is shown for backends that have an authenticate()
+ method without a request parameter.
+ """
+ no_request_backend = '%s.NoRequestBackend' % __name__
+
+ @override_settings(AUTHENTICATION_BACKENDS=[no_request_backend])
+ def test_no_request_deprecation_warning(self):
+ with warnings.catch_warnings(record=True) as warns:
+ warnings.simplefilter('always')
+ authenticate(username='test', password='test')
+ self.assertEqual(len(warns), 1)
+ self.assertEqual(
+ str(warns[0].message),
+ "Update authentication backend %s to accept a positional `request` "
+ "argument." % self.no_request_backend
+ )
diff --git a/tests/test_client_regress/auth_backends.py b/tests/test_client_regress/auth_backends.py
index 02a6d39611..847bef85f0 100644
--- a/tests/test_client_regress/auth_backends.py
+++ b/tests/test_client_regress/auth_backends.py
@@ -5,7 +5,7 @@ from .models import CustomUser
class CustomUserBackend(ModelBackend):
- def authenticate(self, username=None, password=None):
+ def authenticate(self, request, username=None, password=None):
try:
user = CustomUser.custom_objects.get_by_natural_key(username)
if user.check_password(password):