Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merged multi-auth branch to trunk. See the authentication docs for th…

…e ramifications of this change. Many, many thanks to Joseph Kocherhans for the hard work!

git-svn-id: http://code.djangoproject.com/svn/django/trunk@3226 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit aab3a418ac9293bb4abd7670f65d930cb0426d58 1 parent 4ea7a11
Jacob Kaplan-Moss authored June 28, 2006
6  django/conf/global_settings.py
@@ -281,3 +281,9 @@
281 281
 # A tuple of IP addresses that have been banned from participating in various
282 282
 # Django-powered features.
283 283
 BANNED_IPS = ()
  284
+
  285
+##################
  286
+# AUTHENTICATION #
  287
+##################
  288
+
  289
+AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
16  django/contrib/admin/views/decorators.py
... ...
@@ -1,6 +1,7 @@
1 1
 from django import http, template
2 2
 from django.conf import settings
3  
-from django.contrib.auth.models import User, SESSION_KEY
  3
+from django.contrib.auth.models import User
  4
+from django.contrib.auth import authenticate, login
4 5
 from django.shortcuts import render_to_response
5 6
 from django.utils.translation import gettext_lazy
6 7
 import base64, datetime, md5
@@ -69,10 +70,10 @@ def _checklogin(request, *args, **kwargs):
69 70
             return _display_login_form(request, message)
70 71
 
71 72
         # Check the password.
72  
-        username = request.POST.get('username', '')
73  
-        try:
74  
-            user = User.objects.get(username=username, is_staff=True)
75  
-        except User.DoesNotExist:
  73
+        username = request.POST.get('username', None)
  74
+        password = request.POST.get('password', None)
  75
+        user = authenticate(username=username, password=password)
  76
+        if user is None:
76 77
             message = ERROR_MESSAGE
77 78
             if '@' in username:
78 79
                 # Mistakenly entered e-mail address instead of username? Look it up.
@@ -86,8 +87,9 @@ def _checklogin(request, *args, **kwargs):
86 87
 
87 88
         # The user data is correct; log in the user in and continue.
88 89
         else:
89  
-            if user.check_password(request.POST.get('password', '')):
90  
-                request.session[SESSION_KEY] = user.id
  90
+            if user.is_staff:
  91
+                login(request, user)
  92
+                # TODO: set last_login with an event.
91 93
                 user.last_login = datetime.datetime.now()
92 94
                 user.save()
93 95
                 if request.POST.has_key('post_data'):
69  django/contrib/auth/__init__.py
... ...
@@ -1,2 +1,71 @@
  1
+from django.core.exceptions import ImproperlyConfigured
  2
+
  3
+SESSION_KEY = '_auth_user_id'
  4
+BACKEND_SESSION_KEY = '_auth_user_backend'
1 5
 LOGIN_URL = '/accounts/login/'
2 6
 REDIRECT_FIELD_NAME = 'next'
  7
+
  8
+def load_backend(path):
  9
+    i = path.rfind('.')
  10
+    module, attr = path[:i], path[i+1:]
  11
+    try:
  12
+        mod = __import__(module, '', '', [attr])
  13
+    except ImportError, e:
  14
+        raise ImproperlyConfigured, 'Error importing authentication backend %s: "%s"' % (module, e)
  15
+    try:
  16
+        cls = getattr(mod, attr)
  17
+    except AttributeError:
  18
+        raise ImproperlyConfigured, 'Module "%s" does not define a "%s" authentication backend' % (module, attr)
  19
+    return cls()
  20
+
  21
+def get_backends():
  22
+    from django.conf import settings
  23
+    backends = []
  24
+    for backend_path in settings.AUTHENTICATION_BACKENDS:
  25
+        backends.append(load_backend(backend_path))
  26
+    return backends
  27
+
  28
+def authenticate(**credentials):
  29
+    """
  30
+    If the given credentials, return a user object.
  31
+    """
  32
+    for backend in get_backends():
  33
+        try:
  34
+            user = backend.authenticate(**credentials)
  35
+        except TypeError:
  36
+            # this backend doesn't accept these credentials as arguments, try the next one.
  37
+            continue
  38
+        if user is None:
  39
+            continue
  40
+        # annotate the user object with the path of the backend
  41
+        user.backend = str(backend.__class__)
  42
+        return user
  43
+
  44
+def login(request, user):
  45
+    """
  46
+    Persist a user id and a backend in the request. This way a user doesn't
  47
+    have to reauthenticate on every request.
  48
+    """
  49
+    if user is None:
  50
+        user = request.user
  51
+    # TODO: It would be nice to support different login methods, like signed cookies.
  52
