Secure Java Authentication (SSL) to Active Directory

Following on from an old article I wrote, with regards to getting your java applications to authenticate against an LDAP compliant directory server, I thought I’d write another article explaining how to encrypt your communication. It’s all very well getting your users to authenticate against AD, but sending their username/password in cleartext isn’t ideal.

To begin, I have a default installation of Windows Server 2012 R2 onto which I have installed the Active Directory Domain Services role. I created a new user, setup a password and tested I could connect to AD via java using port 389 (unencrypted). It’s also useful to note you can test this with the Microsoft built in client ldp.exe (I’ll be using this later).

You might well have an AD server setup that already supports using port 636 with SSL, but for completeness I’ll explain what I did for testing, as it took a little while to find the correct information. I found various articles about creating a certificate authority, self signing a certificate, then importing into various places using mmc.exe. None of these seemed to work for me and I was unable to establish an SSL connection over port 636 using ldp.exe. In the end I added the role Active Directory Certificate Services (just the Certificate Authority part) and followed the configuration wizard to create an initial certificate. After rebooting the server, the certificate was up and running and a simple Connect using server, port, SSL checked in ldp.exe worked! See the acknowledgements section at the bottom for a guide on installing AD Certificate Services. It’s important you succeed in connecting before moving onto the java code as SSL can with fiddly enough without fighting against other configuration issues as well.

The alterations needed to the code from the original article are pretty small. To keep it short, I assume SSL is required if the default port 636 is used.

LDAPConnection conn = null;
if (port == 636) {
    conn = new LDAPConnection(new LDAPJSSESecureSocketFactory());
} else {
    conn = new LDAPConnection();
}
conn.connect(host, port);

If you have purchased an SSL certificate from a trusted certificate authority, you should be ready to go. If you are using a self signed SSL certificate you’ll see the following errors when trying to bind.

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

To stop this, you need to export the certificate from the AD server and import it into a java keystore so you acknowledge you trust it. Using mmc.exe (add the Certificates snapon if it’s not already added), look in Certificates (Local Computer) – Personal – Certificates and find the AD certificate. Right click on it, All Tasks – Export, do NOT export the private key and export in DER format. I called mine ad-server.cer

The following command will create a new java keystore (called cacerts in the current directory) and import the certificate you just exported.

keytool -import -alias ad.tilion.org.uk -keystore cacerts -file ad-server.cer

You can double check the import with the following command.

keytool -list -keystore cacerts

To run your java code with the new keystore, you need to use javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword if your keystore requires a password. Pay careful attention to the variable names as there are equivalent ending in keyStore, but it’s important we use trustStore in this case. For example;

java -Djavax.net.ssl.trustStore=PATH_TO/cacerts -jar app.jar

If your code is a webapp deployed in something like Apache Tomcat you need to make the addition to the Tomcat startup. For the windows service install, right click on the service monitor and from the menu, Configure…, then under the Java tab update the Java Options: by adding the extra variables. For other Apache Tomcat installs, you need to add to the JAVA_OPTS parameter in catalina.bat or catalina.sh depending if you’re using Windows or Linux.

As a final note, this article describes how to use SSL on port 636. The encrypted channel is setup before any LDAP related communication takes place. If you’re trying to setup TLS it might be working over the default 389 port as TLS is an option that can be enabled by supported clients over unencrypted ports. LDAP communication will take place before the client tries to enable TLS.

Acknowledgements

Java Internationalisation (i18n) Character Encoding

Internationalisation (i18n) of java applications should not be difficult, although dealing with text in languages you don’t understand can be a little confusing! As a developer, you’ll normally be sent a translated version of the text to use in your application. If you’re really lucky the translator will be able to work directly with a java property file and you’ll get back a translated version to drop into your application.

A common scenario will be a series of java property files along the lines of;

  • Messages.properties – key/value pairs in base language, assumed to be English
  • Messages_XX.properties – key/value pairs in language with country code XX
  • Messages_XX_YY.properties – key/value pairs in language with country code XX, variant YY

