Diagnosing LDAP authentication from JES Web Server

This entry gives a short walkthrough of the steps that occur in JES Web Server when it needs to authenticate and authorize a client (say, browser) request using an LDAP server. Understanding what goes on behind the scenes makes it easier to diagnose configuration issues.

In this example the server is configured with an ACL which requires LDAP authentication such as the one below which states that access to /index.html is limited to those users who authenticate correctly and are part of the group webappusers in the ldap directory.

acl "uri=/index.html";
authenticate (user,group) {
  database = "myldap";
  prompt = "ldap authentication";
deny (all) (user = "all");
allow (all) (group = "webappusers");

The database ‘myldap’ referenced above is defined in dbswitch.conf where the directory URL and base DN are configured:

directory myldap ldap://ldapserver:389/dc=virkki,dc=com

I turned on verbose logging in my LDAP server. If you have access to configure the LDAP server you’re authenticating against I recommend doing the same as it will make it easier to follow what is going on (and if you don’t have such access, consider setting up a test LDAP server of your own for this purpose).

I access /index.html through a browser. As expected, I’m prompted for authentication. I entered my username ‘jvirkki’ and my password. Here’s what I find in the LDAP server logs afterwards (omitting some repetitive fields to cut the clutter):

op=0 BIND dn="" method=128 version=3
op=0 RESULT err=0 tag=97 nentries=0 etime=0 dn=""
op=1 SRCH base="dc=virkki,dc=com" scope=2 filter="(uid=jvirkki)" attrs="c"
op=1 RESULT err=0 tag=101 nentries=1 etime=0
op=2 BIND dn="uid=jvirkki,ou=People, dc=virkki,dc=com" method=128 version=3
op=2 RESULT err=0 tag=97 nentries=0 etime=0 dn="uid=jvirkki,ou=people,dc=virkki,
op=3 BIND dn="" method=128 version=3
op=3 RESULT err=0 tag=97 nentries=0 etime=0 dn=""
op=4 SRCH base="dc=virkki,dc=com" scope=2 filter="(|(&(objectClass=groupofunique
names)(|(uniqueMember=uid=jvirkki,ou=People, dc=virkki,dc=com)))(&(objectClass=g
roup)(|(member=uid=jvirkki,ou=People, dc=virkki,dc=com)))(&(objectClass=groupofn
ames)(|(member=uid=jvirkki,ou=People, dc=virkki,dc=com))))" attrs="cn"
op=4 RESULT err=0 tag=101 nentries=1 etime=0
op=5 SRCH base="dc=virkki,dc=com" scope=2 filter="(&(cn=webappusers)(|(objectCla
Class=groupofcertificates)(objectClass=group)))" attrs="c"
op=5 RESULT err=0 tag=101 nentries=1 etime=0

Now, let’s duplicate the steps done by the web server using ldapsearch(1).

In the interest of space I will only go through the succesful request presented above. However, I encourage you to try several failure cases at every step of the way (wrong user, wrong password, wrong group) both via ldapsearch as well as through your browser. Trying both the success and failure cases is quite useful for becoming familiar with what is going on.

The first thing that happens after the browser transmitted my user name ‘jvirkki’ and my password to the web server is that the web server needs to find if such a user even exists. If the client sent a non-existing user there is no need to do anything else – if the user does not exist it will certainly not be able to authenticate nor will it belong to any group (additionally, in order to bind as this user in the next step the server needs to know the full DN of the LDAP entry corresponding to the user, not just the uid which at this point is all it has).

To do this a search is issued to find ‘uid=jvirkki’, which corresponds with the lines of op=1 in the LDAP logs above. We can do the same from the CLI:

% ldapsearch -h ldapserver -p 389 -b "dc=virkki,dc=com"  "(uid=jvirkki)"
version: 1
dn: uid=jvirkki,ou=People, dc=virkki,dc=com
uid: jvirkki
givenName: jyri
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetorgperson
sn: virkki
cn: jyri virkki

As the output shows, the user named jvirkki was found and the LDAP entry is “uid=jvirkki,ou=People, dc=virkki,dc=com”. By default ldapsearch displayed all the attributes of the entry.

A few notes about the ldapsearch issued: Note that the value of the basedn, given by the -b option, must be the same as the basedn configured for this directory in the corresponding dbswitch.conf entry. The last argument to ldapsearch is the search filter, here, “(uid=jvirkki)”. Notice that this corresponds to the ‘filter’ string found in the LDAP server logs above for op=1.

Minor note: If you look at the LDAP logs at this stage you may notice a small difference, the log entry corresponding to the manual ldapsearch query reads ‘attrs=ALL’ whereas the log entry corresponding to the web server query reads ‘attrs=”c”‘. You could duplicate this
precisely by issuing the following ldapsearch command instead:

