Merged multi-auth branch to trunk. See the authentication docs for the 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
This commit is contained in:
Jacob Kaplan-Moss 2006-06-28 16:37:02 +00:00
parent 4ea7a11659
commit aab3a418ac
10 changed files with 229 additions and 46 deletions

View File

@ -281,3 +281,9 @@ COMMENTS_FIRST_FEW = 0
# A tuple of IP addresses that have been banned from participating in various # A tuple of IP addresses that have been banned from participating in various
# Django-powered features. # Django-powered features.
BANNED_IPS = () BANNED_IPS = ()
##################
# AUTHENTICATION #
##################
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)

View File

@ -1,6 +1,7 @@
from django import http, template from django import http, template
from django.conf import settings 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.shortcuts import render_to_response
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
import base64, datetime, md5 import base64, datetime, md5
@ -69,10 +70,10 @@ def staff_member_required(view_func):
return _display_login_form(request, message) return _display_login_form(request, message)
# Check the password. # Check the password.
username = request.POST.get('username', '') username = request.POST.get('username', None)
try: password = request.POST.get('password', None)
user = User.objects.get(username=username, is_staff=True) user = authenticate(username=username, password=password)
except User.DoesNotExist: if user is None:
message = ERROR_MESSAGE message = ERROR_MESSAGE
if '@' in username: if '@' in username:
# Mistakenly entered e-mail address instead of username? Look it up. # Mistakenly entered e-mail address instead of username? Look it up.
@ -86,8 +87,9 @@ def staff_member_required(view_func):
# The user data is correct; log in the user in and continue. # The user data is correct; log in the user in and continue.
else: else:
if user.check_password(request.POST.get('password', '')): if user.is_staff:
request.session[SESSION_KEY] = user.id login(request, user)
# TODO: set last_login with an event.
user.last_login = datetime.datetime.now() user.last_login = datetime.datetime.now()
user.save() user.save()
if request.POST.has_key('post_data'): if request.POST.has_key('post_data'):

View File

@ -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/' LOGIN_URL = '/accounts/login/'
REDIRECT_FIELD_NAME = 'next' 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 File

@ -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 File

@ -1,4 +1,5 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth import authenticate
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.template import Context, loader from django.template import Context, loader
from django.core import validators from django.core import validators
@ -20,8 +21,7 @@ class AuthenticationForm(forms.Manipulator):
self.fields = [ self.fields = [
forms.TextField(field_name="username", length=15, maxlength=30, is_required=True, forms.TextField(field_name="username", length=15, maxlength=30, is_required=True,
validator_list=[self.isValidUser, self.hasCookiesEnabled]), validator_list=[self.isValidUser, self.hasCookiesEnabled]),
forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True, forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True),
validator_list=[self.isValidPasswordForUser]),
] ]
self.user_cache = None self.user_cache = None
@ -30,16 +30,10 @@ class AuthenticationForm(forms.Manipulator):
raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.") 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): def isValidUser(self, field_data, all_data):
try: username = field_data
self.user_cache = User.objects.get(username=field_data) password = all_data.get('password', None)
except User.DoesNotExist: self.user_cache = authenticate(username=username, password=password)
raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
def isValidPasswordForUser(self, field_data, all_data):
if self.user_cache is None: 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.") raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
elif not self.user_cache.is_active: elif not self.user_cache.is_active:
raise validators.ValidationError, _("This account is inactive.") raise validators.ValidationError, _("This account is inactive.")

View File

@ -4,12 +4,8 @@ class LazyUser(object):
def __get__(self, request, obj_type=None): def __get__(self, request, obj_type=None):
if self._user is None: if self._user is None:
from django.contrib.auth.models import User, AnonymousUser, SESSION_KEY from django.contrib.auth import get_user
try: self._user = get_user(request)
user_id = request.session[SESSION_KEY]
self._user = User.objects.get(pk=user_id)
except (KeyError, User.DoesNotExist):
self._user = AnonymousUser()
return self._user return self._user
class AuthenticationMiddleware(object): class AuthenticationMiddleware(object):

View File

@ -4,7 +4,19 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import datetime 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): class SiteProfileNotAvailable(Exception):
pass pass
@ -141,14 +153,7 @@ class User(models.Model):
self.set_password(raw_password) self.set_password(raw_password)
self.save() self.save()
return is_correct return is_correct
algo, salt, hsh = self.password.split('$') return check_password(raw_password, self.password)
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."
def get_group_permissions(self): def get_group_permissions(self):
"Returns a list of permission strings that this user has through his/her groups." "Returns a list of permission strings that this user has through his/her groups."

View File

@ -3,7 +3,6 @@ from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm
from django import forms from django import forms
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.template import RequestContext from django.template import RequestContext
from django.contrib.auth.models import SESSION_KEY
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required 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. # Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or '://' in redirect_to or ' ' in redirect_to: if not redirect_to or '://' in redirect_to or ' ' in redirect_to:
redirect_to = '/accounts/profile/' 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() request.session.delete_test_cookie()
return HttpResponseRedirect(redirect_to) return HttpResponseRedirect(redirect_to)
else: 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'): def logout(request, next_page=None, template_name='registration/logged_out.html'):
"Logs out the user and displays 'You are logged out' message." "Logs out the user and displays 'You are logged out' message."
from django.contrib.auth import logout
try: try:
del request.session[SESSION_KEY] logout(request)
except KeyError: except KeyError:
return render_to_response(template_name, {'title': _('Logged out')}, context_instance=RequestContext(request)) return render_to_response(template_name, {'title': _('Logged out')}, context_instance=RequestContext(request))
else: else:

View File

@ -5,7 +5,6 @@ from django.http import Http404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.template import RequestContext 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.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.contenttypes.models import ContentType
from django.contrib.auth.forms import AuthenticationForm 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 # 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. # 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']): 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'): if errors or request.POST.has_key('preview'):
class CommentFormWrapper(forms.FormWrapper): class CommentFormWrapper(forms.FormWrapper):
def __init__(self, manipulator, new_data, errors, rating_choices): def __init__(self, manipulator, new_data, errors, rating_choices):

View File

@ -267,17 +267,25 @@ previous section). You can tell them apart with ``is_anonymous()``, like so::
How to log a user in 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.models import SESSION_KEY from django.contrib.auth import authenticate, login
request.session[SESSION_KEY] = some_user.id username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
Because this uses sessions, you'll need to make sure you have ``authenticate`` checks the username and password. If they are valid it
``SessionMiddleware`` enabled. See the `session documentation`_ for more returns a user object, otherwise it returns ``None``. ``login`` makes it so
information. 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.
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 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`_. database. To send messages to anonymous users, use the `session framework`_.
.. _session framework: http://www.djangoproject.com/documentation/sessions/ .. _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