Take the file Messages_ar.properties which contains an Arabic translation. Dropping that directly into your application will probably result in it being ignored, cue head scratching … The issue is that java property files must use character encoding ISO-8859-1 and to have been converted into Arabic the file is probably using character encoding UTF8 (or ISO-8859-6). Sun/Oracle solve this problem using native2ascii as follows (rename your original Arabic translation to Messages_ar-UTF8.properties);

native2ascii -encoding utf8 Messages_ar-UTF8.properties Messages_ar.properties

The resulting Messages_ar.properties file isn’t as readble as the UTF8 version as all values have been converted to unicode – but, at least it now works!

If you want to keep your translations in UTF8 encoded files you need to be using Java 1.5 or greater along with XML based property files.

Xmpp/Jabber commons-logging handler (Tomcat)

I wanted to create a live monitoring system to use with some of the webapps I have running in Apache Tomcat. There were various options, but as I have a secure xmpp/jabber chat system setup already, I thought it’d be handy to use that as the transport medium for any communication.

On one server I have an xmpp chat system (ejabberd running on debian) running over SSL. On other servers I have instances of Apache Tomcat running various web applications.

  • I created a new java.util.logging.Handler that could be used with tomcat.
  • Created xmpp user accounts within ejabberd for each webapp I wanted to monitor.
  • Updated the logging.properties file for each webapp and restarted.

The handler was setup to auto-accept contact requests (not a problem on an internal chat server), so I added the new xmpp user accounts as contacts on my own account. Step 1 successful, I can now see when the webapps are online and do something about it when I notice they go offline. To receive live logging information, I’d also built the following commands into the handler.

  • !subscribe
  • !unsubscribe
  • !level <LEVEL_NAME>

First two are pretty obvious, they allow you to subscribe and unsubscribe from being sent log messages via chat. The third command allows you to select the minimum level messages you want to see – on production servers I get way too many messages if I listen to ALL, so I tend to choose WARNING as I’m only interested in seeing if things go wrong.

An example logging.properties file updated from the default one that ships with tomcat;

handlers = org.apache.juli.FileHandler, uk.org.tilion.logging.XmppHandler, java.util.logging.ConsoleHandler

uk.org.tilion.logging.XmppHandler.server = chat.tilion.org.uk
uk.org.tilion.logging.XmppHandler.port = 5222
uk.org.tilion.logging.XmppHandler.username = webapp1-name
uk.org.tilion.logging.XmppHandler.password = webapp1-password

org.apache.juli.FileHandler.level = ALL
org.apache.juli.FileHandler.directory = ${catalina.base}/logs

java.util.logging.ConsoleHandler.level = SEVERE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

The code is squeezed into a single file and packaged in a maven project that uses smack/smackx to perform the xmpp communication. I’ve not attached it to this post, but if anyone is interested, leave a comment below and I’ll make it available.

Quake 3 network protocol 43 proxy server

I decided to experiment with java NIO and thought it’d be useful to resurrect my quake 3 proxy server. The completed code may be a useful followup to an original article I wrote many years ago about the Quake 3 network protocol

If I get time I’ll create a version for protocol 68 that can decode the Huffman compressed packet data on the fly, but don’t hold your breath!

Instructions can be found in the code comments, otherwise leave me a message below.

package uk.org.tilion.quake3.proxy;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.charset.Charset;
import java.util.Iterator;

