Using the repoze.who LDAP plugins in your WSGI Application

Implementing authentication via repoze.who is a simple task that requires few lines of code. So using its LDAP plugins should not be an exception: You just have to configure repoze.who in your application and then add the repoze.who.plugins.ldap plugin(s) you want to use in your application.

Setting up repoze.who with the LDAP authenticator

This section explains how to setup repoze.who in order to use the LDAP plugins in your WSGI application. It is based on the documentation for repoze.who.

You can configure your authentication mechanism powered by repoze.who with two methods: With an INI file or with Python code.

In the examples below we are only going to use the main plugin provided by this package: The LDAP authenticator itself (LDAPAuthenticatorPlugin). The other plugins don’t deal with authentication, but are useful to load automatically data related to the authenticated user from the LDAP server.

Using the repoze.who terminology, LDAPAuthenticatorPlugin is an authenticator plugin and the others are metadata provider plugins.

Configuring repoze.who in a INI file

You can configure your repoze.who based authentication via a *.ini file, and then load such settings in your application.

Say we have a file called who.ini with the following contents:

# These contents have been adapted from:
# http://static.repoze.org/whodocs/#middleware-configuration-via-config-file
[plugin:form]
use = repoze.who.plugins.form:make_plugin
rememberer_name = auth_tkt

[plugin:auth_tkt]
use = repoze.who.plugins.auth_tkt:make_plugin
secret = something

[plugin:ldap_auth]
use = repoze.who.plugins.ldap:LDAPAuthenticatorPlugin
ldap_connection = ldap://ldap.yourcompany.com
base_dn = ou=developers,dc=yourcompany,dc=com

[general]
request_classifier = repoze.who.classifiers:default_request_classifier
challenge_decider = repoze.who.classifiers:default_challenge_decider

[identifiers]
plugins =
    form;browser
    auth_tkt

[authenticators]
plugins =
        ldap_auth

[challengers]
plugins =
    form;browser

With the settings above, authentication via repoze.who is configured this way: Visitors will login with a form, providing their user name and password; then these credentials will be checked against the LDAP server ldap.yourcompany.com under ou=developers,dc=yourcompany,dc=com. This form will be displayed when your WSGI application issues an HTTP 401 error.

For example, if an user enters jsmith as the user name and valencia as their password, the LDAP authenticator will build their Distinguished Name (DN) as uid=jsmith,ou=developers,dc=yourcompany,dc=com and will try to authenticate them in the ldap.yourcompany.com LDAP server with this DN and valencia as the password.

You may modify the way the DN is generated by subclassing LDAPAuthenticatorPlugin to override the _get_dn method.

Finally, you can load these settings by adding the repoze.who middleware to your application:

from repoze.who.config import make_middleware_with_config
app_with_auth = make_middleware_with_config(app, '/path/to/who.ini')

In the documentation for repoze.who there is a more detailed explanation for the INI file method.

Configuring repoze.who with Python code

The Python code below does the same as the INI file above:

# This script has been adapted from
# http://static.repoze.org/whodocs/#module-repoze.who.middleware

# Importing the plugins to be used
from repoze.who.interfaces import IIdentifier, IChallenger
from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin
from repoze.who.plugins.form import FormPlugin
from repoze.who.plugins.ldap import LDAPAuthenticatorPlugin

# Configuring the plugins
ldap_auth = LDAPAuthenticatorPlugin('ldap://ldap.yourcompany.com',
                                    'ou=developers,dc=yourcompany,dc=com')
auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt')
form = FormPlugin('__do_login', rememberer_name='auth_tkt')
form.classifications = { IIdentifier: ['browser'],
                         IChallenger: ['browser'] } # only for browser
identifiers = [('form', form),('auth_tkt',auth_tkt)]
authenticators = [('ldap_auth', ldap_auth)]
challengers = [('form',form)]
mdproviders = []

# Using the default repoze.who classifiers:
from repoze.who.classifiers import default_request_classifier, \
                                   default_challenge_decider
log_stream = None
import os
if os.environ.get('WHO_LOG'):
    log_stream = sys.stdout

Then you can get these settings applied by adding the repoze.who middleware to your application:

app_with_auth = PluggableAuthenticationMiddleware(
    app,
    identifiers,
    authenticators,
    challengers,
    mdproviders,
    default_request_classifier,
    default_challenge_decider,
    log_stream = log_stream,
    log_level = logging.DEBUG
    )

