Spring LDAP Authentication

What is LDAP? LDAP stands for Lightweight Directory Access Protocol. It’s designed to query and interact with directory data. As most enterprises use Microsoft directory service provider called Active Directory or AD, LDAP is commonly used to ‘talk’ to an Active Directory.

Developers tend to be confused about the difference between AD and LDAP. In short, AD is a database and LDAP is a protocol used to talk to an AD database.

The Java JNDI API can also be used for LDAP programming. If you’ve used JNDI API you may realize that it requires a lot more boilerplate code for even for the most simple tasks. Using Spring LDAP template on the other hand is a much more efficient way of implementing all of your LDAP related functionality.

How to use Spring-ldap? Before you write any code, you will need to import the following Spring Framework jars into your Java project. These are:

  • spring-ldap-core.jar
  • spring-core.jar and
  • spring-beans.jar

After importing the above jars, you need to create the ldapTemplate bean in spring context file. For example, I declared my ldapTemplate in the context xml as below.

  
<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
 <property name="url" value="ldap://your.ad.url:389" />
 <property name="base" value="dc=your,dc=base,dc=dn" />
 <property name="userDn" value="${ad.username}" />
 <property name="password" value="${ad.password}" />
 <property name="referral" value="follow" />
 </bean>

 <bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
 <constructor-arg ref="contextSource" />
 </bean>​​ 

url: the url property should be either the IP address or the URL of the Active Directory you’re using. Please make sure that you use port 689 in production. Port 689 ensures communication through LDAPS protocol which is more secure than standard LDAP. You will need to import the certificate of the Active Directory you are connecting to into your java trust store.

After you have correctly set the above properties, you will need to inject the ldapTemplate bean in your java class. In the example below I use the Spring @Autowired annotation to inject the bean into my controller class.

@Autowired LDAPTemplte ldapTemplate;

Once, you’ve injected the bean, you can finally write the java code to enable LDAP Authentication. In the snippet below I write a simple method called authenticate which authenticates a user if the correct username and password combination is supplied.


private boolean authenticate(String username, String password){
 AndFilter filter = new AndFilter();
 filter.and(new EqualsFilter("sAMAccountName", username));
 boolean authenticated = ldapTemplate.authenticate("", filter.toString(), password);
 return authenticated;
}

The above piece of code would work for you if

  • the username supplied is always be correct and
  • the user is always be enabled

This is obviously not going to be the case in real life. So we need some additional information to be returned by this method. It would nice if the method could return information related to failed password count and locked out time. This would help you understand why a user is not authenticated.

We’ll use the user search function provided by spring-ldap to return the user’s attributes. We’ll add the below code to the function we just created above.


List<Object> templates = ldapTemplate.search("", filter.toString(),new AttributesMapper() {
 @Override
 public List<Object> mapFromAttributes(Attributes attrs) throws NamingException {
 List output = new ArrayList<>();
 output.add(attrs.get("badPwdCount").get());
 output.add(attrs.get("lockoutTime").get());
 return output;
 }
});

List<Object> output = (List)templates.get(0);
logger.info("Failed Login Count: "+output.get(0).toString());
if (output.get(1) == null || output.get(1) == 0){
 logger.info("Account is not locked");
} else {
 logger.info("Account is locked")
}

The above code snippet shows how we can use the sAMAccountName attribute to search for the user. The sAMAccountName usually corresponds to the username given to the user. There is a slight chance that this might not be the case for the active directory you’re using so please just verify that sAMAccountName does actually correspond to the username used by users for logging in.

Another thing to note here is that lockoutTime returns a null value if the account has never been locked before and 0 if it’s been locked before but is not locked currently.

Obviously, the method can modified to return the values of other attributes if needed.

Liferay LDAP Export to Active Directory disabled user bug.

