14 nov 2007

NTLM Authentication in Django

Recently I had to integrate NTLM Intranet authentication into a Django application. The first problem was to get mod_ntlm [1] to work in Ubuntu Feisty [2]. After this was done I had to configure my Samba as a Primary Domain Controller (PDC) and add my vmplayer WinXP instance to that domain [3] [4].

After that was done, I wrote a Django authentication backend based on [5].

What I did:
  1. Add a link to my customized registration/login.html page which points to a special location which is protected by mod_ntlm, e.g.:
    <a href="./remote_user/?next={{next}}">Intranet authentication</a>

  2. Configure this location in Apache2 for mod_ntlm:


    #NTLM Auth
    AuthName NTAuth
    AuthType NTLM
    NTLMAuth on
    NTLMAuthoritative on
    NTLMDomain DOMAIN
    NTLMServer pdc.sercer
    NTLMBasicAuth off
    # NTLMBasicRealm SISAM
    NTLMLockfile /tmp/_my.lck
    # NTLMBackup
    Require valid-user
    # Satisfy all

    Here, I suppose that all django mod_python config is already included in /.

  3. Write the authentication backend and a view that captures the "REMOTE_USER" environment variable, authenticates and logs in the user. Here is my "remoteuser.py":

    """User auth based on REMOTE_USER.
    To make it work you need:
    - add RemoteUserAuthBackend en settings.py, en AUTHENTICATION_BACKENDS
    - add ('/login/remote_user/', 'sisamapp.auth.remoteuser.remote_user_login') to your urls.py
    - enable the apache module (e.g. mod_ntlm)
    - configure Apache /login/remote_user/, e.g. for mod_ntlm:

    #NTLM Auth
    AuthName NTAuth
    AuthType NTLM
    NTLMAuth on
    NTLMAuthoritative on
    NTLMDomain DOMAIN
    NTLMServer MACHINE or IP
    # NTLMBasicAuth off
    # NTLMBasicRealm SISAM
    NTLMLockfile /tmp/_my.lck
    # NTLMBackup
    Require valid-user
    # Satisfy all

    We suppose here that the / location has already all django stuff configured (i.e. PythonHandler)
    """
    from django.contrib.auth.models import User

    import sys
    log = sys.stderr.write

    # copied from http://code.djangoproject.com/attachment/ticket/689/remote_user_2.diff
    from django.contrib.auth.backends import ModelBackend
    class RemoteUserAuthBackend(ModelBackend):
    def authenticate(self, **credentials):
    """
    Authenticate user - RemoteUserAuth middleware passes REMOTE_USER
    as username. password param is not used, just added in case :)
    """
    try:
    type = credentials['type']
    if type == "remote_user":
    username = credentials['username']
    except:
    username = None
    if not username:
    return None
    user = None
    try:
    user = User.objects.get(username=username)
    except User.DoesNotExist:
    raise User.DoesNotExist, _T('User %s not configured in this application.') % username
    return user

    class NoRemoteUserInfoAvailable(Exception):
    pass


    from django.http import HttpResponseRedirect
    from django.shortcuts import render_to_response
    from django.template import RequestContext
    import re
    from django.utils.translation import ugettext as _T
    def render_notice(request, errornote, msg):
    return render_to_response('registration/notice.html',
    {'errornote': errornote, 'msg': msg },
    context_instance = RequestContext(request))

    def remote_user_login(request):
    error = """
    remote_user_login requires Django authentication middleware to be installed. (Include in MIDDLEWARE_CLASSES setting 'django.contrib.auth.middleware.AuthenticationMiddleware'.
    """
    msg = _T('Use the __standard login form__ to provide alternative credentials.')
    msg = re.sub('__(.*)__',r'<a href="../?next=%s">\1</a>' % request.GET.get('next',''), msg, re.UNICODE)
    try:
    username = request.META['REMOTE_USER']
    log("Got REMOTE_USER=%s\n" % username)
    except:
    return render_notice(request,
    errornote=_T('Server does not provide REMOTE_USER.'),
    msg=msg)
    if not username:
    return render_notice(request,
    errornote=_T('Could not get your credentials. Are you accessing from anywhere outside the domain or a browser that does not support intranet authentication?'),
    msg=msg)
    from django.contrib.auth import authenticate, login
    # AuthenticationMiddleware is required to create request.user
    assert hasattr(request, 'user'), self.error
    if request.user.is_anonymous():
    log("Request is anonymous. Trying to authenticate user %s\n" % username)
    try:
    user = authenticate(username=username, type="remote_user")
    except:
    user = None
    log("User is %s\n" % user)
    if user is not None:
    request.user = user # set request.user to the authenticated user
    login(request, user) # auto-login the user to Django
    return HttpResponseRedirect(request.GET.get('next','/'))
    return render_notice(request,
    errornote=_T('Your username (%s) is not registered here.') % username,
    msg=msg)
    # user is already authenticated, should logout first
    msg = _T('You have to logout first using __this link__ before logging in again.')
    msg = re.sub('__(.*)__',r'<a href="../../logout/">\1</a>', msg, re.UNICODE)
    return render_notice(request,
    errornote=_T('You are already authenticated.'),
    msg=msg)

    Some notes here:

    • remote_user 'authenticate' uses explicitly another signature as ModelBackend 'authenticate', i.e. it needs the 'type' argument. If you used the same signature (username, password) there is a possibility that a user authenticates without any password!
    • In my configuration, when I acces with Firefox/Linux /login/remote_user/, a browser authentication dialog pops up. I was not able to get rid of it.

    Links:
  1. http://modntlm.sourceforge.net/
  2. http://erny-rev.blogspot.com/2007/11/compiling-modntlm-for-apache2-in-ubuntu.html
  3. http://geeklab.wikidot.com/samba-pdc
  4. http://us1.samba.org/samba/docs/man/Samba-HOWTO-Collection/domain-member.html#machine-trust-accounts
  5. http://code.djangoproject.com/attachment/ticket/689/remote_user_2.diff

No hay comentarios: