Category Archives: LDAP

LDAP under WSL2

The Python Flask application I have been dabbling with for the past year or so uses LDAP to authenticate add and update actions. Moving my development host to Ubuntu on Windows Services for Linux on Windows 10 Home edition means I need a local LDAP server.

Slapd on Ubuntu

Now, since I have a Ubuntu ‘VM’ available that is running native and supports installation of standard Ubuntu packages, installing the slapd package would be a natural starting point. But

Setting up slapd (2.4.45+dfsg-1ubuntu1.4) ...
  Backing up /etc/ldap/slapd.d in /var/backups/slapd-... done.
  Moving old database directory to /var/backups:
  - directory unknown... done.
  Creating initial configuration... done.
  Creating LDAP directory... failed.
Loading the initial configuration from the ldif file () failed with
the following error while running slapadd:
    5d72add0 => mdb_next_id: get failed: MDB_BAD_TXN: Transaction must abort, has a child, or is invalid (-30782)
    5d72add0 => mdb_tool_entry_put: cursor_open failed: MDB_BAD_TXN: Transaction must abort, has a child, or is invalid (-30782)
    slapadd: could not add entry dn="dc=localdomain" (line=1): cursor_open failed: MDB_BAD_TXN: Transaction must abort, has a child, or is invalid (-30782)
    5d72add0 mdb_tool_entry_close: database dc=localdomain: txn_commit failed: MDB_BAD_TXN: Transaction must abort, has a child, or is invalid (-30782)
dpkg: error processing package slapd (--configure):
 installed slapd package post-installation script subprocess returned error exit status 1

This kind of service isn’t supported on WSL2.

Slapd on Docker

Slapd is a service well suited to running under a Docker container. But, Docker on Windows desktop is only supported for Windows 10 Pro. I’m on ome.

Slapd for Windows

A quick search for OpenLDAP on Windows turns up an installer from https://sourceforge.net/projects/openldapwindows/files/openldap-2.4.32/openldap-2.4.32-x86.zip/download. Note that there is a later 2.4.44 version available but this requires a registration key that is no longer available.

Anyhow, Ubuntu’s ldappasswd can be used to create a new rootpw to be added to OpenLDAP\etc\openldap\slapd.conf

$ slappasswd -h {SSHA}

The slapd service can then be started via the Start menu and an ldif in OpenLDAP\etc\ldif\base.ldif edited to include the necessary directory ous, users and groups needed for the Flask service, using the OpenLDAP CLI from the Start menu,

$ ldapadd.exe -v -x -D "cn=Manager,dc=my-domain,dc=com" -f ..\etc\ldif\base.ldif -W

With that the Flask application can make a connection to the LDAP server and authenticate users before allowing updates.

 

Advertisements

Configure Python Flask application for LDAP authentication

Using a crossword-hints Flask application to include authentication for adding, modifying and deleting database content.
We will use ad a users table to the database and hand off the authentication to the directory.

Install the python-ldp pip (3.1.0 as of February 2019)

$ pip install python-ldap --user

Install the Flask-login pip (0.4.1 s of February 2019)

$ pip install flask-login --user

Include the modules in the Flask application

from flask_login import LoginManager, UserMixin, login_required, login_user, logout_user
import ldap

Configure Flask-login in the application

# flask-login
login_manager = LoginManager()
login_manager.init_app(application)
login_manager.login_view = "crossword_login"

The login_view indicated the function to be called to handle the user login.

Flask-login also requires a user_loader function that returns an integer id value for the user,

@login_manager.user_loader
def load_user(id):
    return users.get(users.rowid == int(id))

Add a function to get a connection to the directory (but no authentication at this point)

def get_ldap_connection():
    conn = ldap.initialize('ldap://localhost:389')
    return conn

Add a route to the login form. This accepts GET and POST requests to display the form and then
process the submitted form. Notice that after successful login there is a redirect to a page passed as the next parameter in the query string when an application route requires login: this could be a securiity concern if steps are not taken to prevent the application accepting a spoofed resource as input rather than a permitted resource in the intended application.

@application.route("/login", methods=['GET', 'POST'])
def crossword_login():
    try:
        if current_user.is_authenticated:
        flash('You are already logged in.')
        return redirect(request.path)
    except:
        pass

    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')

        try:
            users.try_login(username, password)
        except ldap.INVALID_CREDENTIALS:
            flash(
                'Invalid username or password. Please try again.', 'danger')
            return(render_template('views/login/login.html', u=username, r=request))

        user = users.get(users.username == username)
        login_user(user)
        return redirect(request.args.get("next"))
    else:
        username = 'username'

    return(render_template('views/login/login.html', u=username, r=request))

A template for the login form could be

{%- import "macros.html" as f -%}
Use this form to login to the site to maintain restricted content.
</p>
<form name="crossword_login" action="{{ r.full_path }}" method="POST">
<fieldset id="crossword_login">
<legend>Login</legend>
{{ f.label('username', 'Username') }}
{{ f.input_tag('username', value=(u|escape()|default('')), class="xwordhints") }}

