Changes between Version 9 and Version 10 of MultipleAuthBackends


Ignore:
Timestamp:
03/14/2006 12:32:54 PM (9 years ago)
Author:
jkocherhans
Comment:

much much simpler

Legend:

Unmodified
Added
Removed
Modified
  • MultipleAuthBackends

    v9 v10  
    11= Multiple Authentication Backends =
    22
    3 Currently, Django's authentication system assumes that all your users live in the db as Django models and you must use a username and password to login. This works ok until you realize that you need to support 200 users who already have Active Directory accounts (or insert your favorite windows-free LDAP server here.) In short, Django shouldn't require you to use Django models for users, and it shouldn't require you to use a username and password. What if I want to use an email address, or use some other unique key for my boss REST interface?
     3See http://code.djangoproject.com/ticket/1428 for the current patch that implements this.
    44
    5 Zope has a pretty simple/cool solution for this. It's not really standalone, but it's not that hard to port either. Regardless of what you think of Zope, they've been doing this longer than most of us, and we could probably learn a thing or two from them. Most (meaning nearly all) of these ideas are modeled on zope.app.authentication
     5== Authentication ==
    66
    7 The details are a little flaky, and the naming kinda sucks, please make suggestions. Basically, here's how it goes:
     7The default authentication mechanism for Django will still check a username/password against django.contrib.auth.models.User
    88
    9 == Credentials ==
     9The path to the default authentication backend can be set in settings.py via the AUTHENTICATION_BACKEND variable. This variable is used by {{{django.contrib.auth.middleware.AuthenticationMiddleware}}} to set the request.user attribute.
    1010
    11 Instead of hard-coding checks for a cookie, or a username/password etc. etc. let's let different callables just grab the credentials from the request. In fact, let's put a bunch of these plugins in a list, so if the first one fails, we can try another.
    1211
     12== Backends ==
     13
     14Authentication backends are pretty simple. They just need to implement 2
     15methods, {{{authenticate}}} and {{{get_user}}}.
     16
     17=== backend.authenticate(self, request) ===
     18{{{backend.authenticate}}} takes a request object, and if it finds valid credentials in the request, it returns a user object. If not, it returns None.
     19
     20Note that when you write an authentication backend, you can grab a token, or whatever else you want out of the request, and check against ldap, another sql database, or pretty much anything accessible via python.
     21
     22The user object will generally be an instance of {{{django.contrib.auth.models.User}}}, but really, it could be anything. You will need to at least fake the interface for {{{django.contrib.auth.models.User}}} if you want to use the admin system however. Your backend can create and save an instance of {{{django.contrib.auth.models.User}}} when a user logs in for the first time. You could also add them to a default set of groups at that time.
     23
     24=== backend.get_user(self, user_id) ===
     25
     26{{{backend.get_user}}} simply takes a user id and returns the user that matches that id.
     27
     28
     29== Authenticating ==
     30
     31Here's a very simple code sample that authenticates a user:
    1332
    1433{{{
    1534#!python
    1635
    17 # in settings.py
    18 CREDENTIALS_EXTRACTORS = (
    19     'django.contrib.auth.SessionCredentials',
    20     # etc, etc.
    21 )
     36from django.conf import settings
     37from django.contrib.auth.backends import load_backend
     38
     39def myview(self, request):
     40    backend = load_backend(settings.AUTHENTICATION_BACKEND)
     41    user = backend.authenticate(request)
     42    if user is None:
     43        # do whatever for invalid logins
     44    else:
     45        # the user is valid, persist their id in a session var or whatever.
     46        # do whatever else this view is suppose to do.
    2247}}}
    2348
    24 {{{
    25 #!python
     49Note that the view is in charge of "logging in" a user, that is, the view must persist the id of the user somehow. A session variable is the easiest place, but a signed cookie would be desireable for those who don't want to use the session middleware. It would be nice if this were more convenient. It shouldn't be hard to do.
    2650
    27 class SessionCredentials:
    28         def __init__(self):
    29                 self.cred_key = '_credentials'
    30        
    31         def extract_credentials(self, request):
    32                 # get the credentials from the request and save them to the session
    33                 # if they exist
    34         login = request.POST.get('login', None)
    35                 # we probably want to go ahead and enrypt the password here since
    36                 # we're going to store it in the session.
    37                 password = request.POST.get('password', None)
    38                 if login and password:
    39                         credentials = {'login': login, 'password': password}
    40                         request.session[self.cred_key] = credentials
    41                 else:
    42                         credentials = request.session.get(self.cred_key, None)
    43                 return credentials
    44        
    45         def logout(self, request):
    46                 del self.session[self.credentials_key]
     51For extra points, there should be ways of tying this all in with WSGI ;)
    4752
    48 }}}
     53== Using Multiple Backends ==
    4954
    50 
    51 
    52 == Authentication ==
    53 
    54 Credendials only get us half-way there. We'd better check them against something. How about a list of authentication backends:
    55 
    56 Add this to settings.py:
    57 {{{
    58 #!python
    59 
    60 AUTHENTICATION_PLUGINS = (
    61     'django.contrib.auth.ModelAuthenticator',
    62     'django.contrib.auth.LDAPAuthenticator',
    63 )
    64 }}}
    65 
    66 {{{
    67 #!python
    68 
    69 from django.contrib.auth.models import User
    70 
    71 class ModelAuthenticator:
    72         def authenticate(self, credentials):
    73                 from django.contrib.auth.models import User
    74 
    75                 user = User.objects.get(username=credentials['login'])
    76                 if user.password == credentials['password']:
    77                         return user
    78                 return None
    79 }}}
    80 
    81 So Zope uses a factory to create a user. Your auth plugin can do whatever you feel like as long as it returns a user object. (We need to come up with a minimum informal interface for a user object. Use duck typing, not formal interfaces. The user does not ''need'' to be a django model object, but it ''can'' be. This will cause problems for permissions and groups. Eventually, those should be reworked so they aren't required to be django models either.) So if we're writing an LDAP plugin, we could make one that actually creates a Django model the first time an LDAP user successfully logs in, or we could just assemble and return a plain old python object. We could use events/factories to make this even more abstract and flexible. Let's lay the groundwork first though.
    82 
    83 
    84 == Multiple Backends ==
    85 
    86 Um, so what if I want to use LDAP '''and''' Django models? Funny you should ask. By the way, this is main interface to the authentication system. You won't use credentials and authenticators directly.
    87 
    88 {{{
    89 #!python
    90 
    91 class AuthUtil:
    92         def __init__(self):
    93                 # XXX: these will be configurable in settings.py
    94                 self.cred_extractors = (SessionCredentials(),)
    95                 self.authenticators = (ModelAuthenticator(),)
    96 
    97         def authenticate(self, request):
    98                 for cred_extractor in self.cred_extractors:
    99                         credentials = cred_extractor.extract_credentials(request)
    100                         for authenticator in self.authenticators:
    101                                 user = authenticator.authenticate(credentials)
    102                                 user.cred_extractor = cred_extractor
    103                                 user.authenticator = authenticator
    104                                 return user
    105                 return None
    106 
    107         def logout(self, request):
    108                 for cred_extractor in self.cred_extractors:
    109                         if hasattr(cred_extractor, 'logout'):
    110                                 cred_extractor.logout(request)
    111 }}}
    112 
    113 
    114 == Integration ==
    115 
    116 How will this integrate into Django? I think either the handlers (mod_python, etc.) should start using AuthUtil, or this can be done in decorators. My vote is certainly for the first option.
    117 
    118 
    119 == Suggestions ==
    120 It makes a lot of sense to store a cache of a user object as a django model, no matter what the authentication scheme, so it might very well be useful to just go ahead, reduce the Users models to its barest core, and I would add an (optional) Expiration date for automatic cache/culling (ie, always check the db first, then try everything else).  (It may even be useful to add Expiration dates to permissions/groups.)  I'm also thinking that many of the Credential types should just all be models in their own right with a Many-to-Many relationship with the base User class (a user could be very likely to have multiple Credentials, and there might be the occaisional need for "Group Credentials"). Once the subclassing system is in place, all of the Credential models should be subclasses of each other, probably, for good Pythonic OO-ness.  Probably most DB Credentials would want Expiration dates, too.  This would be a useful generic/easy system for those GET confirmation Credentials that people do.  Finally, keep in mind that not all Credentials will have a username/password combo.  GET Credentials will probably be some random string or hash.  [http://openid.net/ OpenID] Credentials are URLs (tied to remote server response signatures and remote server spam white/blacklists).  Making Credentials first-class models would help make it easier to remove most of the cases where a seperate User model might be necessary (ie, OpenID users could share the same base User model that Django users do, and someone can use the very same base model if they happen to use both). --Max Battcher
    121 
    122 
    123 
    124 I agree that caching a Django user mode would generally be useful, and there should be an easy way to do it, but one shouldn't HAVE to do it. Also, if they do, they should be able to take advantage of memcached, etc. We shouldn't assume that the user is stored in the db.
    125 
    126 To me it doesn't make sense for credentials to be stored as a model... they are just a dict or string or object or whatever that get extracted/assembled from the request. You just pass to the auth backends for checking. The auth backend already persists them. The username/password combo isn't blessed. Credentials could just as well be an api key similar to backpack, or whatever.
    127 
    128 I think these abstractions allow for the use cases you mention, but the details certainly need to be worked out better. I explicitly want to ''avoid'' coupling this to django models, but it should ''easy'' to do so since it's most likely the default case. --Joseph Kocherhans
    129 
    130 
    131 Here is how they approached this problem in Zope3: [http://www.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/Zope3Book/principalplugin.html].  You can find additional resources here: [http://www.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/Zope3Book] -- Linicks
    132 
    133 I don't believe zope does things that way anymore. That first link looks to be more a step along the way than a final implementation. --Joseph Kocherhans
     55To use multiple authentication backends, you just use another backend called {{{django.contrib.auth.backends.MultiBackend}}} It is configured with a list of backends you'd like to use, and just calls each backend in order.
Back to Top