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:
parent
4ea7a11659
commit
aab3a418ac
|
@ -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',)
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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.")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue