This blog was written by our new committer Vladimir Ivanov who implemented a feature that users have been wanting for a long time. 

In this blog, I'll discuss how container-managed authentication works
with an LDAP server and how to connect and get additional information from it.
I'll use Jetty and Apache Tomcat web servers with Apache Directory Server (ADS)
1.5.7 - a certified LDAPv3 compatible server. It is also possible to use
different LDAP compatible server such as a 389 Directory Server or MS Active
Directory.

An LDAP server is a directory service that contains objects organized in
a hierarchical manner.

Apache Directory Server (ADS)

The ADS project page provides a detailed server installation and
configuration guide, but here are the basic steps: during the installation, the
default server instance will be created. The configuration settings for this
instance are defined in the server.xml file as Spring bean definitions.
Below is an excerpt from the configuration file that was used for this
blog.

server.xml

First of all, it is necessary to create a new partition:

...

...

suffix="dc=lester,dc=org" />

Ports that LDAP server will use are specified as tcpTransport elements:

id="ldapServer"

allowAnonymousAccess="false"

saslHost="ldap.example.com"

saslPrincipal="ldap/ldap.example.com@EXAMPLE.COM"

searchBaseDn="ou=users,ou=system"

maxTimeLimit="15000"

maxSizeLimit="1000">


nbThreads="8" backLog="50" enableSSL="false"/>

enableSSL="true"/>

The local path to the directory containing LDIF files should also be
specified:

id="apacheDS">

#ldapServer

PATH_TO_ADS_INSTALL_DIR/instances/default/ldif

 

Other configuration settings were left unchanged.

Now It is time to create the directory structure. I used the popular
open source tool JXplorer for this task.

It is possible to export the directory structure from the JXplorer tool
to the LDIF file — directory content in text format. Let's review the generated
file.

lester.ldif

Below is a record list beginning from the top. Note that each record has
specific set of object classes defined in this schema. Each object class
defines specific set of attributes, for example, person class defines Surname
(sn) and Given Name (givenName) attributes. One object
class can extend another, for example organizationalPerson class extends
person class. top is a superclass for all other classes.

Domain lester.org is the root of this hierarchy:

dn: dc=lester,dc=org

objectClass: extensibleObject

objectClass: domain

objectClass: top

dc: lester

The Organizational Unit esme is placed one level lower:

dn: ou=esme,dc=lester,dc=org

objectClass: organizationalUnit

objectClass: top

ou: esme

The Organizational Unit Groups resides under the esme
Organizational Unit:

dn: ou=Groups,ou=esme,dc=lester,dc=org

objectClass: organizationalUnit

objectClass: top

ou: Groups

There is only one group esme-users on the lowest level of the hierarchy
and it has the vivanov user (specified by a full path) as a unique member:

dn: cn=esme-users,ou=Groups,ou=esme,dc=lester,dc=org

objectClass: groupOfUniqueNames

objectClass: top

cn: esme-users

ou: Groups

uniqueMember:
uid=vivanov,ou=Users,ou=esme,dc=lester,dc=org

The Organizational Unit Users resides under the esme
organizational unit:

dn: ou=Users,ou=esme,dc=lester,dc=org

objectClass: organizationalUnit

objectClass: top

ou: Users

The user vivanov and its corresponding attributes are defined on the
lowest level of the hierarchy:

dn: uid=vivanov,ou=Users,ou=esme,dc=lester,dc=org

objectClass: organizationalPerson

objectClass: person

objectClass: uidObject

objectClass: inetOrgPerson

objectClass: top

cn: vivanov

givenName: Vladimir

mail: vivanov@lester.org

ou: Users

sn: Ivanov

telephoneNumber: +7 111 222 33 44

uid: vivanov

userPassword:: cXdlcnR5

There is also a special user with administrative rights: uid=admin,ou=system
(default password: secret) defined in system schema. It was
used to connect to ADS from JXplorer.

Those were all basic steps necessary to configure ADS for purpose of
this b log. Let's move to the configuration of the web servers.

Configuration

Before digging into the configuration details specific for each web
server, let's review the common properties used to connect to the LDAP server.

First of all, it is necessary to specify the hostname / ip address and the
port of our LDAP server — localhost:10389, as well as the credentials
for an account that has rights to perform search operation and get attributes
for users and roles in a directory tree. The special admin user
described in previous section was also used for this purpose in this blog.
Sometimes anonymous access is also permitted.

The next set of properties, user base and group base, specify the base
context with which to lookup users and groups. For our configuration web server
will search users under ou=Users,ou=esme,dc=lester,dc=org and groups
under ou=Groups,ou=esme,dc=lester,dc=org paths in the directory tree
accordingly.

The user id and role name attributes specify the prefix for user/group
search filter. In our example, it has the uid value for users and the cn value
for groups.

The uniqueMember attribute is
used to check whether the user belongs to the specified group.

Now it is time to review the configuration for each of the web servers.

Note: The required lift-ldap
dependency has been already included in pom.xml.

Jetty

In order to configure Jetty to use LDAP server, two additional Maven
dependencies: jetty-plus and jetty-ldap-jaas should be added to
the pom.xml file.
Configuration of maven-jetty-plugin includes the following steps: set the
JAASUserRealm as an user realm implementation and specify the ldaploginmodule as the login module
name. It is also necessary to set the system property java.security.auth.login.config
with the ldap-loginModule.conf value:

pom.xml

org.mortbay.jetty

jetty-plus

[6.1.6,)

compile

org.mortbay.jetty

jetty-ldap-jaas

[6.1.6,)

compile

org.mortbay.jetty

maven-jetty-plugin

/

0

implementation="org.mortbay.jetty.plus.jaas.JAASUserRealm">

ESMERealm

ldaploginmodule

java.security.auth.login.config

ldap-loginModule.conf

The file ldap-loginModule.conf is placed under the ESME_ROOT/server
folder. It specifies login module implementation class - LdapLoginModule as
well as LDAP-specific connection properties:

ldap-loginModule.conf

ldaploginmodule {

org.mortbay.jetty.plus.jaas.ldap.LdapLoginModule
required

debug="true"

useLdaps="false"

contextFactory="com.sun.jndi.ldap.LdapCtxFactory"

hostname="localhost"

port="10389"

bindDn="uid=admin,ou=system"

bindPassword="secret"

authenticationMethod="simple"

forceBindingLogin="false"

userBaseDn="ou=Users,ou=esme,dc=lester,dc=org"

userRdnAttribute="uid"

userIdAttribute="uid"

userPasswordAttribute="userPassword"

userObjectClass="inetOrgPerson"

roleBaseDn="ou=Groups,ou=esme,dc=lester,dc=org"

roleNameAttribute="cn"

roleMemberAttribute="uniqueMember"

roleObjectClass="groupOfUniqueNames";

};

Note that for some environments forceBindingLogin attribute must
also be set to true.

Tomcat

The only required change in the Tomcat's server.xml configuration
file (compared to the changes described in the last blog) is a different realm
- JNDIRealm. This realm is used
to connect to LDAP server and search users/groups:

server.xml

...

className="org.apache.catalina.realm.JNDIRealm"

connectionName="uid=admin,ou=system"

connectionPassword="secret"

connectionURL="ldap://localhost:10389" debug="99"

referrals="follow"

roleBase="ou=Groups,ou=esme,dc=lester,dc=org"

roleName="cn"

roleSearch="(uniqueMember={0})"

roleSubtree="true"

userBase="ou=Users,ou=esme,dc=lester,dc=org"

userSearch="(uid={0})"

userSubtree="true"/>

LDAPVendor and ESMELdap.properties
file

The web server is now configured to perform CMA. But the Servlet API
makes only the user principal available for application. In order to fill ESME
user's profile, additional attributes such as firstname, lastname and email are
needed. We will use LDAP server to retrieve these attributes. Let's review
changes in UserAuth.scala, specifically in ContainerManagedAuthModule object:

UserAuth.scala

To connect to the LDAP server from application first of all, it is necessary to create aq subclass of net.lift.ldap.LDAPVendor
class:

object
myLdapVendor extends LDAPVendor

All LDAP-specific connection properties are placed into a resource
bundle — plaintext property file with key-value pairs. It is possible to get
property values by key with the S.? method:

def myLdap :
LDAPVendor = {

val
ldapSrvHost = S.?("ldap.server.host")

val
ldapSrvPort = S.?("ldap.server.port")

val
ldapSrvBase = S.?("ldap.server.base")

val
ldapSrvUsrName = S.?("ldap.server.userName")

val
ldapSrvPwd = S.?("ldap.server.password")

val
ldapSrvAuthType = S.?("ldap.server.authType")

val
ldapSrvReferral= S.?("ldap.server.referral")

val
ldapSrvCtxFactory = S.?("ldap.server.initial_context_factory")

The next step is to configure the LDAPVendor subclass with these
values:

myLdapVendor.configure(Map("ldap.url" ->
"ldap://%s:%s".format(ldapSrvHost, ldapSrvPort),

"ldap.base" -> ldapSrvBase,

"ldap.userName" -> ldapSrvUsrName,

"ldap.password" ->
ldapSrvPwd,

"ldap.authType" -> ldapSrvAuthType,

"referral" -> ldapSrvReferral,

"ldap.initial_context_factory" -> ldapSrvCtxFactory))

myLdapVendor

}

