LDAP Password Policy via Spring and OpenLDAP
Advanced password policy is one of the most coveted features an LDAP server has to offer. Yet, dealing with it programmatically is not as straightforward as one could imagine. The purpose of this post is to provide a simple guideline of how to manage a custom password policy on the OpenLDAP server via Spring.
Originally, I was thinking of using an embedded ApacheDS as it integrates well with Spring LDAP. Surprisingly, the password policy does not seem to be supported prior to the version 2.0 which, as of now, remains unsupported by Spring. I tried setting up the embedded 2.0 server on my own but did not succeed. If you want to give it a shot nevertheless, this article is a good starting point.
Now, to begin download the OpenLDAP server and install it on your machine. The installation wizard is easy to follow and there are no unpleasant surprises involved. Once the software will have been installed, you are ready to proceed with a minimal configuration by amending the slapd.conf. You will find it in the root of the installation directory. What follows is a highlight of important changes in the default configuration.
Provide sensible access rights. If security is a concern you want to make sure nothing can be modified anonymously and the registered users can manage their own accounts only.
access to attrs=userpassword by users manage by * auth access to * by self write by users manage by anonymous read by * auth
Create a dedicated account the application will use when connecting to the server:
# root or superuser rootdn ″cn=admin,dc=example,dc=com″ rootpw {MD5}CY9rzUYh03PK3k6DJie09g==
Finally, specify that a password policy should apply and provide a default one:
overlay ppolicy ppolicy_default ″cn=default,ou=policies,dc=example,dc=com″ ppolicy_hash_cleartext yes ppolicy_use_lockout yes
Next, create entries which will be loaded once the server starts up. Here are the files and their content applicable to this particular example:
people.ldif
dn: ou=people, dc=example,dc=com ou: people description: everyone in organisation objectclass: organizationalUnit
policies.ldif
dn: ou=policies,dc=example,dc=com objectClass: organizationalUnit objectClass: top ou: policies # The default policy dn: cn=default, ou=people, dc=example, dc=com objectClass: pwdPolicy objectClass: person objectClass: top pwdMaxFailure: 5 pwdMinLength: 5 pwdAttribute: userPassword # The 'tough' one (complex passwords, etc. - well, if only..) dn: cn=tough, ou=people, dc=example, dc=com .. pwdMaxFailure: 3 pwdMinLength: 7 .. # The 'relaxed' policy intended for those struggling to remember their passwords:-) dn: cn=relaxed, ou=people, dc=example, dc=com .. pwdMaxFailure: 5 pwdMinLength: 4 ..
To add the entries, you can use the slapadd utility which comes shipped with the server. For example:
slapadd -l my_ldif_file -o ./my_db_dir
At this stage, the server is ready to be started from the command line:
slapd -d 1
If you happen to use MS Windows make sure the server is not running as a service before you proceed with the command above.
Since the server is up and running it is in on time to take a look at the application. I will highlight the most important points only. Firstly, let’s connect to the server using the Spring LDAP configuration:
<security:ldap-server id="ldapServer" url="ldap://127.0.0.1:389/dc=example,dc=com" manager-dn="cn=admin,dc=example,dc=com" manager-password="test" />
The key feature is a standard user management module, here is how the contract looks like:
public interface UserManager { boolean createUser(String username, String password); boolean login(String username, String password); void setPolicy(String username, String policy); String getPolicy(String username); .. }
Most of the methods return boolean which makes it easy to test for successful completion. The implementation obviously makes use of the LDAP server and there is nothing much to say about it. The interesting part though is how the password policy is set.
Typically, the default password policy is rather complex and might be perceived as too strict for a certain group of users with a limited access to the application. In such a case, the default policy can be overridden on the user object’s level by setting a special attribute called pwdPolicySubentry.
Setting an attribute is a piece of cake when using the Spring’s LdapTemplate utility class. In this particular case however, the template would not work. The reason being is that the attribute is server-specific and thus cannot be easily set.
The following does not work. No custom policy is set on the user’s object:
import org.springframework.ldap.core.LdapTemplate; import javax.annotation.Resource; .. @Resource private LdapTemplate template; .. DirContextOperations ctx = template.lookupContext(″uid=lucky.guy,ou=people,dc=example,dc=com″); ctx.setAttributeValue(″pwdPolicySubentry″, ″cn=relaxed,ou=people,dc=example,dc=com″); ldapOperations.modifyAttributes(ctx); ..
Fortunately, Spring provides a direct access to the directory service via the ContextExecutor.
The policy has to be set directly through a reference to the directory service:
import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.ContextExecutor; import javax.naming.directory.DirContext; import javax.naming.directory.Attribute; import javax.naming.directory.BasicAttribute; import javax.naming.directory.Attributes; import javax.annotation.Resource; .. @Resource private LdapTemplate template; .. ContextExecutor executor = new ContextExecutor() { @Override public Object executeWithContext(DirContext ctx) { .. String uid = ″uid=lucky.boy″; String ppolicyEntry = ″pwdPolicySubentry″; String ppolicyValue = ″cn=relaxed,ou=people,dc=example,dc=com″; // Let's assume that nothing is found, Attributes are empty Attributes attributes = ctx.getAttributes(uid, ppolicyEntry); // All right, let's add a new attribute Attribute attribute = new BasicAttribute(ppolicyEntry, ppolicyValue); attributes.put(attribute); .. }; // Use the template to save the changes template.executeReadWrite(executor); .. }
It took me a couple of days to figure this out. I hope someone finds this post useful.