Friday, September 19, 2008

Java SwingX ActiveDirectory/LDAP LoginService

Sorry for the long break from posting, but I haven't worked on anything interesting enough to post for a while. I've just completed some basic ActiveDirectory auth code with a Java Swing application. I had a heck of a time connecting to AD at all with Java - all the examples I found online seemed like they should have worked, but my test code just hung with no errors, and with no useful debugging info. I'm still not sure what was going on exactly, but the following helper class worked for me (the most notable exception between this code and my previous attempts is that it's now specifying the domain controller):

LDAPUtils.java:
package app;

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

/*
@author Randy Coates
Modified slightly by Dave Gruska
*/
public class LDAPUtils {
static String ATTRIBUTE_FOR_USER = "sAMAccountName";
public Attributes authenticateUser(String username, String password, String _domain, String host, String dn) {
String returnedAtts[] ={ "sAMAccountName", "memberOf" };
String searchFilter = "(&(objectClass=user)(" + ATTRIBUTE_FOR_USER + "=" + username + "))";

//Create the search controls
SearchControls searchCtls = new SearchControls();
searchCtls.setReturningAttributes(returnedAtts);

//Specify the search scope
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
Hashtable environment = new Hashtable();
environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");

//Using starndard Port, check your instalation
environment.put(Context.PROVIDER_URL, "ldap://" + host + ":389");
environment.put(Context.SECURITY_AUTHENTICATION, "simple");

environment.put(Context.SECURITY_PRINCIPAL, username + "@" + _domain);
environment.put(Context.SECURITY_CREDENTIALS, password);
LdapContext ctxGC = null;

try {
ctxGC = new InitialLdapContext(environment, null);

//search for objects in the GC using the filter
String searchBase = dn;
NamingEnumeration answer = ctxGC.search(searchBase, searchFilter, searchCtls);
while (answer.hasMoreElements()) {
SearchResult sr = (SearchResult)answer.next();
Attributes attrs = sr.getAttributes();
if (attrs != null)
return attrs;
}
}
catch (NamingException e) {
e.printStackTrace();
}
return null;
}
}
I'm then extending SwingX's LoginService to work with this class and return some extra details, like the user's group (this can be easily expanded to capture an Active Directory attribute):

LDAPLoginService:
package app;

import javax.naming.directory.Attributes;
import org.jdesktop.swingx.auth.LoginService;

/*
@author Dave Gruska
*/
public class LDAPLoginService extends LoginService {
private String domain;
private String host;
private String dn;
private String userName;
private String groupName;

public String getDomain() {
return domain;
}

public void setDomain(String domain) {
this.domain = domain;
}

public String getHost() {
return host;
}

public void setHost(String host) {
this.host = host;
}

public String getDn() {
return dn;
}

public void setDn(String dn) {
this.dn = dn;
}

public String getUserName() {
return userName;
}

public String getGroupName() {
return groupName;
}

public LDAPLoginService(String domain, String host, String dn) {
this.domain = domain;
this.host = host;
this.dn = dn;
}

@Override
public boolean authenticate(String name, char[] password, String server) throws Exception {
LDAPUtils LDAPlogin = new LDAPUtils();

//TODO: investigate if there's a more efficient way to convert this
StringBuilder passwd = new StringBuilder();
for(char c : password) {
passwd.append(c);
}

Attributes attrs = LDAPlogin.authenticateUser(name, passwd.toString(), this.domain, this.host, this.dn);

if(attrs == null) {
//login failed
return false;
} else {
//login successful
String[] splitUserName = attrs.get("sAMAccountName").toString().split(":");
userName = splitUserName[1].trim();
groupName = attrs.get("memberOf").contains("CN=AppAdmins,CN=Users,DC=domain,DC=com") ? "admin" : "user";

return true;
}
}
}
and finally, the presentation layer code displays a login pane (JXLoginPane dialog) that gets called in the constructor right after the components are initialized:
loginService = new LDAPLoginService("domain.com", "[domain controller IP]", "cn=Users,dc=domain,dc=com");
JXLoginPane.Status status = JXLoginPane.showLoginDialog(null, loginService);

if(!status.equals(status.SUCCEEDED)) {
System.exit(0);
}

loggedInAsLabel.setText(String.format("%S (%s)", loginService.getUserName(), loginService.getGroupName()));
BTW, the System.exit code only gets called when the user gives up on trying to log in.