I put this in a file called auth.py, then referenced it in the settings.py like so:
AUTHENTICATION_BACKENDS = ('myproject.myapp.auth.ActiveDirectoryBackend',)
This has been tested on my office network, with the following setup:
Django 0.96
Python 2.4.4
python-ldap
Fedora Core 5 (On the server hosting Django)
AD Native Mode
2 Windows 2003 AD servers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | -- Added to settings.py --
### ACTIVE DIRECTORY SETTINGS
# AD_DNS_NAME should set to the AD DNS name of the domain (ie; example.com)
# If you are not using the AD server as your DNS, it can also be set to
# FQDN or IP of the AD server.
AD_DNS_NAME = 'example.com'
AD_LDAP_PORT = 389
AD_SEARCH_DN = 'CN=Users,dc=example,dc=com'
# This is the NT4/Samba domain name
AD_NT4_DOMAIN = 'EXAMPLE'
AD_SEARCH_FIELDS = ['mail','givenName','sn','sAMAccountName']
AD_LDAP_URL = 'ldap://%s:%s' % (AD_DNS_NAME,AD_LDAP_PORT)
-- In the auth.py file --
from django.contrib.auth.models import User
from django.conf import settings
import ldap
class ActiveDirectoryBackend:
def authenticate(self,username=None,password=None):
if not self.is_valid(username,password):
return None
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
l = ldap.initialize(settings.AD_LDAP_URL)
l.simple_bind_s(username,password)
result = l.search_ext_s(settings.AD_SEARCH_DN,ldap.SCOPE_SUBTREE,
"sAMAccountName=%s" % username,settings.AD_SEARCH_FIELDS)[0][1]
l.unbind_s()
# givenName == First Name
if result.has_key('givenName'):
first_name = result['givenName'][0]
else:
first_name = None
# sn == Last Name (Surname)
if result.has_key('sn'):
last_name = result['sn'][0]
else:
last_name = None
# mail == Email Address
if result.has_key('mail'):
email = result['mail'][0]
else:
email = None
user = User(username=username,first_name=first_name,last_name=last_name,email=email)
user.is_staff = False
user.is_superuser = False
user.set_password(password)
user.save()
return user
def get_user(self,user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
def is_valid (self,username=None,password=None):
## Disallowing null or blank string as password
## as per comment: http://www.djangosnippets.org/snippets/501/#c868
if password == None or password == '':
return False
binddn = "%s@%s" % (username,settings.AD_NT4_DOMAIN)
try:
l = ldap.initialize(settings.AD_LDAP_URL)
l.simple_bind_s(binddn,password)
l.unbind_s()
return True
except ldap.LDAPError:
return False
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
I had this on my to-do list for quite some time, thanks for sharing.
#
Very nice, thanks. Perhaps it should fetch the configuration variables from the settings module though.
#
Yeah, I was thinking about that. I use ConfigParser and an external .ini file in my installation. For the snippet, I just wanted to have everything in the same place for readability. I'll update once I figure out how to move the variables to the settings module.
#
I've gone ahead and moved it to the settings.py and updated the snippet to reflect this. I also updated the variable names so that they're a little more settings.py-ish and removed the unnecessary call to urlunparse.
#
Forgive my ignorance, but I just want to clarify that this authenticates against the AD server each time a user logs in, not just the first?
#
Yes, since the is_valid method is called on every authentication, it will attempt to bind each time a user logs in.
One thing I was thinking of was changing the user.is_staff and the user.is_superuser to grab that info based on group membership in AD. Maybe a mapping of Domain Users to is_staff and Administrators to is_superuser?
#
I think it would be good to tag this snippet as 'auth' or so, as other authentification snippets do.
#
BE CAREFUL with line 83. Don't allow a blank or null password.
Here's possible scenario:
(1) enter a valid username and blank password
(2) "is_valid" (line 78) returns a True (according to LDAP protocol)
(3) the user's fname, lname, email is found via the search (line 44)
(4) the user (with a blank pw) becomes a superuser
(Thanks for the snippet!)
#
Thanks for the heads up -- I'm fixing the snippet here. If you have a good suggestion on how to better manage staff/superusers , I'd be glad to integrate it here. I'm testing locally an idea I had about group membership..basically Domain Users will be is_staff, and Administrators will be is_superuser. Once I have it working correctly, I'll post it here.
#
Foremost thanks for you snippet.
Why not to use user's login and password for binding in authenticate() instead of creating GuestBindAccount?
ldap.open is deprecated, so ldap.initialize would be better to use, IMHO.
To my mind
user.is_staff = False and user.is_superuser = False
as defaults are safer. Mapping to domain groups is not obviously suits to every project.
#
@max3: Thank you for your thanks! To answer your points:
1) In the authenticate method, the self.is_valid call uses the user's login and password to authenticate. If it fails due to the user not existing (User.DoesNotExist), the method still needs to be able to bind to be complete the task, thus the GuestBindAccount credentials.
2) Thanks for the heads up. I'm reading up on the usage for ldap.initialize and will update this snippet shortly.
3) I've changed the snippet to make user.is_staff and user.is_superuser False by default. You're right, not every project will have clean group mappings for these attributes.
#
continued from above post..
Duh :) I'm way off. Of course the user will exist. It's been a few months since I've really looked at this snippet deeply. I'll go ahead and make the changes to the snippet to remove the need for the GuestBindAccount. Thanks max3!
#
I'm having a dead brain day. Didn't realize that I was already using ldap.initialize earlier. I went ahead and removed the requirement for the GuestBindAccount and also realized that I didn't unbind ldap in the authenticate method.
Sorry for the comment spam!
#
Could you please explain why the
is_valid
method uses the AD_NT4_DOMAIN when binding whereas authenticate uses only the username? I'm getting an invalid credentials error after passing theis_valid
test when binding for the second time to find the user details.{'info': '80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 525, vece', 'desc': 'Invalid credentials'}
If I change either of the binds to match the other I receive the following error:
{'info': '00000000: LdapErr: DSID-0C090627, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, vece', 'desc': 'Operations error'}
I've tried both styles of username; the ones we log into AD with "Forename.Surname" and "Surname, Forename". Each one works for the appropriate bind call but then fails on the search.
Any ideas? Thanks.
#
I had the same initial problem as ascott, but I was able to remedy it with the fix he initially suggested.
At line 14 of auth.py, instead of:
I now have:
Works like a charm for me otherwise! Thanks for the snippet!
#
It seems like this snippet currently creates a user even if it already exists.
Shouldn't it return user on a succesful User.objects.get?
#
In fact, it seems as if the user does exist, then result is never set and auth throws an exception.
#
This is just an outstanding snippet. I've found that if the user doesn't exist in Django, though, there can be a really long wait and an eventual timeout before the search function returns. I don't think this is necessarily common, but it's happening on searches against my AD server. I found that the delay can be avoided by preventing the search from following referrals. Right after "l = ldap.initialize()", set this option:
If you're having trouble with long delays and timeouts, try this. It cured the problem for me.
#
谢谢了,很好用
#
When using this code with the django admin, I had an issue with permissions. Unless superuser, the admin would always say "You don't have permission to edit anything" despite what was in the database. (using django 1.2)
The solution was to extend the default model backend:
and then get_all_permissions, has_perm, has_module_perms worked like they should. Also, defining "get_user" was no longer necessary.
#
If anyone runs into the same issue as ascott or Tarken, you should be able to change the section on creating a user from:
to
To get it to work correctly.
#
Thanks for this! It was a huge help for me. One thing that was causing me trouble, though, is that Windows allows the username to be case-insensitive. I had some trouble with my end-users taking advantage of this (and getting multiple accounts). To address this, I wrote the following patch to force a consistent (lower) case in the Django Users database:
#
After ldap.initialize, you should do:
l.set_option(ldap.OPT_REFERRALS, 0)
otherwise you may get errors like: {'info': '00000000: LdapErr: DSID-0C090627, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, vece', 'desc': 'Operations error'}
#
Please login first before commenting.