I spend quite some time implementing a login using Active Directory via LDAP for our Spring Boot 2 application, using Spring Security. This article outlines the implementation options I faced. On my quest to solve the many problems I encountered with this, I learned that there is not much documentation available in the web. I hope this article is of some help for other developers.
I will use the term “LDAP” when refering to the Active Directory because that’s what most developers do when they mean “authorization using LDAP”.
Setup: Use an LDAP-Tool
The first thing to do when working with LDAP is probably to install an LDAP tool to explore the structure of the directory and find the correct attributes. I use the free Ldapadmin.
Setup: Add Certificate to JDK
To be able to connect securly to LDAP via TLS, a certificate has to be used. For local development, adding this certificate to the used JDK is sufficient. This is the command that has to be executed from the bin-folder in the JDK directory:
This approach is to cumbersome for running the application on test- or production stages. There, the certificate should be added to a key-store which is part of the source code. There are many formats for key-stores, for example p12 and jks. Here are both snippets; one of them has to be added to the application.yml:
The p12- or jks-file have to be in the src\main\resources folder right next to the application.yml (if you have that in your resources-folder; sometimes it’s in the root directory)
To create and change certificates in key-stores, KeyStore Explorer can be used.
SSL-Handshake-Exceptions and Connect-Exceptions
In trying to find the right configuration, I encountered many of those:
or those:
First, I thought that these exceptions appeared when I tried to use the wrong attribute for either logging in or searching for a user. Sometimes, this exception occurred when using the common name (“cn”), sometimes when using the SAM-Account name (“sAMAccountName”). Turns out that I tried to connect to a server that only forwards requests to different LDAP servers. Those servers obviously are configured differently, which sometimes caused perfectly fine working connections, sometimes handshake-exceptions and sometimes even time-outs. If you encounter such behavior, try to figure out if you connect to a specific LDAP server or if your request is being forwarded.
Low-Hanging Fruit: Complete Patterns!
I had a lot of unsuccessful test runs because I did not use fully qualified patterns. It’s important to not only use for example this:
This is the full string needed:
The following sections describe different approaches I used to connect to the LDAP server.
Approach 1: Getting the Password from LDAP and Comparing it Server-Side
The first examples I found in the web simply used the credentials provided by the user to establish a connection to LDAP. Then, the value of the attribute “userPassword” is requested. The encrypted password is send to the Spring server where it is compared with what the user entered in the login-form. If both are equal, the provided credentials are correct. Here’s how my code looked with this approach:
This is generally valid and can work. However, the LDAP server I wanted to use prohibits giving away (encrypted) passwords and produced this exception:
I could have seen this error coming because the attribute “userPassword” cannot be seen when I view the directory in Ldapadmin. What cannot be seen most likely cannot be read. That’s why I try to use approach 2.
Approach 2: bindauthenticator with Login-User’s Credentials
A “bind” is simply a login on an LDAP server. However, the BindAuthenticator sends the credentials to the LDAP server instead of requesting the stored (encrypted) password. The comparison of the stored password and the provided string is done by the LDAP server, not the Spring server. Here’s code:
This looked nice, however it also produced this:
Short story: I needed a separate, technical system-user. Long story: read approach 3. :)
Approach 3: bindauthenticator with Technical System-User
(Once again) from Stackoverflow (here, first answer, here and here, first answer) I learned that the best way to
authenticate with LDAP is to use a technical account for the first bind. After successful connecting to the server, a search with the credentials from the “real” user can be executed. Here’s the code:
This code finally did it, I can now connect to LDAP in a technically clean way, using Spring infrastructure.
There’s also a nice log statement that shows the correct combination of technical user and login-user:
Using samaccountname instead of cn
In the last code snippet above, the common name (CN) is used to identify the user in LDAP. This can be for example the first name and last name, in my case “Steven Schwenke”. This may not be the best attribute to use because names can be long and there is always the danger of duplicates. An alternative attribute to use is SamAccountName or sAMAccountName. Here’s some background information about that attribute that is often used as logon name and shorter than most names.
To use the SamAccountName in the setup of “Approach 3”, only the LdapAuthenticator has to be changed to use a FilterBasedLdapUserSearch. This is the implementation of LdapUserSearch which states in its JavaDoc “Obtains a user’s information from the LDAP directory given a login name. May be optionally used to configure the LDAP authentication implementation when a more sophisticated approach is required than just using a simple username->DN mapping.” Here’s the changed code (the rest of the code of “Approach 3” is the same):
Bonus-Approach: Plain Java
The formerly mentioned SO-article holds yet another solution using plain Java. This is the code, copy-pasted without change just to have all the information right here in this article. Credits for this one go to Atanu Sarkar:
Special Case: Connecting to Active Directory
When connecting to an Active Directory (AD), I found two additional aspects that could be useful.
First, the before-mentioned time-outs could have been exceptions caused by the AD. Following this SO-question, the following code would have been a solution (but was not, because the time-outs were caused by the fact that my requests have been forwarded - see above):
The other potentially interesting thing I found is the class ActiveDirectoryLdapAuthenticationProvider which can be used instead of LdapAuthenticationProvider. However, it’s not compatible to the configuration used in this article.
Spring Data LDAP
Yet another approach to accessing LDAP from a Spring application is Spring Data LDAP which uses the Spring repositories. I didn’t look into this, but it should be mentioned here.
Update
I’ve been told that the mentioned SSLHandshakeException and ConnectExceptions are indeed caused by misconfiguration. Also, here’s more information on why not to use the sAMAccountName. In my setting however, I was told to use it so I guess it’s fine for my case. ;) Thanks for the input!
TL;DR
There are a ton of aspects that can make connecting to an LDAP hell. See above for some hints that hopefully help you save some time.