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! 🚀
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.
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 givenAttributesMapper
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 aContextMapper
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()
andgetStringAttributes()
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:
- Basic LDAP concepts
- Spring LDAP overview
- Spring Framework LDAP
- Build an OpenLdap Docker Image That’s Populated With Users
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! 🙏