+    request.session[SESSION_KEY] = user.id
  53
+    request.session[BACKEND_SESSION_KEY] = user.backend
  54
+
  55
+def logout(request):
  56
+    """
  57
+    Remove the authenticated user's id from request.
  58
+    """
  59
+    del request.session[SESSION_KEY]
  60
+    del request.session[BACKEND_SESSION_KEY]
  61
+
  62
+def get_user(request):
  63
+    from django.contrib.auth.models import AnonymousUser
  64
+    try:
  65
+        user_id = request.session[SESSION_KEY]
  66
+        backend_path = request.session[BACKEND_SESSION_KEY]
  67
+        backend = load_backend(backend_path)
  68
+        user = backend.get_user(user_id) or AnonymousUser()
  69
+    except KeyError:
  70
+        user = AnonymousUser()
  71
+    return user
21  django/contrib/auth/backends.py
... ...
@@ -0,0 +1,21 @@
  1
+from django.contrib.auth.models import User, check_password
  2
+
  3
+class ModelBackend:
  4
+    """
  5
+    Authenticate against django.contrib.auth.models.User
  6
+    """
  7
+    # TODO: Model, login attribute name and password attribute name should be
  8
+    # configurable.
  9
+    def authenticate(self, username=None, password=None):
  10
+        try:
  11
+            user = User.objects.get(username=username)
  12
+            if user.check_password(password):
  13
+                return user
  14
+        except User.DoesNotExist:
  15
+            return None
  16
+
  17
+    def get_user(self, user_id):
  18
+        try:
  19
+            return User.objects.get(pk=user_id)
  20
+        except User.DoesNotExist:
  21
+            return None
16  django/contrib/auth/forms.py
... ...
@@ -1,4 +1,5 @@
1 1
 from django.contrib.auth.models import User
  2
+from django.contrib.auth import authenticate
2 3
 from django.contrib.sites.models import Site
3 4
 from django.template import Context, loader
4 5
 from django.core import validators
@@ -20,8 +21,7 @@ def __init__(self, request=None):
20 21
         self.fields = [
21 22
             forms.TextField(field_name="username", length=15, maxlength=30, is_required=True,
22 23
                 validator_list=[self.isValidUser, self.hasCookiesEnabled]),
23  
-            forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True,
24  
-                validator_list=[self.isValidPasswordForUser]),
  24
+            forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True),
25 25
         ]
26 26
         self.user_cache = None
27 27
 
@@ -30,16 +30,10 @@ def hasCookiesEnabled(self, field_data, all_data):
30 30
             raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
31 31
 
32 32
     def isValidUser(self, field_data, all_data):
33  
-        try:
34  
-            self.user_cache = User.objects.get(username=field_data)
35  
-        except User.DoesNotExist:
36  
-            raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
37  
-
38  
-    def isValidPasswordForUser(self, field_data, all_data):
  33
+        username = field_data
  34
+        password = all_data.get('password', None)
  35
+        self.user_cache = authenticate(username=username, password=password)
39 36
         if self.user_cache is None:
40  
-            return
41  
-        if not self.user_cache.check_password(field_data):
42  
-            self.user_cache = None
43 37
             raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
44 38
         elif not self.user_cache.is_active:
45 39
             raise validators.ValidationError, _("This account is inactive.")
8  django/contrib/auth/middleware.py
@@ -4,12 +4,8 @@ def __init__(self):
4 4
 
5 5
     def __get__(self, request, obj_type=None):
6 6
         if self._user is None:
7  
-            from django.contrib.auth.models import User, AnonymousUser, SESSION_KEY
8  
-            try:
9  
-                user_id = request.session[SESSION_KEY]
10  
-                self._user = User.objects.get(pk=user_id)
11  
-            except (KeyError, User.DoesNotExist):
12  
-                self._user = AnonymousUser()
  7
+            from django.contrib.auth import get_user
  8
+            self._user = get_user(request)
13 9
         return self._user
14 10
 
15 11
 class AuthenticationMiddleware(object):
23  django/contrib/auth/models.py
@@ -4,7 +4,19 @@
4 4
 from django.utils.translation import gettext_lazy as _
5 5
 import datetime
6 6
 
7  
-SESSION_KEY = '_auth_user_id'
  7
+def check_password(raw_password, enc_password):
  8
+    """
  9
+    Returns a boolean of whether the raw_password was correct. Handles
  10
+    encryption formats behind the scenes.
  11
+    """
  12
+    algo, salt, hsh = enc_password.split('$')
  13
+    if algo == 'md5':
  14
+        import md5
  15
+        return hsh == md5.new(salt+raw_password).hexdigest()
  16