{{ f.label('password', 'Password') }}
{{ f.input_tag('password', type="password", class="xwordhints") }}
</fieldset>

<p>
<input type="submit" name="login" value="Login" />
<input type="reset" value="Reset" />
<br /><br />
<a href="/crossword-hints">Back to index</a>
</p>
</form>

macros.html (there’s a good GitHub gist with some macros but I can’t find a link to it) contains helpers to create the form input elements. The form action
uses the request’s full path which includes the query string containing the next parameter.

Provide a logout route. Note that this requires a valid login. Going direct to /logout will show a prompt to login!

@application.route("/logout")
@login_required
def logout():
    logout_user()
    flash("Logout successful. Please close browser for best security.")
    return(redirect("/"))

A PeeWee user model

class users(BaseModel):
    rowid = AutoField()
    username = CharField(null=False, max_length=32, unique=True)
    created_at = DateTimeField(default=datetime.now())
    updated_at = DateTimeField(default=datetime.now())

    @staticmethod
    def try_login(username, password):
        conn = get_ldap_connection()
        conn.simple_bind_s(
            'uid=%s,ou=People,dc=my-domain,dc=com' % username, password
        )

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        conn = get_ldap_connection()
        return(self.rowid)

    def get_name(self):
       return(self.username)

The get_name method is used by HTML templates to display the logged in username. For authentication the try_login method just attempts to bind to the directory as the user.

{% if current_user.get_id() is not none %}Logged in {{ current_user.get_name() }}&nbsp; <a href="/logout">Logout</a>{% endif %}

Routes that need authentication (and authoristion) before access simply need to state ‘login_required):

@application.route("/crossword-setters/<int:id>/edit", methods=["GET", "POST"])
@login_required
def crossword_setters_edit(id):

Accessing this route will redirect to the login form and successful login will redirect back to the edit page.

Unit tests

When requiring authentication to enable certain operations in the application, consideration needs to be given to the impact on running unit tests.

The protecting views section of the Flask-login site indicates that this is simply a matter of setting the following option:

LOGIN_DISABLED=True

which disables the login_required decoration applied to routes. Adding this to a settings file used when running the tests allows everything to complete as expected.

APP_SETTINGS='test-settings.py' python crossword-hints-tests.py

 

Improvements

  • Don’t use the SQL database to store the users
  • Control authorisation according to group membership.
  • Prevent authentication leakage via spoofed ‘next’ resources.
  • Don’t request a login when going direct to the logout route.

References

ArchLinux OpenLDAP setup for webapp authentication

Setting up a simple OpenLDAP directory to allow authentication from a web (or other) application.

Based on https://wiki.archlinux.org/index.php/OpenLDAP

Configure LDAP service

Install openldap packages

Create a salted hashed password for the directory manager:

$ slappasswd -h {SSHA}

Enter and confirm a password and receive output such as

{SSHA}c920AmsQ9Evay0YaCU/r0GAdnMroyL4O

Configure /etc/openldap/slapd.conf to use the following:

database mdb
suffix "dc=my-domain,dc=com"
rootdn "cn=Manager,dc=my-domain,dc=com"
...
rootpw {SSHA}c920AmsQ9Evay0YaCU/r0GAdnMroyL4O

As the superuser, setup the directory database and start the service,

# cp /var/lib/openldap/openldap-data/DB_CONFIG.example /var/lib/openldap/openldap-data/DB_CONFIG
# slaptest -f /etc/openldap/slapd.conf -F /etc/openldap/slapd.d
# slapindex
# chown -R ldap:ldap /etc/openldap/slapd.d /var/lib/openldap/openldap-data
# systemctl start slapd

Test connection,

$ ldapsearch -D "cn=Manager,dc=my-domain,dc=com" -W -x '(objectclass=*)'
Enter LDAP Password:

Create base directory, users and groups

Create an LDIF file, say, base.ldif, for the directory manager and base groups (whether or not the groups are used by the application is another matter):

dn: dc=my-domain,dc=com
objectClass: dcObject
objectClass: organization
dc: my-domain
o: My-domain
description: My-domain directory

dn: cn=Manager,dc=my-domain,dc=com
objectClass: organizationalRole
cn: Manager
description: Directory Manager

# People, my-domain.com
dn: ou=People,dc=my-domain,dc=com
ou: People
objectClass: top
objectClass: organizationalUnit

# Groups, my-domain.com
dn: ou=Groups,dc=my-domain,dc=com
ou: Groups
objectClass: top
objectClass: organizationalUnit

# xword-admins group
dn: cn=xword-admins,ou=Groups,dc=my-domain,dc=com
objectClass: top
objectClass: posixGroup
gidNumber: 10000