In the documentation for repoze.who there is a detailed explanation for this method.

Framework-specific documentation

You may want to check the following framework-specific documents to learn tips on how to implement repoze.who in the framework you are using:

Using the LDAP plugins for repoze.who

Once you’ve setup repoze.who, you’ll be ready to use its LDAP plugins. Below you will find how to use them in your application. The repoze.who.plugins.ldap module export two different concrete authentication plugins, both derived from LDAPBaseAuthenticatorPlugin.

class repoze.who.plugins.ldap.LDAPBaseAuthenticatorPlugin(ldap_connection, base_dn, naming_attribute='uid', returned_id='dn', start_tls=False, bind_dn='', bind_pass='')

Any class derived from this one is in charge of the LDAP authentication itself, using the LDAP connection object provided in the constructor (ldap_connection) – which can be an LDAP URL or a ldap.ldapobject.SimpleLDAPObject instance.

By default, the returned user name will be the full DN; if your downstream WSGI application needs to use the bare login, you must set returned_id to 'login'.

If the parameter start_tls is set to a True value, any communication with the directory server will be encrypted.

If the parameters bind_dn and bind_pass are set to non-empty strings, before doing any operation, the plugin will try to bind with the server using the supplied credentials.

This plugin and its subclasses are compatible with any identifier plugin that defines the login and password items in the identity dictionary (the identifier plugins provided by the built-in repoze.who.plugins.form plugin are some of them).

It is a highly customizable plugin which can be adapted to your needs with no hassle. You could also include in the login form a select field for people to select the department they belong to, being the key of such departments the Organizational Unit in the LDAP server; then, in the _get_dn method you would get such value from the WSGI environment object (environ).

Any derived class must set the way the DN is created by overriding the abstract _get_dn method. For example, say in your company (with dc=yourcompany,dc.com as its DN) everybody belongs to the Organizational Unit (OU) employees (ou=employees), except the shareholders who belong to the OU shareholders (ou=shareholders):