/**
 * A Quake3 proxy server that communicates using protocol 43, 1.11 - 1.16 point
 * release of quake3 engine. It's designed to handle a single client and provide
 * a way of experimenting with the quake3 protocol on the fly.
 * 
 * Packet fragmentation is ignored for simplicity, with sequenceId being read
 * without performing fragmentation checks.
 * 
 * Example handshake (OOB prefixed packets)
 *   CLIENT : getChallenge
 *   SERVER : challengeResponse <ID>
 *   CLIENT : connect "cg_predictItems1sexmalehandicap100color3snaps40rate10000modeldoom/rednameUnnamaedPlayerprotocol68qport<PORT>challenge<ID>"
 *   SERVER : connectResponse
 * 
 * Client game data packet
 * +-----------------+-----+----------+-----------+
 * | NAME            | LEN | ENCODING | ENDIANESS |
 * +-----------------+-----+----------+-----------+
 * |  SequenceNumber |  32 |     None |    Little |
 * |           QPort |  16 |     None |    Little |
 * |                 |     |      XOR |           |
 * 
 * Server game data packet
 * +-----------------+-----+----------+-----------+
 * | NAME            | LEN | ENCODING | ENDIANESS |
 * +-----------------+-----+----------+-----------+
 * |  SequenceNumber |  32 |     None |    Little |
 * |                 |     |      XOR |           |
 * 
 * If anyone gets time to implement the XOR decoding, do let me know! I suspect
 * it's the Netchan_UnScamblePacket() function from qcommon/net_chan.c in the 
 * 1.32 full source release.
 * 
 * @author Darren Edmonds
 */
public class Protocol43ProxyServer {

    private static final int BUFFER_SIZE = 1024 * 2;  // size in bytes
    
    private int localPort;
    private SocketAddress serverAddr;
    private SocketAddress clientAddr;
    private DatagramChannel serverChannel; // proxy <==> quake3 server
    private DatagramChannel clientChannel; // proxy <==> quake3 client
    private boolean running;
    
    Protocol43ProxyServer(int localPort, String remoteServer, int remotePort) {
        this.localPort = localPort;
        this.serverAddr = new InetSocketAddress(remoteServer, remotePort);
    }

    /**
     * Start the proxy server
     * @throws IOException 
     */
    public void start() throws IOException {
        Selector selector = Selector.open();
        
        this.clientChannel = DatagramChannel.open();
        this.clientChannel.configureBlocking(false);
        this.clientChannel.socket().bind(new InetSocketAddress(this.localPort));
        this.clientChannel.register(selector, SelectionKey.OP_READ);
        
        this.serverChannel = DatagramChannel.open();
        this.serverChannel.configureBlocking(false);
        this.serverChannel.socket().bind(null);
        this.serverChannel.register(selector, SelectionKey.OP_READ);
        
        ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
        buffer.order(ByteOrder.BIG_ENDIAN);

        this.running = true;
        Iterator<SelectionKey> it = null;
        SelectionKey key = null;
        while (this.running) {
            int n = selector.select();
            if (n == 0) continue; // nothing to do
            
            it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                key = it.next();
                
                if (key.isReadable()) {
                    this.handlePacket((DatagramChannel)key.channel(), buffer);
                }
                
                it.remove();
            }
        }
    }

    /**
     * Read a packet of data from the client (or server) datagram channel then
     * proxy it over to the other channel.
     * @param channel
     * @param buffer
     * @throws IOException 
     */
    private void handlePacket(DatagramChannel channel, ByteBuffer buffer)
            throws IOException {
        buffer.clear();
        
        if (channel == this.clientChannel) {
            SocketAddress sender;
            
            while ((sender = channel.receive(buffer)) != null) {
                if (this.clientAddr == null) this.clientAddr = sender;
                //System.out.println("CLIENT sent " + buffer.position() + " bytes");
                inspectClientPacket(buffer);
                writePacket(this.serverChannel, buffer, this.serverAddr);
            }
            
        } else {
            while (channel.receive(buffer) != null) {
                //System.out.println("SERVER sent " + buffer.position() + " bytes");
                inspectServerPacket(buffer);
                writePacket(this.clientChannel, buffer, this.clientAddr);
            }
        }
    }

    /**
     * Write buffer to channel, transmitting content to recipient
     * @param channel
     * @param buffer
     * @param recipient
     * @throws IOException 
     */
    private void writePacket(DatagramChannel channel, ByteBuffer buffer,
            SocketAddress recipient) throws IOException {
        buffer.flip();
        while (buffer.hasRemaining()) {
            channel.send(buffer, recipient);
        }
    }

    /**
     * Chance to inspect/modify packet sent from client before it is relayed
     * to the server
     * @param buffer
     */
    private void inspectClientPacket(ByteBuffer buffer) {
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        int sequenceId = buffer.getInt(0);

        if (sequenceId == -1) { // 4 OOB bytes in the header
            byte[] textArr = new byte[buffer.position() - 4];
            int oldPos = buffer.position();
            buffer.position(4);
            buffer.get(textArr);
            buffer.position(oldPos);

            String command = new String(textArr, Charset.forName("UTF-8"));
            System.out.println("CLIENT " + command);
            
            if (command.startsWith("connect ")) {
                /* rewrite the qport value to reflect the proxy local port
                 * rather than the client local port - not critical for testing
                 * but required for multiple clients on same IP via NAT */
            }
        } else {
            int qport = buffer.getShort(4);
            System.out.println("CLIENT seq=  " + String.format("%15d", sequenceId));
            System.out.println("CLIENT qport=" + String.format("%15d", qport));
        }
        
        buffer.order(ByteOrder.BIG_ENDIAN);
    }

    /*
     * Chance to inspect/modify packet sent from server before it is relayed
     * to the client
     * @param buffer
     */
    private void inspectServerPacket(ByteBuffer buffer) {
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        int sequenceId = buffer.getInt(0);
        
        if (sequenceId == -1) { // 4 OOB bytes in the header
            byte[] textArr = new byte[buffer.position() - 4];
            int oldPos = buffer.position();
            buffer.position(4);
            buffer.get(textArr);
            buffer.position(oldPos);

            String command = new String(textArr, Charset.forName("UTF-8"));
            System.out.println("SERVER " + command);
            
            if (command.startsWith("connectResponse")) {
                /* server puts client into connecting state and starts sending
                 * game updates */
            }
        } else {
            System.out.println("SERVER seq=  " + String.format("%15d", sequenceId));
        }
        
        buffer.order(ByteOrder.BIG_ENDIAN);
    }

    /**
     * Main
     * @param args 
     */
    public static void main(String[] args) {
        Protocol43ProxyServer server = new Protocol43ProxyServer(
                27960, 
                "myserver.com", // CHANGE TO REAL SERVER IP
                27961); // CHNAGE TO REAL SERVER PORT
        try {
            server.start();
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        
        /* now start quake3.exe and connect localhost:27960
         * your connection will end up at myserver.com:27961, proxied via
         * localhost to allow you to inspect packets on the fly */
    }
}

