Skip to content

Commit

Permalink
Merged multi-auth branch to trunk. See the authentication docs for th…
Browse files Browse the repository at this point in the history
…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
jacobian committed Jun 28, 2006
1 parent 4ea7a11 commit aab3a41
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 48 deletions.
6 changes: 6 additions & 0 deletions django/conf/global_settings.py
Expand Up @@ -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',)
16 changes: 9 additions & 7 deletions 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
Expand Down Expand Up @@ -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.
Expand All @@ -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'):
Expand Down
69 changes: 69 additions & 0 deletions 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
21 changes: 21 additions & 0 deletions 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
16 changes: 5 additions & 11 deletions 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
Expand All @@ -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

Expand All @@ -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.")
Expand Down
8 changes: 2 additions & 6 deletions django/contrib/auth/middleware.py
Expand Up @@ -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):
Expand Down
23 changes: 14 additions & 9 deletions django/contrib/auth/models.py
Expand Up @@ -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
Expand Down Expand Up @@ -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."
Expand Down
7 changes: 4 additions & 3 deletions django/contrib/auth/views.py
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions django/contrib/comments/views/comments.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
109 changes: 99 additions & 10 deletions docs/authentication.txt
Expand Up @@ -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
----------------------------------
Expand Down Expand Up @@ -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

0 comments on commit aab3a41

Please sign in to comment.