How to Query the Active Directory Using Spring Boot LdapTemplate

Medium Link: How to Query the Active Directory Using Spring Boot LdapTemplate
If you have a medium.com membership, I would appreciate it if you read this article on medium.com instead to support me~ Thank You! 🚀
images/intro.png

Intro

Recently, I was playing around with the Active Directory (AD) and tried to retrieve user information via LDAP Queries. It was definitely a frustrating experience as I wasn’t very familiar with LDAP. Hence, I decided to write about what I have learned.

Note: at the point of writing, I am working with Spring Boot v2.7.7, Gradle v7.6, Docker-Compose v2.14.0, Docker v20.10.3, and Java v11.

Overview

Let’s start with a bit of background context. An Active Directory (AD) is a commonly used directory service by many companies for user & group management, policy administration, authentication, and etc… LDAP (Lightweight Directory Access Protocol) is a protocol that we can use to communicate with the LDAP servers (eg. the AD).

When developing services, we can authenticate users with LDAP or retrieve user information from the LDAP server. In this article, I will focus mainly on some of the methods I used to query the LDAP server using LdapTemplate. The LdapTemplate is a set of ready-to-use APIs for executing core LDAP operations such as creation, modification, retrieval, etc…

Setting up a LDAP Server

Before we start, we will need to have an LDAP server to query. In this demo, instead of setting up an entire Active Directory, we will set up an LDAP server using osixia/docker-openldap. This is an OpenLDAP docker image that allows us to easily populate the LDAP database using an LDIF file.

To get started, we will prepare a bootstrap LDIF file that contains all the data that we want to seed the LDAP database with. For demonstration purposes, we will only be populating the LDAP database with some user information as shown below.

# Sample Bootstrap LDIF file
#
# ------------------------------------------------------------------------------
#   Create Organizational Units
# ------------------------------------------------------------------------------

dn: ou=Users,dc=example,dc=org
changetype: add
objectclass: organizationalUnit
ou: Users

# ------------------------------------------------------------------------------
#   Create Users
# ------------------------------------------------------------------------------

dn: cn=Amanda,ou=Users,dc=example,dc=org
changetype: add
objectclass: inetOrgPerson
uid: U0
cn: Amanda
sn: Amanda
givenname: Amanda
displayname: Amanda
mail: amanda@example.org
mail: amanda@test.org
userpassword: amanda

dn: cn=Becca,ou=Users,dc=example,dc=org
changetype: add
objectclass: inetOrgPerson
uid: U1
cn: Becca
sn: Becca
givenname: Becca
displayname: Becca
mail: becca@example.org
mail: becca@test.org
userpassword: becca

dn: cn=Charmaine,ou=Users,dc=example,dc=org
changetype: add
objectclass: inetOrgPerson
uid: U2
cn: Charmaine
sn: Charmaine
givenname: Charmaine
displayname: Charmaine
mail: charmaine@example.org
mail: charmaine@test.org
userpassword: charmaine

There are 3 users (also known as an entry) populated in the LDAP database with 3 attributes that we will be retrieving in our demo:

  • uid : stores the user’s Id
  • displayname : stores the user’s name
  • mail : stores all the user’s email addresses

Note: An LDAP entry is a collection of information about an entity. Each entry consists of three primary components: a distinguished name, a collection of attributes, and a collection of object classes

To start the OpenLdap server, I have created the following docker-compose YAML file.

# Sample Docker Compose for OpenLdap Server
version: '3.9'

services:
  openldap:
    hostname: openldap
    domainname: "example.org"
    image: osixia/openldap:1.5.0
    container_name: openldap
    ports:
      - "389:389"
    command: [--copy-service, --loglevel, debug]
    environment:
      LDAP_LOG_LEVEL: "256"
      LDAP_ORGANISATION: "Example Org."
      LDAP_DOMAIN: "example.org"
      LDAP_BASE_DN: "dc=example,dc=org"
      LDAP_ADMIN_PASSWORD: "admin"
      LDAP_REMOVE_CONFIG_AFTER_SETUP: "false"
    volumes:
      - ./bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/50-bootstrap.ldif:z
      - ./storage/ldap_data:/var/lib/ldap
      - ./storage/ldap_config:/etc/ldap/slapd.d

Note that the domain is example.org and LDAP directory has the following tree structure after bootstrapping with the LDIF file defined above.

images/openldap.png
LDAP Directory Tree Structure

Querying with ldapsearch

With the OpenLdap server running, we can try to craft some LDAP queries using ldapsearch as shown below.

# Step into the OpenLdap Docker Container Instance
docker exec -it openldap /bin/bash

# Execute LDAP query
ldapsearch -x \
    -H ldap://localhost:389 \
    -b "dc=example,dc=org" \
    -D "cn=admin,dc=example,dc=org" \
    -w admin

The -b option indicates the search base for the query and in our case, we can use either dc=example,dc=org or ou=Users,dc=example,dc=org as referenced from our LDAP directory tree structure above.

The -D and -w option indicates which distinguished name to bind to which is required for authentication and by default, docker-openldap creates an administrator account with password admin.

Note: A distinguished name (DN) is the name that uniquely identifies and describes an entry in a LDAP server.

Now that we know how to query using ldapsearch, let’s try to do the same thing in Spring Boot with LdapTemplate.

Create Spring Boot Project

Spring Boot provides Spring Data LDAP which is a library for simple LDAP programming. It allows us to easily configure a Spring Boot project to connect and communicate with an LDAP server. We can do that by importing the dependency spring-boot-starter-data-ldap to our project.

# Gradle
dependencies {
 implementation("org.springframework.boot:spring-boot-starter-data-ldap")
}
# Maven
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>