Liferay 6.1 LDAP tool has a bug where the Liferay user is exported into Active Directory but is disabled by default. After much deliberation I found a workaround where we can write a hook to export users instead of using the out of the box tool. The hook uses a Model Listener on the User Model to look for changes to the user object i.e. the custom code is triggered when anything about the User object changes. The custom code uses the Java JNDI interface to create and or update users in Active Directory.

	@Override
	public void onAfterCreate(User user) throws ModelListenerException {
		LDAPUtil util = new LDAPUtil();	

				try {
					log.info("Attempting to create user: Screen Name: "
							+ user.getScreenName() + " :First Name: "
							+ user.getFirstName() + " : LastName: "
							+ user.getLastName() + " Email:"
							+ user.getEmailAddress()+ " OpenID: "
					+ user.getOpenId()+ " JobTitle: "
					+ user.getJobTitle());
					util.createAdUser(user.getScreenName(),
							user.getPasswordUnencrypted(), user.getFirstName(),
							user.getLastName(), user.getEmailAddress(),
							user.getOpenId(), user.getJobTitle());

				} catch (Exception e) {
					e.printStackTrace();
					throw new ModelListenerException(e);
				}
	}

 	public void createAdUser(String screenName, String password, String firstName, String lastName, String emailAddress, String openID, String jobTitle) {
		try {

			// Create the initial directory context
			LdapContext ctx = getLdapContext();
			String userDn = getUserDN(screenName);

			// Create attributes to be associated with the new user
			Attributes attrs = new BasicAttributes(true);

			// These are the mandatory attributes for a user object
			// Note that Win2K3 will automagically create a random
			// samAccountName if it is not present. (Win2K does not)
			List<String> objectClasses =  Arrays.asList(PropsUtil.get("your.object.classes").split(","));
			for (String objectClass: objectClasses){
				attrs.put("objectClass", objectClass);
			}
			//attrs.put("samAccountName", screenName);
			
			attrs.put("cn", screenName);
			attrs.put("mail", emailAddress);
			// These are some optional (but useful) attributes
			attrs.put("givenName",firstName);
			attrs.put("sn", lastName);
			attrs.put("displayName", screenName);
			if (openID != null && openID.trim().length() > 0){
				log.info("AD User: Setting emplyeeID to: "+openID);
				attrs.put("employeeID", openID);
			}
			if (jobTitle != null && jobTitle.trim().length() > 0){
				log.info("AD User: Setting emplyeeNumber to: "+openID);
				attrs.put("employeeNumber", jobTitle);
			}
			//attrs.put("userPrincipalName", username);

			// Note that the user object must be created before the password
			// can be set. Therefore as the user is created with no
			// password, userAccountControl must be set to the following
			// otherwise the Win2K3 password filter will return error 53
			// unwilling to perform.
			attrs.put("userAccountControl", Integer.toString(UF_NORMAL_ACCOUNT + UF_PASSWD_NOTREQD + UF_PASSWORD_EXPIRED + UF_ACCOUNTDISABLE));
			log.info("Creating AD user: " + userDn);
			// Create the context
			Context result = ctx.createSubcontext(userDn, attrs);
			log.info("Created account: " + userDn);

			// Set password is a ldap modfy operation
			// and we'll update the userAccountControl
			// enabling the account and forcing the password to never expire
			if (password != null) {
				ModificationItem[] mods = new ModificationItem[2];

				//Replace the "unicdodePwd" attribute with a new value
				//Password must be both Unicode and a quoted string
				String newQuotedPassword = "\"" + password + "\"";
				byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");

				mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("unicodePwd", newUnicodePassword));
				mods[1] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("userAccountControl", Integer.toString(UF_NORMAL_ACCOUNT + UF_DONT_EXPIRE_PASSWD)));

				// Perform the update
				ctx.modifyAttributes(userDn, mods);
				log.info("Successfully set password and updated userAccountControl");
			}

			ctx.close();

		} catch (NamingException e) {
			log.info("NamingException while creating AD user " + screenName);
			throw new RuntimeException(e);
		} catch (IOException e) {
			log.info("IOException while creating AD user " + screenName);
			throw new RuntimeException(e);
		}
	}