Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
@jacobian jacobian authored
View
6 django/conf/global_settings.py
@@ -281,3 +281,9 @@
# A tuple of IP addresses that have been banned from participating in various
# Django-powered features.
BANNED_IPS = ()
+
+##################
+# AUTHENTICATION #
+##################
+
+AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
View
16 django/contrib/admin/views/decorators.py
@@ -1,6 +1,7 @@
from django import http, template
from django.conf import settings
-from django.contrib.auth.models import User, SESSION_KEY
+from django.contrib.auth.models import User
+from django.contrib.auth import authenticate, login
from django.shortcuts import render_to_response
from django.utils.translation import gettext_lazy
import base64, datetime, md5
@@ -69,10 +70,10 @@ def _checklogin(request, *args, **kwargs):
return _display_login_form(request, message)
# Check the password.
- username = request.POST.get('username', '')
- try:
- user = User.objects.get(username=username, is_staff=True)
- except User.DoesNotExist:
+ username = request.POST.get('username', None)
+ password = request.POST.get('password', None)
+ user = authenticate(username=username, password=password)
+ if user is None:
message = ERROR_MESSAGE
if '@' in username:
# Mistakenly entered e-mail address instead of username? Look it up.
@@ -86,8 +87,9 @@ def _checklogin(request, *args, **kwargs):
# The user data is correct; log in the user in and continue.
else:
- if user.check_password(request.POST.get('password', '')):
- request.session[SESSION_KEY] = user.id
+ if user.is_staff:
+ login(request, user)
+ # TODO: set last_login with an event.
user.last_login = datetime.datetime.now()
user.save()
if request.POST.has_key('post_data'):
View
69 django/contrib/auth/__init__.py
@@ -1,2 +1,71 @@
+from django.core.exceptions import ImproperlyConfigured
+
+SESSION_KEY = '_auth_user_id'
+BACKEND_SESSION_KEY = '_auth_user_backend'
LOGIN_URL = '/accounts/login/'
REDIRECT_FIELD_NAME = 'next'
+
+def load_backend(path):
+ i = path.rfind('.')
+ module, attr = path[:i], path[i+1:]
+ try:
+ mod = __import__(module, '', '', [attr])
+ except ImportError, e:
+ raise ImproperlyConfigured, 'Error importing authentication backend %s: "%s"' % (module, e)
+ try:
+ cls = getattr(mod, attr)
+ except AttributeError:
+ raise ImproperlyConfigured, 'Module "%s" does not define a "%s" authentication backend' % (module, attr)
+ return cls()
+
+def get_backends():
+ from django.conf import settings
+ backends = []
+ for backend_path in settings.AUTHENTICATION_BACKENDS:
+ backends.append(load_backend(backend_path))
+ return backends
+
+def authenticate(**credentials):
+ """
+ If the given credentials, return a user object.
+ """
+ for backend in get_backends():
+ try:
+ user = backend.authenticate(**credentials)
+ except TypeError:
+ # this backend doesn't accept these credentials as arguments, try the next one.
+ continue
+ if user is None:
+ continue
+ # annotate the user object with the path of the backend
+ user.backend = str(backend.__class__)
+ return user
+
+def login(request, user):
+ """
+ Persist a user id and a backend in the request. This way a user doesn't
+ have to reauthenticate on every request.
+ """
+ if user is None:
+ user = request.user
+ # TODO: It would be nice to support different login methods, like signed cookies.
+ request.session[SESSION_KEY] = user.id
+ request.session[BACKEND_SESSION_KEY] = user.backend
+
+def logout(request):
+ """
+ Remove the authenticated user's id from request.
+ """
+ del request.session[SESSION_KEY]
+ del request.session[BACKEND_SESSION_KEY]
+
+def get_user(request):
+ from django.contrib.auth.models import AnonymousUser
+ try:
+ user_id = request.session[SESSION_KEY]
+ backend_path = request.session[BACKEND_SESSION_KEY]
+ backend = load_backend(backend_path)
+ user = backend.get_user(user_id) or AnonymousUser()
+ except KeyError:
+ user = AnonymousUser()
+ return user
View
21 django/contrib/auth/backends.py
@@ -0,0 +1,21 @@
+from django.contrib.auth.models import User, check_password
+
+class ModelBackend:
+ """
+ Authenticate against django.contrib.auth.models.User
+ """
+ # TODO: Model, login attribute name and password attribute name should be
+ # configurable.
+ def authenticate(self, username=None, password=None):
+ try:
+ user = User.objects.get(username=username)
+ if user.check_password(password):
+ return user
+ except User.DoesNotExist:
+ return None
+
+ def get_user(self, user_id):
+ try:
+ return User.objects.get(pk=user_id)
+ except User.DoesNotExist:
+ return None
View
16 django/contrib/auth/forms.py
@@ -1,4 +1,5 @@
from django.contrib.auth.models import User
+from django.contrib.auth import authenticate
from django.contrib.sites.models import Site
from django.template import Context, loader
from django.core import validators
@@ -20,8 +21,7 @@ def __init__(self, request=None):
self.fields = [
forms.TextField(field_name="username", length=15, maxlength=30, is_required=True,
validator_list=[self.isValidUser, self.hasCookiesEnabled]),
- forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True,
- validator_list=[self.isValidPasswordForUser]),
+ forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True),
]
self.user_cache = None
@@ -30,16 +30,10 @@ def hasCookiesEnabled(self, field_data, all_data):
raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
def isValidUser(self, field_data, all_data):
- try:
- self.user_cache = User.objects.get(username=field_data)
- except User.DoesNotExist:
- raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
-
- def isValidPasswordForUser(self, field_data, all_data):
+ username = field_data
+ password = all_data.get('password', None)
+ self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None:
- return
- if not self.user_cache.check_password(field_data):
- self.user_cache = None
raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
elif not self.user_cache.is_active:
raise validators.ValidationError, _("This account is inactive.")
View
8 django/contrib/auth/middleware.py
@@ -4,12 +4,8 @@ def __init__(self):
def __get__(self, request, obj_type=None):
if self._user is None:
- from django.contrib.auth.models import User, AnonymousUser, SESSION_KEY
- try:
- user_id = request.session[SESSION_KEY]
- self._user = User.objects.get(pk=user_id)
- except (KeyError, User.DoesNotExist):
- self._user = AnonymousUser()
+ from django.contrib.auth import get_user
+ self._user = get_user(request)
return self._user
class AuthenticationMiddleware(object):
View
23 django/contrib/auth/models.py
@@ -4,7 +4,19 @@
from django.utils.translation import gettext_lazy as _
import datetime
-SESSION_KEY = '_auth_user_id'
+def check_password(raw_password, enc_password):
+ """
+ Returns a boolean of whether the raw_password was correct. Handles
+ encryption formats behind the scenes.
+ """
+ algo, salt, hsh = enc_password.split('$')
+ if algo == 'md5':
+ import md5
+ return hsh == md5.new(salt+raw_password).hexdigest()
+ elif algo == 'sha1':
+ import sha
+ return hsh == sha.new(salt+raw_password).hexdigest()
+ raise ValueError, "Got unknown password algorithm type in password."
class SiteProfileNotAvailable(Exception):
pass
@@ -141,14 +153,7 @@ def check_password(self, raw_password):
self.set_password(raw_password)
self.save()
return is_correct
- algo, salt, hsh = self.password.split('$')
- if algo == 'md5':
- import md5
- return hsh == md5.new(salt+raw_password).hexdigest()
- elif algo == 'sha1':
- import sha
- return hsh == sha.new(salt+raw_password).hexdigest()
- raise ValueError, "Got unknown password algorithm type in password."
+ return check_password(raw_password, self.password)
def get_group_permissions(self):
"Returns a list of permission strings that this user has through his/her groups."
View
7 django/contrib/auth/views.py
@@ -3,7 +3,6 @@
from django import forms
from django.shortcuts import render_to_response
from django.template import RequestContext
-from django.contrib.auth.models import SESSION_KEY
from django.contrib.sites.models import Site
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
@@ -19,7 +18,8 @@ def login(request, template_name='registration/login.html'):
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or '://' in redirect_to or ' ' in redirect_to:
redirect_to = '/accounts/profile/'
- request.session[SESSION_KEY] = manipulator.get_user_id()
+ from django.contrib.auth import login
+ login(request, manipulator.get_user())
request.session.delete_test_cookie()
return HttpResponseRedirect(redirect_to)
else:
@@ -33,8 +33,9 @@ def login(request, template_name='registration/login.html'):
def logout(request, next_page=None, template_name='registration/logged_out.html'):
"Logs out the user and displays 'You are logged out' message."
+ from django.contrib.auth import logout
try:
- del request.session[SESSION_KEY]
+ logout(request)
except KeyError:
return render_to_response(template_name, {'title': _('Logged out')}, context_instance=RequestContext(request))
else:
View
4 django/contrib/comments/views/comments.py
@@ -5,7 +5,6 @@
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import render_to_response
from django.template import RequestContext
-from django.contrib.auth.models import SESSION_KEY
from django.contrib.comments.models import Comment, FreeComment, PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.forms import AuthenticationForm
@@ -219,7 +218,8 @@ def post_comment(request):
# If user gave correct username/password and wasn't already logged in, log them in
# so they don't have to enter a username/password again.
if manipulator.get_user() and new_data.has_key('password') and manipulator.get_user().check_password(new_data['password']):
- request.session[SESSION_KEY] = manipulator.get_user_id()
+ from django.contrib.auth import login
+ login(request, manipulator.get_user())
if errors or request.POST.has_key('preview'):
class CommentFormWrapper(forms.FormWrapper):
def __init__(self, manipulator, new_data, errors, rating_choices):
View
109 docs/authentication.txt
@@ -267,17 +267,25 @@ previous section). You can tell them apart with ``is_anonymous()``, like so::
How to log a user in
--------------------
-To log a user in, do the following within a view::
+Depending on your task, you'll probably want to make sure to validate the
+user's username and password before you log them in. The easiest way to do so
+is to use the built-in ``authenticate`` and ``login`` functions from within a
+view::
+
+ from django.contrib.auth import authenticate, login
+ username = request.POST['username']
+ password = request.POST['password']
+ user = authenticate(username=username, password=password)
+ if user is not None:
+ login(request, user)
+
+``authenticate`` checks the username and password. If they are valid it
+returns a user object, otherwise it returns ``None``. ``login`` makes it so
+your users don't have send a username and password for every request. Because
+the ``login`` function uses sessions, you'll need to make sure you have
+``SessionMiddleware`` enabled. See the `session documentation`_ for
+more information.
- from django.contrib.auth.models import SESSION_KEY
- request.session[SESSION_KEY] = some_user.id
-
-Because this uses sessions, you'll need to make sure you have
-``SessionMiddleware`` enabled. See the `session documentation`_ for more
-information.
-
-This assumes ``some_user`` is your ``User`` instance. Depending on your task,
-you'll probably want to make sure to validate the user's username and password.
Limiting access to logged-in users
----------------------------------
@@ -672,3 +680,84 @@ Finally, note that this messages framework only works with users in the user
database. To send messages to anonymous users, use the `session framework`_.
.. _session framework: http://www.djangoproject.com/documentation/sessions/
+
+Other Authentication Sources
+============================
+
+Django supports other authentication sources as well. You can even use
+multiple sources at the same time.
+
+Using multiple backends
+-----------------------
+
+The list of backends to use is controlled by the ``AUTHENTICATION_BACKENDS``
+setting. This should be a tuple of python path names. It defaults to
+``('django.contrib.auth.backends.ModelBackend',)``. To add additional backends
+just add them to your settings.py file. Ordering matters, so if the same
+username and password is valid in multiple backends, the first one in the
+list will return a user object, and the remaining ones won't even get a chance.
+
+Writing an authentication backend
+---------------------------------
+
+An authentication backend is a class that implements 2 methods:
+``get_user(id)`` and ``authenticate(**credentials)``. The ``get_user`` method
+takes an id, which could be a username, and database id, whatever, and returns
+a user object. The ``authenticate`` method takes credentials as keyword
+arguments. Many times it will just look like this::
+
+ class MyBackend:
+ def authenticate(username=None, password=None):
+ # check the username/password and return a user
+
+but it could also authenticate a token like so::
+
+ class MyBackend:
+ def authenticate(token=None):
+ # check the token and return a user
+
+Regardless, ``authenticate`` should check the credentials it gets, and if they
+are valid, it should return a user object that matches those credentials.
+
+The Django admin system is tightly coupled to the Django User object described
+at the beginning of this document. For now, the best way to deal with this is
+to create a Django User object for each user that exists for your backend
+(i.e. in your LDAP directory, your external SQL database, etc.) You can either
+write a script to do this in advance, or your ``authenticate`` method can do
+it the first time a user logs in. Here's an example backend that
+authenticates against a username and password variable defined in your
+``settings.py`` file and creates a Django user object the first time they
+authenticate::
+
+ from django.conf import settings
+ from django.contrib.auth.models import User, check_password
+
+ class SettingsBackend:
+ """
+ Authenticate against vars in settings.py Use the login name, and a hash
+ of the password. For example:
+
+ ADMIN_LOGIN = 'admin'
+ ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de'
+ """
+ def authenticate(self, username=None, password=None):
+ login_valid = (settings.ADMIN_LOGIN == username)
+ pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
+ if login_valid and pwd_valid:
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ # Create a new user. Note that we can set password to anything
+ # as it won't be checked, the password from settings.py will.
+ user = User(username=username, password='get from settings.py')
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+ return user
+ return None
+
+ def get_user(self, user_id):
+ try:
+ return User.objects.get(pk=user_id)
+ except User.DoesNotExist:
+ return None
Please sign in to comment.
Something went wrong with that request. Please try again.