The method getAttrs takes the username as a parameter and returns
a map of [attribute name / list of attribute values] pairs (attribute in LDAP
might contain more than one value) for this user. Let's review the method
definition. It is possible to get attributes for user with LDAPVendor.attributesFromDn()
method. It takes the distinguished name
as a parameter, so it is necessary to append the prefix and the user base from the
property file to the username to construct it. Note that the attributesFromDn
method returns javax.naming.directory.Attributes therefore the interfaces
from javax.naming.directory package must be
imported correctly:

import _root_.javax.naming.directory.{Attributes,
Attribute => Attr}

The shorthand Attr is used for the javax.naming.directory.Attribute
because the scala.xml.Attribute trait has already been imported
and placed in scope.

Then attribute's id and values are used to populate the result map.

The getAttrs method definition
is shown below:

def
getAttrs(who : String) : Map[String, List[String]] = {

val
uidPrefix = S.?("ldap.uidPrefix")

val userBase
= S.?("ldap.userBase")

var attrsMap
= Map.empty[String, List[String]]

val dn =
"%s=%s,%s".format(uidPrefix, who, userBase)

val attrs :
Attributes = myLdap.attributesFromDn(dn)

if (attrs !=
null) {

val
allAttrs = attrs.getAll();

if
(allAttrs != null) {

while(allAttrs.hasMore()) {

val
attribute = allAttrs.next().asInstanceOf[Attr];

var
attrValues = List.empty[String]

for(i
<- 0 until attribute.size()) {

attrValues ::= attribute.get(i).toString

}

attrsMap += (attribute.getID() -> attrValues)

}

}

}

attrsMap

}

The last step is to modify the performInit method. First of all, it
is necessary to check if LDAP is enabled as configured in the property file.
Then the values for attributes givenName, sn and mail are
extracted from the map, returned via the getAttrs method call and then used
to populate User instance.

def performInit(): Unit = {

...

val usr =
User.createAndPopulate.nickname(username).saveMe

//find and
save additional attributes in LDAP if It is enabled

val
ldapEnabled = S.?("ldap.enabled")

if(ldapEnabled.toBoolean) {

val
ldapAttrs = getAttrs(username)

val
firstName = ldapAttrs("givenName").head

val
lastName = ldapAttrs("sn").head

val mail
= ldapAttrs("mail").head

usr.firstName(firstName).lastName(lastName).save

}

...

}

The ESMELdap property file is
shown below. It essentially resembles connection properties in web server
configuration files that we have seen previously.

ESMELdap.properties file

#This flag specifies whether LDAP should be used

ldap.enabled=true

# Hostname or IP of LDAP server

ldap.server.host=localhost

# Port
of LDAP server

ldap.server.port=10389

# Base DN from the LDAP Server

ldap.server.base=ou=esme,dc=lester,dc=org

# User that has access to LDAP server to perform
search operations

ldap.server.userName=uid=admin,ou=system

# Password for user above

ldap.server.password=secret

# Authentication type

ldap.server.authType=simple

# Referral

ldap.server.referral=follow

# Initial context factory class

ldap.server.initial_context_factory=com.sun.jndi.ldap.LdapCtxFactory

# Prefix for user to whom additional LDAP attributes
belong, for example 'uid' or 'sAMAccountName'

ldap.cnPrefix=uid

# User base DN for user to whom additional LDAP
attributes belong

ldap.userBase=ou=Users,ou=esme,dc=lester,dc=org

The last thing that must be done is to tell Lift where to look for the ESMELdap.properties
file. The list of resource file names is assigned to LiftRules.resourceNames
var in Boot.scala:

Boot.scala

LiftRules.resourceNames = "ESMELdap" :: "ESMECustom"
:: "ESMEBase" :: "ESMEUI" :: Nil

Conclusion

We have just configured both web servers - Jetty and Tomcat - to perform
authentication and authorization via LDAP. We have also improved ContainerManagedAuthModule
to get additional attributes for authenticated user from LDAP with Lift LDAP
API.

Links

1. Apache Directory Server: http://directory.apache.org/apacheds/1.5/

2. Jxplorer: http://jxplorer.org/

3. Jetty login modules: http://docs.codehaus.org/display/JETTY/JAAS

http://jetty.codehaus.org/jetty/jetty-6/apidocs/org/mortbay/jetty/plus/jaas/ldap/LdapLoginModule.html

4. Tomcat user realms: http://tomcat.apache.org/tomcat-6.0-doc/realm-howto.html

5. Lift API: http://scala-tools.org/mvnsites/liftweb-2.3/