# xword-users group
dn: cn=xword-users,ou=Groups,dc=my-domain,dc=com
objectClass: top
objectClass: posixGroup
gidNumber: 10001
$ ldapadd -D "cn=Manager,dc=my-domain,dc=com" -W -x -c -f /path/to/base.ldif

(-c allows for repeated runs of the same LDIF as typos and errors are wrinkled out)
Enter the password when prompted and check that the OUs and groups are created with:

$ ldapsearch -D "cn=Manager,dc=my-domain,dc=com" -W -x '(objectclass=*)'

Add groups and users

Generate a hashed password for each user, to be used in the subsequent LDIF file,

$ slappasswd -h {SSHA}

Create another ldap, say, users.ldif. contining something like,

dn: uid=xword,ou=People,dc=my-domain,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: xword
cn: Crossword Hints
sn: Hints
givenName: Crossword
userPassword: {SSHA}V7i9IYaW4Z4Z2mVjbtOk0PcinzgJof9y
labeledURI: http://xword-hints.my-domain.com/
loginShell: /bin/bash
uidNumber: 9999
gidNumber: 9999
homeDirectory: /home/xword
description: Crossword hints website user

# Add xword to the LDAP xword-admins group
# group
dn: cn=xword-admins,ou=Groups,dc=my-domain,dc=com
changetype: modify
add: memberuid
memberuid: xword

# Add xword to the LDAP xword-admins group
dn: cn=xword-users,ou=Groups,dc=my-domain,dc=com
changetype: modify
add: memberuid
memberuid: xword

Then add the users to the directory

$ ldapadd -D "cn=Manager,dc=my-domain,dc=com" -W -x -c -f /path/to/users.ldif

Check the users can authenticate through the directory,

$ ldapsearch -D "uid=xword,ou=People,dc=my-domain,dc=com" -W -x '(objectclass=*)'

This will probably show the entire directory contents; this can be tightened up as needed.

The directory is now ready for user authentication.

Troubleshooting

Reset the password described at https://www.digitalocean.com/community/tutorials/how-to-change-account-passwords-on-an-openldap-server although the Arch LDAP server does not have the EXTERNAL mechanism enabled by default; it’s probably easier to rebuild the directory.

Fedora 24 LDAP setup for Rails applications

To support the devise authentication in my application, I need to configure a local LDAP directory. The setup details of the Fedora aren’t very good, but I cam across https://www.server-world.info/en/note?os=Fedora_23&p=openldap which worked a treat on my Fedora 24 install.

Used the following files for the build.

# Install using: ldapmodify -Y EXTERNAL -H ldapi:/// -f mydomain.ldif
#
# use slappasswd to generate SSHA passwords
#
dn: olcDatabase={1}monitor,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to * by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth"
 read by dn.base="cn=Manager,dc=my-domain,dc=com" read by * none

dn: olcDatabase={2}mdb,cn=config
changetype: modify
replace: olcSuffix
olcSuffix: dc=my-domain,dc=com

dn: olcDatabase={2}mdb,cn=config
changetype: modify
replace: olcRootDN
olcRootDN: cn=Manager,dc=my-domain,dc=com

dn: olcDatabase={2}mdb,cn=config
changetype: modify
replace: olcRootPW
olcRootPW: {SSHA}lFyoikpFOrg....kIZ4lo85qK

dn: olcDatabase={2}mdb,cn=config
changetype: modify
add: olcAccess
olcAccess: {0}to attrs=userPassword,shadowLastChange by
 dn="cn=Manager,dc=my-domain,dc=com" write by anonymous auth by self write by * none
olcAccess: {1}to dn.base="dc=my-domain,dc=com" by * read
olcAccess: {2}to * by dn="cn=Manager,dc=my-domain,dc=com" write by * read

And,

# Install using: ldapadd -x -D cn=Manager,dc=my-domain,dc=com -W -f domain.ldif
dn: dc=my-domain,dc=com
objectClass: top
objectClass: dcObject
objectclass: organization
o: Server World
dc: my-domain

dn: cn=Manager,dc=my-domain,dc=com
objectClass: organizationalRole
cn: Manager
description: Directory Manager

dn: ou=People,dc=my-domain,dc=com
objectClass: organizationalUnit
ou: People

dn: ou=Group,dc=my-domain,dc=com
objectClass: organizationalUnit
ou: Group

An LDAP browser can then bind to the service using ‘cn=Manager’ and create users and other objects.

A Rails application can use the directory for authentication with the following in config/ldap.yml (assuming use of the devise_ldap_authenticatable gem),

development:
  # <<: *AUTHORIZATIONS
  host: 127.0.0.1
  port: 389
  attribute: cn
  base: dc=my-domain,dc=com
  admin_user: cn=Manager,dc=my-domain,dc=com
  admin_password: the_password
  ssl: false

Not really expecting this to be useful to anyone else, but it should be useful the next time I have to rebuld the laptop environment.