Convert java keystore key into DSA (understood by Apache)

I have a wildcard SSL certificate bought from GoDaddy that serves various websites. Most of them run on Apache Tomcat, but when I had the need to run an SSL secured site in Apache HTTP server as well, I had the choice … buy another SSL certificate (which seemed pointless as I already own a wildcard certificate and can use whatever subdomains I need), or work out how to get my certificate up and running on both servers.

The problem isn’t getting my signed certificate in formats that both Apache Tomcat and Apache HTTP will understand, that bit’s easy. The problem is that my certificate request was created using a private key stored in a java keystore, which Apache HTTP doesn’t understand. Quick google later and the solution wasn’t very difficult, here’s what I did!

GoDaddy created my certificate and provide their root CA bundle, let’s call them wild.tilion.org.uk.crt and gd_bundle.crt respectively.

I created the initial signing request with a java keystore, tomcat.keystore, under alias tilion.

Usage instructions for ExportPriv were quick and easy to follow.

javac ExportPriv.java Base64Coder.java
java ExportPriv tomcat.keystore tilion <password> | openssl pkcs8 -inform PEM -nocrypt > wild.tilion.org.uk.key

Apache2 configuration parameters:

SSLEngine on
SSLCertificateFile /path/to/wild.tilion.org.uk.crt
SSLCertificateKeyFile /path/to/wild.tilion.org.uk.key
SSLCertificateChainFile /path/to/gd_bundle.crt

Tomcat SSL Certificate – Alias tomcat name does not identify a key entry

A post that may help someone out if they get into the same situation I did with regards to importing SSL certificates into a java keystore for Tomcat.

When renewing my certificate, my CA had the ability to use my old CSR (certificate signing request) which I accepted as it saved me a few minutes. Before, I’d always started with an empty keystore, generated my private key, CSR, then imported my new certificate along with the any needed to complete the chain. It seemed easy, I just needed to import my new certificate into the old keystore, right?