+    elif algo == 'sha1':
  17
+        import sha
  18
+        return hsh == sha.new(salt+raw_password).hexdigest()
  19
+    raise ValueError, "Got unknown password algorithm type in password."
8 20
 
9 21
 class SiteProfileNotAvailable(Exception):
10 22
     pass
@@ -141,14 +153,7 @@ def check_password(self, raw_password):
141 153
                 self.set_password(raw_password)
142 154
                 self.save()
143 155
             return is_correct
144  
-        algo, salt, hsh = self.password.split('$')
145  
-        if algo == 'md5':
146  
-            import md5
147  
-            return hsh == md5.new(salt+raw_password).hexdigest()
148  
-        elif algo == 'sha1':
149  
-            import sha
150  
-            return hsh == sha.new(salt+raw_password).hexdigest()
151  
-        raise ValueError, "Got unknown password algorithm type in password."
  156
+        return check_password(raw_password, self.password)
152 157
 
153 158
     def get_group_permissions(self):
154 159
         "Returns a list of permission strings that this user has through his/her groups."
7  django/contrib/auth/views.py
@@ -3,7 +3,6 @@
3 3
 from django import forms
4 4
 from django.shortcuts import render_to_response
5 5
 from django.template import RequestContext
6  
-from django.contrib.auth.models import SESSION_KEY
7 6
 from django.contrib.sites.models import Site
8 7
 from django.http import HttpResponse, HttpResponseRedirect
9 8
 from django.contrib.auth.decorators import login_required
@@ -19,7 +18,8 @@ def login(request, template_name='registration/login.html'):
19 18
             # Light security check -- make sure redirect_to isn't garbage.
20 19
             if not redirect_to or '://' in redirect_to or ' ' in redirect_to:
21 20
                 redirect_to = '/accounts/profile/'
22  
-            request.session[SESSION_KEY] = manipulator.get_user_id()
  21
+            from django.contrib.auth import login
  22
+            login(request, manipulator.get_user())
23 23
             request.session.delete_test_cookie()
24 24
             return HttpResponseRedirect(redirect_to)
25 25
     else:
@@ -33,8 +33,9 @@ def login(request, template_name='registration/login.html'):
33 33
 
34 34
 def logout(request, next_page=None, template_name='registration/logged_out.html'):
35 35
     "Logs out the user and displays 'You are logged out' message."
  36
+    from django.contrib.auth import logout
36 37
     try:
37  
-        del request.session[SESSION_KEY]
  38
+        logout(request)
38 39
     except KeyError:
39 40
         return render_to_response(template_name, {'title': _('Logged out')}, context_instance=RequestContext(request))
40 41
     else:
4  django/contrib/comments/views/comments.py
@@ -5,7 +5,6 @@
5 5
 from django.core.exceptions import ObjectDoesNotExist
6 6
 from django.shortcuts import render_to_response
7 7
 from django.template import RequestContext
8  
-from django.contrib.auth.models import SESSION_KEY
9 8
 from django.contrib.comments.models import Comment, FreeComment, PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
10 9
 from django.contrib.contenttypes.models import ContentType
11 10
 from django.contrib.auth.forms import AuthenticationForm
@@ -219,7 +218,8 @@ def post_comment(request):
219 218
     # If user gave correct username/password and wasn't already logged in, log them in
220 219
     # so they don't have to enter a username/password again.
221 220
     if manipulator.get_user() and new_data.has_key('password') and manipulator.get_user().check_password(new_data['password']):
222  
-        request.session[SESSION_KEY] = manipulator.get_user_id()
  221
+        from django.contrib.auth import login
  222
+        login(request, manipulator.get_user())
223 223
     if errors or request.POST.has_key('preview'):
224 224
         class CommentFormWrapper(forms.FormWrapper):
225 225
             def __init__(self, manipulator, new_data, errors, rating_choices):
109  docs/authentication.txt
@@ -267,17 +267,25 @@ previous section). You can tell them apart with ``is_anonymous()``, like so::
267 267
 How to log a user in
268 268
 --------------------
269 269
 
270  
-To log a user in, do the following within a view::
  270
+Depending on your task, you'll probably want to make sure to validate the 
  271
+user's username and password before you log them in. The easiest way to do so 
  272
+is to use the built-in ``authenticate`` and ``login`` functions from within a 
  273
+view::
  274
+
  275
+    from django.contrib.auth import authenticate, login
  276
+    username = request.POST['username']
  277
+    password = request.POST['password']
  278
+    user = authenticate(username=username, password=password)
  279
+    if user is not None:
  280
+        login(request, user)
  281
+
  282
+``authenticate`` checks the username and password. If they are valid it 
  283
