UPDATE: 2015-02-25 Today I nearly crapped a cow. I was testing out a custom ErrorDocument 401
directive that would redirect back to the sign in page (BTW: that's a bad idea, IE & Firefox sign on windows are modal). I clicked OK with empty username and empty password fields, and I got the dreaded Internal Sever Error HTTP/1.1 500 page. Then because the browser had cached the empty creds, I could not get back on the server. Clearing the cache and browser history had no effect. I actually thought I had broken Apache! Stack Exchange ServerFault to the rescue. The fix is to set AuthLDAPBindAuthoritative off
in
TL;DR
This is surprisingly easy, although there is some new syntax to learn, and you will need to get some info from your system administrator. Here are some steps for Apache-2.4 from ApacheLounge.- Follow the directions in the Django documentation on Authentication using REMOTE_USER and add
RemoteUserMiddleware
andRemoteUserBackend
toAUTHENTICATION_BACKENDS
to your settings file. This will use theREMOTE_USER
environment variable set by Apache when it authorizes users and use it for authentication on the Django website. - Get the URL or IP address of your Active Directory server from your system administrator. For LDAP with basic authentication, the port is usually 389, but check to make sure.
- Also get the "Distringuished Name" of the "search base" from your system administrator. A "Distringuished Name" is LDAP lingo for a string made up of several components, usually the "Organizational Unit (OU)" and the "Domain Components (DC)", that distinguish entries in the Active Directory.
- Finally ask your system administrator to set up a "binding" distinguished name and password to authorize searches of the Active Directory.
- Then in
httpd.conf
enablemod_authnz_ldap
andmod_ldap
. - Also in
httpd.conf
add aLocation
for the URL endpoint, EG:/
for the entire website, to be password protected. - You must set
AuthName
. This will be displayed to the user when they are prompted to enter their credentials. - Also must also set
AuthType
,AuthBasicProvider
,AuthLDAPUrl
andRequire
. Prependldap://
to your AD server name and append the port, base DN, scope, attribute and search filter. The port is separated by a colon (:), the base DN by a slash (/) and the other parameters by question marks (?) such as:ldap://host:port/basedn?attribute?scope?filter
- The "attribute" to search for in Windows Active Directory is "SAM-Account-Name" or
sAMAccountName
. This is the equivalent of a user name. - The default "scope" is
sub
which means it will search the base DN and everything below it in the Active Directory. And the default "filter" is(objectClass=*)
which is the equivalent of no filter. - There are several options for limiting users and groups. If you set
Require
tovalid-user
then any user in the AD who can authenticate will be authorized. - Set
AuthLDAPBindDN
andAuthLDAPBindPassword
to the binding account's DN and password. - It has been reported that
LDAPReferrals
should be set to off or you may get the following error.(70023)This function has not been implemented on this platform: AH01277: LDAP: Unable to add rebind cross reference entry. Out of memory?
- Finally, restart your Apache httpd server and test out your site.
Note: This will change how Django works; for example, any authorized user not in the Django Users model will have their username automatically added and set to active, but their password and the
is_staff
attribute will not be set.
<Location /> AuthName "Please enter your SSO credentials." AuthBasicProvider ldap AuthType basic AuthLDAPUrl "ldap://my.activedirectory.com:389/OU=Offices,DC=activedirectory,DC=com?sAMAccountName" AuthLDAPBindDN "CN=binding_account,OU=Administrators,DC=activedirectory,DC=com" AuthLDAPBindPassword binding_password AuthLDAPBindAuthoritative off LDAPReferrals off Require valid-user </Location>
Loggout
In addition to adding authenticated users to the Django Users model, the users credentials are stored in the browser. This makes logging out akward since the user will need to close their browser to logout. There are several approaches to get Django to logout a user.- redirect the user to a URL with fake basic authentication prepended to the path.
- render a template with status set to 401 which is the code for unauthorized that will clear the credentials in browser cache.
http://log:out@example.com
from django.shortcuts import render from django.contrib.auth import logout as auth_logout import logging # import the logging library logger = logging.getLogger(__name__) # Get an instance of a logger def logout(request): """ Replaces ``django.contrib.auth.views.logout``. """ logger.debug('user %s logging out', request.user.username) auth_logout(request) return render(request, 'index.html', status=401)
Using Telnet to ping AD server
A lot of sites suggest this. First you will need to enable Telnet on your Windows PC. This can be done from Uninstall a program in the Control Panel by selecting Turn Windows features on or off and checking Telnet Client. Then opening a command terminal and typingtelnet
followed by open my.activedirectory.com 389
. Surprise! If it works you will only see the output:
Connecting to my.activedirectory.com...
If it does not work then you will see this additional output:
Could not open connection to the host, on port 389: Connect failed
Now treat yourself and try open towel.blinkenlights.nl
. Use control + ] to kill the connection, then type quit
to quit telnet.
Testing LDAP using Python
- Python-LDAP So to learn more about LDAP there are a couple of packages that you can use to interrogate and authenticate with and AD server using LDAP. Python-LDAP seems to be common and easy to use. It's based on OpenLDAP Here's a list of common LDAP Queries from Google.
>>> import ldap >>> server = ldap.initialize('ldap://my.activedirectory.com:389') >>> server.simple_bind('CN=bind_user,OU=Administrators,DC=activedirectory,DC=com','bind_password') # returns 1 on success 1 >>> user = server.search_s('OU=Users,DC=activedirectory,DC=com',ldap.SCOPE_SUBTREE,'(&(sAMAccountName=my_username)(ObjectClass=user))',('cn','sAMAccountName','mail')) >>> user [('CN=My Name,OU=Super-Users,OU=USA,OU=California,OU=Sites,DC=activedirectory,DC=com', {'cn': ['My Name'], 'sAMAccountName': ['my_username'], 'mail': ['my_username@activedirectory.com']})] >> users = server.search_s('OU=Users,DC=activedirectory,DC=com',ldap.SCOPE_SUBTREE,'(&(memberOf=CN=@my_group,OU=Groups,OU=Users,DC=activedirectory,DC=com)(ObjectClass=user))',('cn','sAMAccountName','mail')) [('CN=My Name,OU=Super-Users,OU=USA,OU=California,OU=Sites,DC=activedirectory,DC=com', {'cn': ['My Name'], 'sAMAccountName': ['my_username'], 'mail': ['my_username@activedirectory.com']}), ('CN=Somebody_Else,OU=Super-Users,OU=USA,OU=California,OU=Sites,DC=activedirectory,DC=com', {'cn': ['Their name'], 'sAMAccountName': ['their_username'], 'mail': ['their_username@activedirectory.com']})]
Alternatives
- SSPI/NTLM If users will only use the Django application on a Windows PC which they already have been authorized, EG through windows logon, then using either
- Apache-2.4
- Apache-2.2
- Django Extensions and Snippets
- SAML and OAuth Sure you could do this. You can also use SSL with LDAP or Kerebos with SSPI/NTLM. But, alas, I did not research these options althought I did come across a few references.
mod_authnz_sspi
or mod_authnz_ntlm
to acquire those credentials from your Windows session is also an option.
There are several Django extensions and snippets that use Python-LDAP and override ModelBackend
so that Django handles authorization and authentication instead of Apache.
Some Django extensions and snippets also exist to subclass ModelBackends
to use PyWin32 to use local credentials from the current windows machine for authorization and authentication from within Django.
CSS and JS
The references section loosely based on Javascript TOC robot. It could also use the counters and the::before
style pseudo-element, but since I'm using JavaScript it doesn't make sense. But here's what that looked like anyway.
Example
Example
In case it wasn't clear above the JavaScript below is not what I'm using on this page. It was for a different approach using counters
which I scratched, so these examples are very contrived and don't really make sense anymore.