keytool -import -alias intermed -keystore tomcat.keystore -trustcacerts -file gd_intermediate.crt
Enter keystore password:  
keytool error: java.lang.Exception: Certificate not imported, alias  already exists

I see, I’ve already got a certificate under that alias, so I need to remove it first. Into the manual, -delete option looks good and away we go … I delete and then import 2 certificates that make up the chain and do the same with my newly issued certificate. Update my tomcat config to be greeted by the following:

LifecycleException:  service.getName(): "Catalina";  Protocol handler start failed: java.io.IOException: Alias name tomcat does not identify a key entry

To cut a long story short, when you use the -delete option of keytool on an alias with a private key in it, it doesn’t just remove the certificate, it removes your private key as well. Adding in my new certificate is all well and good if I no longer have a private key associated with it! The correct thing to do is not use the -delete option at all, because keytool will not complain if you’re importing a new certificate like that over the top of an old one, e.g I already have a certificate in the alias ‘tomcat’ but …

keytool -import -alias tomcat -keystore tomcat.keystore -trustcacerts -file mydomain.com.crt
Enter keystore password:  
Certificate was added to keystore

Tilion Live Tournament Manager Demo

Imagine the scene … you’re running a pool competition and need some way to coordinate everything on the day. You could use pieces of paper, even excel spreadsheets, but wouldn’t it be easier if you could do it all in one simple software application?

What follows isn’t a new idea and doesn’t necessarily include new concepts. However, it is my take on a solution that allows me to continually extend if necessary – it’s often very difficult to extend someone else’s work, especially if it is closed source.

Screenshots

Players overview: when you announce a competition you’ll get a list of players stating their interest, some even paying you money, but at this point there is no guarantee they’ll turn up on the day.

Competitions overview: most competitions I’ve been involved in include a main event and then a plate event to keep early round losers interested. Competitions have multiple rounds and can be run simultaneously over the available tables.

Teams overview: for most competitions this tab would be better named as Registered Players, but the software is designed to work for single, or team events, in the same way. When the players arrive at the venue you can register them by creating a team entry (1 or more players per team) for a particular competition.

Matches overview: this tab shows a complete list of matches that need to be played. Grouped by competition/round and showing home team and away team.

Tables overview: tables available for use at the venue. Each table has a status so you can tell if it’s in use, or the players on it just happen to be practicing and causing delays!

Status: this page is where the benefit of using a software application comes into its own. At the top you can see the currently in progress matches. At the bottom you can see a list of matches that need to be played and if there are any free tables.

Omissions from this first demo?

Yes, there are some things missing and some things I’ll change as development progresses. For example, most tabs include a data entry area and an Add button which makes development/testing easier, but these will be moved into dialog boxes in time as they clutter up the main UI.

There’s also no obvious way to create matches between teams. This is coming, although it’s a bit more complex than other tabs as it needs to include a manual match creation and a randomised draw creation.

Results, I don’t see anywhere I can see match results! Again, an extra tab to be developed. The reason it’s not in the demo screenshots is that I don’t want it to be a tab in the same way as the other sections. I want it to be a separate window so those that run tournaments with a laptop and second screen can display the results window on the second screen. This allows players to come and see results (as well as the draw) without having to bug the organiser with questions.

Finally, the extra bits only possible by a software application. What if there was an internet forum you planned to update with results throughout the day? Wouldn’t it be nice if there was a Publish button that did everything for you – from logging into the forum, formatting the results and continually updating each time you click the Publish button? And, what about a website? A lot of organisers need their results published in a way they can put on their website.

The development continues …

JasperReports Tips

JasperReports is a java based open source reporting framework with similar, if not more, functionality to that of Crystal Reports. There is a visual report designer called iReport, but my experience of using it for anything but the simplest report has been a waste. It is useful for dragging and dropping report layouts around, but once you get the hang of it, it’s far easier to do it directly in XML.

