LDAP authentication via Lift API in Apache ESME
This blog was written by our new committer Vladimir Ivanov who implemented a feature that users have been wanting for a long time.
In the first two parts of this blog series, scenarios were discussed where the authentication process was performed by the container. While it often makes sense to delegate this task to the container in order to integrate with corporate services such as Single Sign-On, sometimes it might be better to take full control of the authentication process and perform this task directly in the source code. In this final part of the blog series, I'll show how to authenticate user in LDAP via the Lift API as well as introduce some changes that have been made in the authentication modules.
Changes in UserAuth.scala
In order to perform authentication via the Lift API, a new module LDAPAuthModule has been added. Basically, it uses the same operation sequence as the ContainerManagedAuthModule: it gets the user credentials, tries to authenticate and authorize the user, retrieves additional attributes from the LDAP for a new user and finally logs the user in. Since several operations are common to both authentication modules, it is worth placing them in a base trait LDAPBase:
trait LDAPBase {
LDAPBase is marked with a self-type to denote that any concrete class that mixes with this trait is a AuthModule instance
this : AuthModule =>
To check whether the user has a specific role, a list of roles, separated by commas, is read from the property file, split and placed into a val.
val rolesToCheck = Props.get("role_list") match { case Full(s) => s.split(',').toList case _ => Nil }
A new variable was also added to hold the current role for the User.
var currentRole : String = _
The object LDAPVendor and method that extracts attributes from LDAP are used by both modules and were left without changes
object myLdapVendor extends LDAPVendor def myLdap : LDAPVendor = ... def getAttrs(dn : String) : Map[String, List[String]] = ...
The following two methods are used to construct distinguished name for the user/group pair. Since users and groups can have different bases and prefixes, specific properties are retrieved from the property file depending on the isGroup flag (The new feature, default parameters, introduced in Scala 2.8 is used to set default value for this flag).
def constructDistinguishedName(who : String, isGroup : Boolean = false) = { val base = Props.get( if(isGroup) {"ldap.groupBase"} else {"ldap.userBase"} ) openOr "" val dn = "%s,%s".format(constructNameWithPrefix(who, isGroup), base) dn } def constructNameWithPrefix(username: String, isGroup: Boolean = false) = { val prefix = if(isGroup) {"cn"} else {Props.get("ldap.uidPrefix") openOr ""} val nameWithPrefix = "%s=%s".format(prefix, username) nameWithPrefix }
The method logInUser was modified to set the current role for the authenticated User.
def logInUser(who: User) { User.logUserIn(who) User.setRole(currentRole) S.notice(S.?("base_user_msg_welcome", who.niceName)) } }
The singleton object ContainerManagedAuthModule now mixes with the LDAPBase trait and uses it's constructDistinguishedName method to get the LDAP attributes. In all other aspects, it has the same implementation as before.
object ContainerManagedAuthModule extends AuthModule with LDAPBase ...
Now let's review the new LDAPAuthModule.
object LDAPAuthModule extends AuthModule with LDAPBase {
At first, a set of standard methods for all authentication modules is defined:
override def isDefault = false def loginPresentation: Box[NodeSeq] = ... def moduleName: String = "ldap" def createHolder() = ...
The method performInit makes most of the module's work:
def performInit(): Unit = {
The new module is mapped to the /ldap/login URL.
LiftRules.dispatch.append { case Req("ldap" :: "login" :: Nil, _, PostRequest) => val from = S.referer openOr "/"
It is necessary to check whether LDAP is enabled based on the setting in the property file and if that's the case, the user credentials are read from the HTTP request and used for the subsequent authentication in LDAP with the LDAPVendor.bindUser method. It takes the distinguished name (composed of username that comes from the HTTP request and the prefix/user base taken from the property file) and password and returns a Boolean. The next step is to check if the user is authorized with the checkRoles method.
val ldapEnabled = Props.getBool("ldap.enabled") openOr false if(ldapEnabled) { val name = S.param("username").map(_.trim.toLowerCase) openOr "" val pwd = S.param("password").map(_.trim) openOr "" if(myLdap.bindUser(constructNameWithPrefix(name), pwd) && checkRoles(constructDistinguishedName(name))) {
After successful authentication and authorization, an attempt is made to find the existing User. If found, the User is logged in into the application. Otherwise, a new instance of User class is created, populated with attributes from LDAP, saved into the database and then logged in.
(for { user <- UserAuth.find(By(UserAuth.authKey, name), By(UserAuth.authType, moduleName)).flatMap(_.user.obj) or User.find(By(User.nickname, name)) } yield user) match { case Full(user) => logInUser(user) case Empty => val usr = User.createAndPopulate.nickname(name).saveMe val ldapAttrs = getAttrs(constructDistinguishedName(name)) val firstName = ldapAttrs("givenName").head val lastName = ldapAttrs("sn").head val mail = ldapAttrs("mail").head usr.firstName(firstName).lastName(lastName).save UserAuth.create.authType(moduleName).user(usr).authKey(name).save logInUser(usr) } } else { S.error(S.?("base_user_err_unknown_creds")) } } S.redirectTo(from) } }
The method checkRoles takes the username as a parameter and iterates through a list of roles defined in the property file to see whether the current role contains this name as a value of its uniqueMember attribute. If that's the case, it assign this role to the currentRole var of LDAPBase trait (it will be then saved in the HTTP session) and returns true, otherwise it returns false.
def checkRoles(who : String) : Boolean = { for (role <-rolesToCheck) { val ldapAttrs = getAttrs(constructDistinguishedName(role, true)) val uniqueMember = ldapAttrs("uniqueMember").head if(who == uniqueMember) { currentRole = role return true } } return false; }
The last thing I'm going to mention are the two new properties in the default.props file. The property ldap.groupBase specifies the path in LDAP under which groups are searched. The list of application roles is set in the role_list property as a string separated by commas.
default.props
;Group base DN to check whether user has specific role ldap.groupBase=ou=Groups,ou=esme,dc=lester,dc=org ;Allow access to application for following roles role_list=esme-users,monitoring-admin
Note that the lift-ldap module also has to be added as a dependency to the Maven pom.xml and/or the SBT project file to work with the Lift LDAP API.
Conclusion
In this article, we have discovered how to authenticate the User directly in LDAP using the Lift API as well as the corresponding changes in the UserAuth class.
Links
Lift API: http://scala-tools.org/mvnsites/liftweb-2.3/