Using UnboundID to authenticate against an LDAP server

UnboundID is an open source project which provides an SDK for connecting to an LDAP server. Compared with other solutions, UnboundID has two advantages. For starters UnboundId includes an in-memory server which is very practical for unit testing. Secondly (and useful in my practical case) is that the UnboundId API makes it easy to deal with the complexities of an SSL connection.

My use case was to authenticate using LDAP for Geomajas. The easiest solution there is to provide an implementation of the AuthenticationService in the Geomajas staticsecurity plug-in.

Let’s start with the interesting bits, setting up a in-memory server for testing. The server needs to be created. We use a null schema to assure all attributes are allowed. Bind credentials are added for the user we add and a base data is added to the server.

protected InMemoryDirectoryServer server;

@Before
public void before() throws Exception {
  InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=org");
  config.addAdditionalBindCredentials("cn=test,dc=staticsecurity,dc=geomajas,dc=org", "cred");
  InMemoryListenerConfig listenerConfig = new InMemoryListenerConfig("test", null, PORT, null, null, null);
  config.setListenerConfigs(listenerConfig);
  config.setSchema(null); // do not check (attribute) schema
  server = new InMemoryDirectoryServer(config);
  server.startListening();

  server.add("dn: dc=org", "objectClass: top", "objectClass: domain", "dc: org");
  server.add("dn: dc=geomajas,dc=org", "objectClass: top", "objectClass: domain", "dc: geomajas");
  server.add("dn: dc=roles,dc=geomajas,dc=org", "objectClass: top", "objectClass: domain", "dc: roles");
  server.add("dn: dc=staticsecurity,dc=geomajas,dc=org", "objectClass: top", "objectClass: domain",
      "dc: staticsecurity");
  server.add("dn: cn=testgroup,dc=roles,dc=geomajas,dc=org", "objectClass: groupOfUniqueNames",
      "cn: testgroup");
  server.add("dn: cn=test,dc=staticsecurity,dc=geomajas,dc=org", "objectClass: person", "locale: nl_BE",
      "sn: Tester", "givenName: Joe", "cn: test", "memberOf: cn=testgroup,dc=roles,dc=geomajas,dc=org");
}

@After
public void shutdown() {
  server.shutDown(true);
}

In true TDD style, let’s look at the test for our authentication service before we look at the actual code. The main test (excluding initialization) look like this. For the fluent asserts, the FEST-assert library is used.

@Test
public void testLdapAuthenticationService() throws Exception {
  String password = "bladibla";
  assertThat(service.convertPassword("me", password)).isEqualTo(password);

  assertThat(service.isAuthenticated("wrong", "wrong")).isNull();

  String userId = "test";
  UserInfo authResult = service.isAuthenticated(userId, "cred");
  assertThat(authResult).isNotNull();
  assertThat(authResult.getUserId()).isEqualTo(userId);
  assertThat(authResult.getUserName()).isEqualTo("Joe Tester");
  assertThat(authResult.getUserLocale()).isEqualTo(new Locale("nl_BE"));
  assertThat(authResult.getUserOrganization()).isEqualTo("test");
  assertThat(authResult.getUserDivision()).isEqualTo("person");
  List<AuthorizationInfo> auths = authResult.getAuthorizations();
  assertThat(auths).hasSize(1);
}

On to the actual authentication. On a login request, we connect with the LDAP server and send a bind request to see whether the credentials are valid.

There is a special handling for the case when SSL certificate checks should be ignored. This can be particularly useful for testing. It is not recommended in production as this reduces security (and makes you vulnerable to man-in-the-middle attacks and and attacks using DNS, redirecting to a different server).

public UserInfo isAuthenticated(String user, String password) {
  String userDn = userDnTemplate.replace("{}", user);
  LDAPConnection connection = null;
  try {
    if (allowAllSocketFactory) {
      SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
      connection = new LDAPConnection(sslUtil.createSSLSocketFactory(), serverHost, serverPort);
    } else {
      connection = new LDAPConnection(serverHost, serverPort);
    }

    BindResult auth = connection.bind(userDn, password);
    if (auth.getResultCode().isConnectionUsable()) {
      List<String> attributes = new ArrayList<String>();
      attributes.add("cn");
      addAttribute(attributes, givenNameAttribute);
      addAttribute(attributes, surNameAttribute);
      addAttribute(attributes, localeAttribute);
      addAttribute(attributes, organizationAttribute);
      addAttribute(attributes, divisionAttribute);
      addAttribute(attributes, rolesAttribute);
      SearchRequest request = new SearchRequest(userDn, SearchScope.SUB,
          Filter.createEqualityFilter("objectclass", "person"),
          attributes.toArray(new String[attributes.size()]));
      return getUserInfo(user, connection.search(request));
    }
  } catch (LDAPException le) {
    String message = le.getMessage();
    if (!message.startsWith("Unable to bind as user ")) {
      log.error(le.getMessage(), le);
    }
  } catch (GeneralSecurityException gse) {
    log.error(gse.getMessage(), gse);
  } finally {
    if (null != connection) {
      connection.close();
    }
  }
  return null;  // not logged in
}

The information about the logged in user and the roles is built from the configured attributes in the getUserInfo method.

private UserInfo getUserInfo(String userId, SearchResult search) {
  if (search.getEntryCount() > 0) {
    SearchResultEntry entry = search.getSearchEntries().get(0);
    UserInfo result = new UserInfo();
    result.setUserId(userId);
    String name = entry.getAttributeValue(givenNameAttribute);
    String name2 = entry.getAttributeValue(surNameAttribute);
    if (null != name) {
      if (null != name2) {
        name += " " + name2;
      }
    } else {
      name = name2;
    }
    result.setUserName(name);
    result.setUserLocale(entry.getAttributeValue(localeAttribute));
    result.setUserOrganization(entry.getAttributeValue(organizationAttribute));
    result.setUserDivision(entry.getAttributeValue(divisionAttribute));
    result.setAuthorizations(getAuthorizations(entry));
    return result;
  }
  return null;
}

The full source code can be found here.

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

*