Next, to connect to the OpenLdap server, we will provide the following connection settings in our application.yaml:

spring.ldap:
  urls: ldap://localhost:389
  base: dc=example,dc=org
  username: cn=admin,dc=example,dc=org
  password: admin

With that, we are now ready to use LdapTemplate to query the OpenLdap server.

Querying with LdapTemplate

Firstly, let’s inspect each user entry. Below is an example of a user entry.

dn: cn=Amanda,ou=Users,dc=example,dc=org
objectclass: inetOrgPerson
uid: U0
cn: Amanda
sn: Amanda
givenname: Amanda
displayname: Amanda
mail: amanda@example.org
mail: amanda@test.org
userpassword: amanda

We are interested to retrieve the attributes uid, displayname, and mail. As you may have noticed, there is more than 1 value for the attribute mail. So let’s see how we can retrieve all these attributes using LdapTemplate.

#1 Using AttributesMapper

When searching with LdapTemplate, we can use the AttributesMapper to map the attribute values. According to the documentations,

Internally, LdapTemplate iterates over all entries found, calling the given AttributesMapper for each entry, and collects the results in a list. The list is then returned by the search method.

Here’s an example on how we can retrieve the user’s attributes using AttributesMapper. Notice that we are searching from the directory ou=Users within the base directory dc=example,dc=org which we have defined above. A filter uid=$userId is also applied to find the specific user entry.

@Service
class LdapQueryService(
    private val ldapTemplate: LdapTemplate
) {
    fun queryWithAttributeMapper(userId: String): List<User> {
        return ldapTemplate.search("ou=Users", "uid=$userId", AttributesMapper { attributes ->
            val id = attributes.get("uid").get().toString()
            val name = attributes.get("displayname").get().toString()
            val emails = attributes.get("mail").all.toList() as List<String>
            User(userId = id, name = name, emails = emails)
        })
    }
}

Expected output:

[{
  "userId":"U0",
  "name":"Amanda",
  "emails":["amanda@example.org","amanda@test.org"]
}]

However, please be aware that there is a slight limitation where if you are trying to retrieve attributes that do not exist, you will get an NullPointerException. Moreover, you will not be able to retrieve the distinguished name (dn) or schemas using the AttributesMapper.

#2 Using ContextMapper

Similarly, we can use the AbstractContextMapper to map the attributes of an entry. According to the documentations,

Whenever an entry is found in the LDAP tree, its attributes and Distinguished Name (DN) will be used by Spring LDAP to construct a DirContextAdapter. This enables us to use a ContextMapper to transform the found values.

Here’s an example on how we can retrieve the user’s attributes using the AbstractContextMapper. The search operation is the same as the AttributesMapper except that the mapper is replaced with the AbstractContextmapper.

@Service
class LdapQueryService(
    private val ldapTemplate: LdapTemplate
) {
    fun queryWithContextMapper(userId: String): List<User> {
        return ldapTemplate.search("ou=Users", "uid=$userId", object: AbstractContextMapper<User>() {
            override fun doMapFromContext(ctx: DirContextOperations): AdUser {
                val dn = ctx.dn
                val id = ctx.getStringAttribute("uid")
                val name = ctx.getStringAttribute("displayname")
                val emails = ctx.getStringAttributes("mail").toList()
                return User(id, name, emails)
            }
        })
    }
}

Expected output:

[{
  "userId":"U0",
  "name":"Amanda",
  "emails":["amanda@example.org","amanda@test.org"]
}]

The advantage of the ContextMapper is that

  • it handles NullPointerException by returning null instead of throwing exceptions.
  • it simplifies attributes retrieval operations (especially for multi-value attributes) with getStringAttribute() and getStringAttributes() methods.
  • it allows you to retrieve distinguished name (dn), schemas and object classes.

#3 Extra Tips - DefaultIncrementAttributesMapper

If you know the distinguished name (dn), you can use the DefaultIncrementAttributesMapper to look up for multi-valued attributes (eg. the mail attribute). Here’s an example of how you can use it.

@Service
class LdapQueryService(
    private val ldapTemplate: LdapTemplate
) {
    private fun getMultiValuedAttributesWithDefaultIncrementAttributesMapper(dn: String, attr: String): List<String> {
        val attributes = DefaultIncrementalAttributesMapper.lookupAttributes(ldapTemplate, dn, attr)
        val results = ArrayList<String>()
        for (i in 0 until attributes.get(attr).size()) {
            results.add(attributes.get(attr)[i].toString())
        }
        return results
    }
    
    fun query(): List<String> {
        val dn = "cn=Amanda,ou=Users"
        val attr = "mail"
        val emails = getMultiValuedAttributesWithDefaultIncrementAttributesMapper(dn, attr)
        return emails
    }
}

Expected Output

["amanda@example.org","amanda@test.org"]

Note that the user’s dn is cn=Amanda,ou=Users,dc=example,dc=org. As we have already defined the base directory to be dc=example,dc=org, there is no need to include dc=example,dc=org in the dn when searching. It will result in error code 32 if it is included.

javax.naming.NameNotFoundException: [LDAP: error code 32 - No Such Object]; remaining name 'cn=Amanda,ou=Users,dc=example,dc=org'

Summary

That’s it! If you are familiar with LDAP and Spring Boot, most of this information can be easily found in the respective documentation. This article is mainly to give a general idea of how you can perform a search for attributes using LdapTemplate. I hope you enjoy this article as much as I enjoyed writing it :)

For more information, check out these useful links:

Refer to the Git Repo below for the code reference


Thank you for reading till the end! ☕
If you enjoyed this article and would like to support my work, feel free to buy me a coffee on Ko-fi. Your support helps me keep creating, and I truly appreciate it! 🙏