+returns a user object, otherwise it returns ``None``. ``login`` makes it so 
  284
+your users don't have send a username and password for every request. Because 
  285
+the ``login`` function uses sessions, you'll need to make sure you have 
  286
+``SessionMiddleware`` enabled. See the `session documentation`_ for 
  287
+more information.
271 288
 
272  
-    from django.contrib.auth.models import SESSION_KEY
273  
-    request.session[SESSION_KEY] = some_user.id
274  
-
275  
-Because this uses sessions, you'll need to make sure you have
276  
-``SessionMiddleware`` enabled. See the `session documentation`_ for more
277  
-information.
278  
-
279  
-This assumes ``some_user`` is your ``User`` instance. Depending on your task,
280  
-you'll probably want to make sure to validate the user's username and password.
281 289
 
282 290
 Limiting access to logged-in users
283 291
 ----------------------------------
@@ -672,3 +680,84 @@ Finally, note that this messages framework only works with users in the user
672 680
 database. To send messages to anonymous users, use the `session framework`_.
673 681
 
674 682
 .. _session framework: http://www.djangoproject.com/documentation/sessions/
  683
+
  684
+Other Authentication Sources
  685
+============================
  686
+
  687
+Django supports other authentication sources as well. You can even use 
  688
+multiple sources at the same time.
  689
+
  690
+Using multiple backends
  691
+-----------------------
  692
+
  693
+The list of backends to use is controlled by the ``AUTHENTICATION_BACKENDS`` 
  694
+setting. This should be a tuple of python path names. It defaults to 
  695
+``('django.contrib.auth.backends.ModelBackend',)``. To add additional backends
  696
+just add them to your settings.py file. Ordering matters, so if the same
  697
+username and password is valid in multiple backends, the first one in the
  698
+list will return a user object, and the remaining ones won't even get a chance.
  699
+
  700
+Writing an authentication backend
  701
+---------------------------------
  702
+
  703
+An authentication backend is a class that implements 2 methods: 
  704
+``get_user(id)`` and ``authenticate(**credentials)``. The ``get_user`` method 
  705
+takes an id, which could be a username, and database id, whatever, and returns 
  706
+a user object. The  ``authenticate`` method takes credentials as keyword 
  707
+arguments. Many times it will just look like this::
  708
+
  709
+    class MyBackend:
  710
+        def authenticate(username=None, password=None):
  711
+            # check the username/password and return a user
  712
+
  713
+but it could also authenticate a token like so::
  714
+
  715
+    class MyBackend:
  716
+        def authenticate(token=None):
  717
+            # check the token and return a user
  718
+
  719
+Regardless, ``authenticate`` should check the credentials it gets, and if they 
  720
+are valid, it should return a user object that matches those credentials.
  721
+
  722
+The Django admin system is tightly coupled to the Django User object described 
  723
+at the beginning of this document. For now, the best way to deal with this is 
  724
+to create a Django User object for each user that exists for your backend 
  725
+(i.e. in your LDAP directory, your external SQL database, etc.) You can either 
  726
+write a script to do this in advance, or your ``authenticate`` method can do 
  727
+it the first time a user logs in.  Here's an example backend that 
  728
+authenticates against a username and password variable defined in your 
  729
+``settings.py`` file and creates a Django user object the first time they 
  730
+authenticate::
  731
+
  732
+    from django.conf import settings
  733
+    from django.contrib.auth.models import User, check_password
  734
+
  735
+    class SettingsBackend:
  736
+        """
  737
+        Authenticate against vars in settings.py Use the login name, and a hash 
  738
+        of the password. For example:
  739
+
  740
+        ADMIN_LOGIN = 'admin'
  741
+        ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de'
  742
+        """
  743
+        def authenticate(self, username=None, password=None):
  744
+            login_valid = (settings.ADMIN_LOGIN == username)
  745
+            pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
  746
+            if login_valid and pwd_valid:
  747
+                try:
  748
+                    user = User.objects.get(username=username)
  749
+                except User.DoesNotExist:
  750
+                    # Create a new user. Note that we can set password to anything
  751
+                    # as it won't be checked, the password from settings.py will.
  752
+                    user = User(username=username, password='get from settings.py')
  753
+                    user.is_staff = True
  754
+                    user.is_superuser = True
  755
+                    user.save()
  756
+                return user
  757
+            return None
  758
+
  759
+        def get_user(self, user_id):
  760
+            try:
  761
+                return User.objects.get(pk=user_id)
  762
+            except User.DoesNotExist:
  763
+                return None

0 notes on commit aab3a41

Please sign in to comment.
Something went wrong with that request. Please try again.