% ldapsearch -h ldapserver -p 389 -b "dc=virkki,dc=com"  "(uid=jvirkki)" c
version: 1
dn: uid=jvirkki,ou=People, dc=virkki,dc=com

Ok now that the web server knows the user exists it needs to verify the password. This corresponds to the log entries op=2 seen earlier. You can duplicate this from ldapsearch by giving the -D option which tells ldapsearch to attempt to bind (authenticate) as the given user and then entering the same password you entered into the browser:

% ldapsearch -h ldapserver -p 389 -b "dc=virkki,dc=com" -D "uid=jvirkki,ou=Peopl
e, dc=virkki,dc=com"  "(uid=jvirkki)" c
Enter bind password:
version: 1
dn: uid=jvirkki,ou=People, dc=virkki,dc=com

All right.. so the user exists and the password is correct. If the ACL permitted this user to perform the request, that would be all that is needed. However, the ACL I set earlier requires the user to belong to the group ‘webappusers’. So that’s the next step.

Look at the log entry for op=4 above. The web server queries to see if there is any group where this user is a member and it wants to find the name (cn) of such a group (if any). Copying the filter line from the op=4 log entry to the ldapsearch command:

% ldapsearch -h ldapserver -p 389 -b "dc=virkki,dc=com"  "(|(&(objectClass=group
ofuniquenames)(|(uniqueMember=uid=jvirkki,ou=People, dc=virkki,dc=com)))(&(objec
tClass=group)(|(member=uid=jvirkki,ou=People, dc=virkki,dc=com)))(&(objectClass=
groupofnames)(|(member=uid=jvirkki,ou=People, dc=virkki,dc=com))))" cn
version: 1
dn: cn=webappusers,ou=Groups, dc=virkki,dc=com
cn: webappusers

A group entry ‘cn=webappusers,ou=Groups, dc=virkki,dc=com’ was identified. Next the web server attempts to retrieve the LDAP entry corresponding to this name. I’ll reproduce the query once again by copying the filter line, from op=5, to ldapsearch:

% ldapsearch -h ldapserver -p 389 -b "dc=virkki,dc=com"  "(&(cn=webappusers)(|(o
version: 1
dn: cn=webappusers,ou=Groups, dc=virkki,dc=com
description: users who have access to the web applications
objectClass: top
objectClass: groupofuniquenames
cn: webappusers
uniqueMember: uid=jvirkki,ou=People, dc=virkki,dc=com

There’s the group and “uid=jvirkki,ou=People, dc=virkki,dc=com” is a member (see uniqueMember attribute) of it. The ACL requirements have been satisfied and the index.html page is sent to the browser.

If you compare the LDAP server logs between the web server and manual runs (you should), you’ll probably notice a few differences. These don’t change the results we’re interested in that I discussed above. Here’s some of the differences:

  • Each ldapsearch CLI invocation needs to connect and bind, run the
    search and then disconnect. So you’ll see a lot more BIND/UNBIND
    events occurring when running ldapsearch. The web server reuses
    connections so it does not need to constantly connect and disconnect.
  • ldapsearch is meant to (surprise!) run a search, so some search filter needs to be given. Even when I only care to check the password (in step 2),
    I gave ldapsearch a search filter to run. When the web server checks
    the password it can issue only a BIND so the logs will not show this
    extra search.
  • The web server runs all the searches under the identity of the default
    user (if any) given for this LDAP server in dbswitch.conf (see
    binddn/bindpw options in http://docs.sun.com/source/817-6248/crflothr.html#wp1026118). Since the web server reuses connections, after binding as the remote user (‘jvirkki’, in my example) to verify the password, it will re-bind as the binddn user to restore the correct identity for subsequent searches. Since ldapsearch creates a new connect ion every time, this is not needed. Since the myldap directory configuration does not include binddn/bindpw, anonymous bind is used whenever needed (corresponding to the BIND dn=”” entried in the logs).

Hopefully this walk-through helps you visualize what goes on when the web server uses an LDAP directory for authentication and access control. As we’ve seen the steps are quite straightforward and they can easily be reproduced via command line LDAP tools. If you ever need to diagnose such installations it is very helpful to run these steps to observe what is going on:

  • Turn on verbose logging, both on the web and LDAP servers.
  • In the LDAP server logs, observe what requests are being sent.
  • Reproduce the same queries via ldapsearch. If you can not find or
    authenticate or check the group info via ldapsearch, it’s not going to
    work via the web server either.
  • If nothing seems to work, run ldapsearch with the search filter
    “(objectclass=*)”. Every LDAP entry has an objectclass attribute and
    by matching all objectclass values, you will get a list of every visible LDAP
    entry (under the given base DN). If that output doesn’t contain the entries
    you were looking for, either the basedn (-b) is wrong, the LDAP server doesn’t
    really contain any of the data you thought it did or maybe the LDAP server
    is not configured to allow access to that data given the user (-D) you’re using.