class YourCompanyLDAPAuthenticatorPlugin(LDAPBaseAuthenticatorPlugin):
    """Sample LDAP authenticator adapted to your company."""

    shareholders = ('lgarcia, 'mferreira', 'cnarea')
    """Set of shareholders of the company"""

    def _get_dn(self, environ, identity):
        try:
            if identity['login'] in self.shareholders:
                ou = 'shareholders'
            else:
                ou = 'employees'
            return u'uid=%s,ou=%s,%s' % (identity['login'], ou,
                                         self.base_dn)
        except (KeyError, TypeError):
            raise ValueError('Could not find the DN from the identity '
                             'and environment')

It is possibly an useless example on how to customize the way the DN is found, but it’s enough to show how to override it.

If you’re using a custom LDAP authenticator, as in the example above, you would have to change the use directive accordingly – for example:

[plugin:ldap_auth]
use = yourpackage.lib.auth:YourCompanyLDAPAuthenticatorPlugin
ldap_connection = ldap://yourcompany.com
base_dn = ou=employees,dc=yourcompany,dc=com
class repoze.who.plugins.ldap.LDAPAuthenticatorPlugin(ldap_connection, base_dn, naming_attribute='uid', returned_id='dn')

This plugin connects to the specified LDAP server and tries to bind with the Distinguished Name (DN) made by joining the login in the identity dictionary as the naming attribute value and the base_dn specified in the constructor, and then it tries to bind with the password found in the identity dictionary; As a default, the used naming attribute is the user id (uid).

For example, if the login provided by the identifier is carla and the base_dn provided in the constructor is ou=employees,dc=example,dc=org, the resulting DN will be uid=carla,ou=employees,dc=example,dc=org.

If the directory server’s naming attribute were the email attribute, and we provided naming_attribute=’email’ in the constructor, the DN resulting for the identifier carla@example.org would be email=carla@example.org,ou=employees,dc=example,dc=org.

To configure this plugin from an INI file, you’d have to include a section like this:

[plugin:ldap_auth]
use = repoze.who.plugins.ldap:LDAPAuthenticatorPlugin
ldap_connection = ldap://yourcompany.com
base_dn = ou=employees,dc=yourcompany,dc=com
naming_attribute = uid
start_tls = True
class repoze.who.plugins.ldap.LDAPSearchAuthenticatorPlugin(ldap_connection, base_dn, naming_attribute='uid', search_scope='subtree', filterstr='', returned_id='dn')

This plugin connects to the specified LDAP server and searches an entry residing below the base_dn, whose naming attribute’s value is equal to the supplied login. If such an entry is found, it tries to bind as the entry’s DN with the password found in the identity dictionary; As a default, the used naming attribute is the user id (uid).

The search_scope parameter in the constructor allows to choose whether to search the entry in the whole subtree below base_dn, or just on the level below if set as search_scope=’onelevel’.

For example, if the login provided by the identifier is carla and the base_dn provided in the constructor is dc=example,dc=org, with the default settings, the system could find the entry uid=carla,ou=employees,dc=example,dc=org; if we set search_scope=’onelevel’, the entry would not be found.

If you would like to only allow some entries, you may setup a filter by means of the filterstr parameter, which is an string whose format is defined by RFC 4515 - Lightweight Directory Access Protocol (LDAP): String Representation of Search Filters. E.g. we can assert only person entries bearing a telephone number starting with 999111 can login by setting: filterstr=’(&(objectClass=person)(telephoneNumber=999111*))’ in the constructor.

To configure this plugin from an INI file, you’d have to include a section like this:

[plugin:ldap_auth]
use = repoze.who.plugins.ldap:LDAPSearchAuthenticatorPlugin
ldap_connection = ldap://yourcompany.com
base_dn = ou=employees,dc=yourcompany,dc=com
naming_attribute = uid
search_scope = subtree
start_tls = True

Finally, add the plugin to the set of authenticators:

[authenticators]
plugins =
        ldap_auth

But if you’re configuring repoze.who via Python code, you can use the code below:

ldap_auth = LDAPAuthenticatorPlugin('ldap://ldap.yourcompany.com',
                                    'ou=developers,dc=yourcompany,dc=com')

or, respectively:

ldap_auth = LDAPSearchAuthenticatorPlugin('ldap://ldap.yourcompany.com',
                                    'ou=developers,dc=yourcompany,dc=com')

But if you’re using a custom LDAP authenticator, you would have to use the code below instead:

ldap_auth = YourCompanyLDAPAuthenticatorPlugin('ldap://ldap.yourcompany.com',
                                               'ou=employees,dc=yourcompany,dc=com')

Finally, add this authenticator to the set of authenticators:

authenticators = [('ldap_auth', ldap_auth)]

As in the example above.

class repoze.who.plugins.ldap.LDAPAttributesPlugin(ldap_connection[, attributes=None[, filterstr='(objectClass=*)']])

This plugin enables you to load data for the authenticated user automatically and have it available from the WSGI environment — in the identity dictionary, specifically.

ldap_connection represents the connection to the LDAP server, which, as in LDAPAuthenticatorPlugin, can be either an LDAP URL or an instance of ldap.ldapobject.SimpleLDAPObject. attributes represents the list of user’s attributes that you would like to fetch from the LDAP server; it can be an iterable, an string where the attributes are separated by commas, or None to fetch all the available attributes.

By default it loads the attributes available for any entry whose DN is the same as the one found by LDAPAuthenticatorPlugin, which is desired in most situations. However, if you would like to exclude some entries, you may setup a filter by means of the filterstr parameter, which shares the same semantics as the filterstr parameter in LDAPSearchAuthenticatorPlugin.

There is no advanced usage for this plugin, and hopefully you would never need to subclass it to suit your needs.

To configure this plugin from an INI file, you’d have to include a section like this:

[plugin:ldap_attributes]
use = repoze.who.plugins.ldap:LDAPAttributesPlugin
ldap_connection = ldap://ldap.yourcompany.com
attributes = cn,sn,mail

If instead of loading the Common Name, surname and email, as with the settings above, you’d prefer to load all the available attributes for the authenticated user, you’d just have to remove the attributes directive.

Finally, add the plugin to the set of metadata providers:

[mdproviders]
plugins =
        ldap_attributes

But if you want to configure it via Python code, you can use the code below:

ldap_attributes = LDAPAttributesPlugin('ldap://ldap.yourcompany.com',
                                       ['cn', 'sn', 'email'])

Again, if you would prefer to load all the available attributes for the user, you just have to remove the second parameter.

Finally, add this authenticator to the set of metadata providers in your Python code:

mdproviders = [('ldap_attributes', ldap_attributes)]