Documentation is available, but it’s quite often difficult to get simple answers via a google search – as is possible for most open source software. The current books also lack good examples when they explain some of the more powerful features. Simple things like, I don’t want my report to have nulls printed on it, what are my options are suprisingly awkward to locate! My own, FAQs …

  • Null values in reports, what can I do?

    If you want to replace the null value with a blank entry this is easy to achieve with the isBlankWhenNull attribute of textField

    <textField isBlankWhenNull="true"> ... </textField>

    If you need to replace the null value with something else, you can do this directly in the report as shown below. Using the ternary operator the GROUP_NAME field is checked for a null, if it is null the text None is used instead.

    <textFieldExpression>(($F{GROUP_NAME} != null) ? $F{GROUP_NAME} : "None")</textFieldExpression>
  • How to add a row count?

    If you need a row count and don’t want to add unnecessary fields into the SQL statement you can use a variable. Straight after your field definitions include the following, assuming you have a field named USER_NAME in your fied definitions!

    <variable name="row_count" class="java.lang.Integer" calculation="Count">
        <variableExpression><![CDATA[$F{USER_NAME}]]></variableExpression>
        <initialValueExpression><![CDATA[new java.lang.Integer(0)]]></initialValueExpression>
    </variable>

    This can be used in the report as follow

    <textField>
        <reportElement x="0" y="0" width="30" height="20"/>
        <textFieldExpression class="java.lang.Integer"><![CDATA[$V{row_count}]]></textFieldExpression>
    </textField>
  • Highlight certain rows depending on the value of other data?

    If you need to highlight some text, the trick is to create two versions of the textField that displays it and use the <printWhenExpression> tag to determine which one displays the text. The below example highlight in red if the PASS_DATE field is null.

    <!-- main textField -->
    <textField>
        <reportElement x="466" y="0" width="228" height="20">
            <printWhenExpression>new Boolean($F{PASS_DATE} != null)</printWhenExpression>
        </reportElement>
        <textFieldExpression class="java.lang.String"><![CDATA[$F{USER_NAME}]]></textFieldExpression>
    </textField>
    
    <!-- highlight textField -->
    <textField>
        <reportElement x="466" y="0" width="228" height="20" forecolor="red">
            <printWhenExpression>new Boolean($F{PASS_DATE} == null)</printWhenExpression>
        </reportElement>
        <textFieldExpression class="java.lang.String"><![CDATA[$F{USER_NAME}]]></textFieldExpression>
    </textField>

JSTL Internationalisation

Some notes on how to get JSTL pages ready.

Setup

Inside a JSP page included on every other, for example a header jsp file.

<fmt:setLocale value="${param.locale}" scope="request" />
<fmt:setTimeZone value="${param.timeZone}" scope="request" />
<fmt:setBundle basename="Messages"/>

At your classpath root create a fallback localised file called Messages.properties in which to store key/value pairs. Files for other languages take the form of Messages_en.properties, Messages_en_US.properties, etc.

page.text.title=Page Title
page.text.message=Page Message
page.text.welcomeMessage=Welcome {0}
page.text.returnLink=<a href="{0}" title="Return">Return</a>

Examples

<h1>Page Title</h1>
<h1><fmt:message value="page.text.title"/></h1>

<h1>Welcome <c:out value="${person.name}"/></h1>
<h1><fmt:message value="page.text.welcomeMessage"><fmt:param value="${fn:escapeXml(person.name)"}/></fmt:message></h1>

Java Authentication – LDAP and Active Directory

Been asked to integrate your application’s authentication with an LDAP directory (Active Directory is LDAP v3 compliant)? Me too! There is a fair amount of information about this topic available by searching, but when I was doing this I couldn’t find one place that had everything explained in detail, so I decided to document how I did it.

What follows will explain how to validate a username/password combination against an LDAP compliant directory server using java and the opensource LDAP library called jldap.

First off, go and download the jldap jar file and browse around the code samples as it’s a well documented library. Second, take a look how easy it is to make a connection to an LDAP directory server.

LDAPConnection conn = new LDAPConnection();
conn.connect("localhost", 389);
conn.bind("cn=admin,dc=tilion,dc=org,dc=uk", "password");

If you’re not too sure about LDAP syntax you may like to read the Wiki LDAP entry. In short, LDAP uses a tree structure where each entry has a unique identifier, known as it’s Distinguished Name (DN). In the username above cn=admin is the Relative Distinguished Name (RDN) and dc=tilion,dc=org,dc=uk is the DN of it’s parent entry. Put together these form the DN for a user with privileges to bind to the LDAP directory (DC stands for Domain Component). Entries generally have a CN attribute, known as the Common Name along with a whole load of more familiar named attributes.

If binding to an Active Directory server, the username is more likely to be of the format cn=Administrator,cn=Users,dc=tilion,dc=org,dc=uk. Part of the complexity with LDAP queries is that there is no fixed format for where particular types of entry live from server to server. Most Active Directory servers will be alike, but won’t be the same when compared to a Novell directory or an OpenLDAP server. For the purposes of authentication we need to locate where in the directory the entries that represent a user object live.

  • cn=Users,dc=tilion,dc=org,dc=uk is the default for Active Directory
  • ou=People,dc=tilion,dc=org,dc=uk is the default an OpenLDAP server storing unix users accounts (the one I have anyway!)

Whoever set up the LDAP server should be able to tell you the base DN for your environment. For example, when setting up Active Directory you specify the name (check terminology) in the format machine.domain.ext, which would lead to the base DN of dc=machine,dc=domain,dc=ext.

Here is where we get to the querk. Imagine we need to check a login where the username is darren and the password, well, lets just say it’s the right password for the username. On an OpenLDAP server, all you need to do is try binding to the directory as shown below.

conn.bind("uid=darren,ou=People,dc=tilion,dc=org,dc=uk", "password");

uid is the attribute that holds the actual username value (the CN, or Common Name is often different to the actual username).

Unfortunately, Active Directory is different, in that you can only bind to it using a DN, which references the actual entry via it’s CN. What you have to do is perform a query to check if the username exists, grab it’s CN and then perform a bind using the CN and the given password. So, let’s see this in action …

// assume we have a connection, which is already bound
LDAPSearchResults searchResults = conn.search(
        "cn=Users,dc=tilion,dc=org,dc=uk",
        LDAPConnection.SCOPE_ONE,
        sAMAccountName + "=" + <username>,  // <username> came from the user trying to login
        null,
        false);
LDAPEntry entry = searchResults.next();
if (entry != null) {  // the username is valid, lets pull out the CN from the attributes
    String cnValue = null;
    LDAPAttributeSet attrSet = entry.getAttributeSet();
    Iterator allAttrs = attrSet.iterator();
    while (allAttrs.hasNext()) {
        LDAPAttribute attr = (LDAPAttribute)allAttrs.next();
        String attrName = attr.getName();
        if (attrName.equalsIgnoreCase("cn")) {  // we got the CN
            cnValue = attr.getStringValues().nextElement();
        } else {
            continue;
        }
    }

    if (cnValue == null) {
        // return auth failed, the username doesn't exist
    }

    // attempt a bind with CN and given password
    LDAPConnection tmp = new LDAPConnection();
    tmp.connect(HOST, PORT);
    tmp.bind("cn=" + cnValue + "," + "cn=Users,dc=tilion,dc=org,dc=uk", <password>);  // <password> came from the user trying to login

    // return auth successful, username and password are valid

    // an LDAPException is thrown if the credentials are invalid
}

Concepts covered, you’re probably wondering how are you going to find all those cn,dn,dc,xyz details about your particular LDAP directory? That’s exactly why I created a standalone application to query an LDAP server when I was learning this stuff. You can download the LDAP test application (NOT UPLOADED YET!), which includes the the compiled jar, full source and a maven pom.xml.

The code shown here is for illustration purposes only and should not be used in production without proper error handling additions. It is as concise as possible to illustrate a point.

Useful Attributes

A quick round up of useful attributes in various LDAP compliant servers.

Active Directory

  • sAMAccountName holds the username
  • displayName holds the full name
  • mail holds the email address

OpenLDAP (holding unix user accounts)

  • uid holds the username
  • cn holds the full name
  • mailacceptinggeneralid holds the email address