Entries tagged [authentication]

Thursday May 26, 2011

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/

Thursday April 07, 2011

Container-Managed Authentication with Apache ESME: Part 1

Part 1: Performing authentication with plaintext/xml user-role mapping

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

Introduction

Apache ESME currently supports two different authentication schemes: when user credentials are stored in the database and via OpenID.  Corporate users, however, might be interested in container-managed authentication (CMA) — because this scheme supports integration with enterprise services such as LDAP and Single Sign-On. In the first part of this blog,  I'll explain how ESME-based applications can use CMA and how to configure two popular web servers- Apache Tomcat and Jetty - with simple user-role mapping. In the second part, I'll describe LDAP integration to perform CMA and how get additional user attributes via the Lift LDAP API.

ContainerManagedAuthModule: The necessary code changes

A new authentication module ContainerManagedAuthModule was introduced to hook into the container-managed authentication process. First of all, it was registered along with the other authentication modules:

Boot.scala

    UserAuth.register(UserPwdAuthModule)

    UserAuth.register(OpenIDAuthModule) 

    UserAuth.register(ContainerManagedAuthModule)

UserAuth.scala

All authentication modules should extend the AuthModule trait:

object ContainerManagedAuthModule extends AuthModule

Currently, the list with security role (group) names is also defined in the source code:

  val rolesToCheck = List(

    "esme-users"

  )

It is also possible to get the list of roles from some external source, for example, from a property file or a LDAP.

The method moduleName defines the name for the new auth module. This value acts as a discriminator and will be stored in the DB:

  def moduleName: String = "cm"

After the container finishes the authentication and authorization phases, it is  neccessary to hook into the normal user processing to save the user data. This task is performed in the performInit method:

def performInit(): Unit = {

CMA must be applied to a specific URL, for example /cm/login, so it is necessary to append a partial function to LiftRules.dispatch to perform the neccessary operations:

    LiftRules.dispatch.append {

      case Req("cm" :: "login" :: Nil, _, _) =>  {

        val from = "/"

Note: The majority of necessary steps to further utilize this new auth method have already been desribed in the Lift Wiki.

In short, it is neccessary to unwrap the javax.servlet.http.HttpServletRequest object to get the username and role names. If a user has one of the specified roles, the module should attempt to find an existing user with the same nickname which previously has logged in via this module. If such a user hasn't been found, a new User is created.  The last step is to save the userId in the HTTP session via User.logUserIn method call.

        S.request match {

          case Full(req) => {

            val httpRequest: HTTPRequest = req.request

            val hrs = httpRequest.asInstanceOf[HTTPRequestServlet]

            val hsr: HttpServletRequest = hrs.req

            val username : String = hsr.getRemoteUser

            if(username!=null){

              val currentRoles = rolesToCheck.filter(hsr.isUserInRole(_))

              if(currentRoles.size == 0) {

                info("No roles have been found")

                S.error(S.?("base_user_err_unknown_creds"))

              } else {

                currentRoles.map(cr => {

                (for {

                    user <- UserAuth.find(By(UserAuth.authKey, username),

                                          By(UserAuth.authType, moduleName)).flatMap(_.user.obj) or

                    User.find(By(User.nickname, username))

                  } yield user) match {

                    case Full(user) => {

                      logInUser(user)

                    }

                    case _ => {

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

                      //find and save additional attributes in LDAP if it's enabled

              ...

                      }

                      UserAuth.create.authType(moduleName).user(usr).authKey(username).save

                      logInUser(usr)

                    }

                  }

                })

              }

    }

Configuration

Now it's time to set-up the configuration for the CMA. All configuration settings for a Java EE web application (ESME is based on the Lift web framework, so it's packaged as a WAR file), including security settings, are defined in the web.xml file:

web.xml

For this example, we will use form-based authentication:

<login-config>

      <auth-method>FORM</auth-method>

      <realm-name>ESMERealm</realm-name>

Next, the login and error pages are specified.

        <form-login-config>

            <form-login-page>/cm_login.jsp</form-login-page>

            <form-error-page>/cm_error.jsp</form-error-page>

        </form-login-config>

    </login-config>

Then, the security-role name, which any authenticated user must have for successful authorization, is defined:

    <!-- Security roles referenced by this web application -->

    <security-role>

      <description>An authenticated ESME user</description>

      <role-name>esme-users</role-name>

    </security-role>

And finally it is necessary to configure the mapping between the security role and the URL which is associated with the new authentication module:

  <!-- Secured resources -->

    <security-constraint>

      <web-resource-collection>

        <web-resource-name>ForceLogin</web-resource-name>

        <description>Secured page for forcing the container to request login</description>

        <url-pattern>/cm/login</url-pattern>

      </web-resource-collection>

      <auth-constraint>

        <role-name>esme-users</role-name>

      </auth-constraint>

    </security-constraint>

The login page contains a form with specific action attributes and two input fields:

cm_login.jsp

<html>

    <head>

      <title>Login</title>

    </head>

    <body id="cm_login">

        <form method="POST" action="j_security_check">

          Username: <input type="text" name="j_username"/><br>

          Password: <input type="password" name="j_password"/><br>

          <input type="submit"/>

        </form>

    </body>

</html>

Let's move on to the web server configuration for the next steps. We must define the users for our web application as well as mapping between these users and the security role that is specified in the web.xml file. I'll show how to configure simple user-role mapping for two popular web servers — Jetty and Tomcat.

Jetty

The HashUserRealm implementation is used to specify the user-role mapping in the properties file for Jetty. The maven-jetty-plugin has been already included in the Maven project file pom.xml for the ESME application, so it is possible to configure Jetty in the plugin configuration section:

pom.xml

            <plugin>

                <groupId>org.mortbay.jetty</groupId>

                <artifactId>maven-jetty-plugin</artifactId>

                <configuration>

                    <contextPath>/</contextPath>

                    <scanIntervalSeconds>0</scanIntervalSeconds>

                    <userRealms>

                        <userRealm implementation="org.mortbay.jetty.security.HashUserRealm">

                            <name>ESMERealm</name>

                            <config>jetty-login.properties</config>

                        </userRealm>

                    </userRealms>

                </configuration>

            </plugin>

The format for this property file has the following form: username: password [,rolename ...].

An example is shown below:

jetty-login.properties

cmuser: cmuser, esme-users

That's it. Now Jetty is configured for CMA. Execute mvn clean jetty-run command to start Jetty and type http://localhost:8080/cm/login URL in your browser. You should see the form containing the username and password fields. Now try to log in with the user with the id cmuser.

Tomcat

The configuration of Tomcat web server is very similar to that of Jetty, except that the user-role mapping is specified in a XML file.  The MemoryUserDatabaseFactory implementation is used to define the mapping file.  The corresponding realm UserDatabaseRealm is also specified in the server.xml configuration file:

server.xml

  <GlobalNamingResources>

    <!-- Editable user database that can also be used by

         UserDatabaseRealm to authenticate users

    -->

    <Resource name="UserDatabase" auth="Container"

              type="org.apache.catalina.UserDatabase"

              description="User database that can be updated and saved"

              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"

              pathname="conf/tomcat-users.xml" />

  </GlobalNamingResources>

...

      <Realm className="org.apache.catalina.realm.UserDatabaseRealm"

             resourceName="UserDatabase"/>

Below is an example of the user-role mapping definition in the tomcat-users.xml file which is usually located in the  tomcat/conf directory.

tomcat-users.xml

<tomcat-users>

  <role rolename="esme-users"/>

  <user name="cmuser" password="cmuser" roles="esme-users"/>

</tomcat-users>

Now it's neccessary to package the WAR file with the mvn clean package command and deploy it to Tomcat either via maven plugin or in Tomcat's administrative console. Then proceed to the following URL:

http://localhost:8080/your_web_context/cm/login

The login form should be displayed.

Conclusion

In this part of the blog, I've covered the new authentication module, application and server configuration and simple user-role mapping. In the next part, I'll show how to configure Tomcat to use LDAP for CMA and get additional attributes for the authenticated user.

Links

1. Lift Wiki - How to use Container Managed Security : http://www.assembla.com/wiki/show/liftweb/How_to_use_Container_Managed_Security

2. Jetty HashUserRealm: http://jetty.codehaus.org/jetty/jetty-6/apidocs/org/mortbay/jetty/security/HashUserRealm.html

3. Tomcat Realms: http://tomcat.apache.org/tomcat-6.0-doc/realm-howto.html

4. Java EE 5 Tutorial – Securing Web Applications: http://download.oracle.com/javaee/5/tutorial/doc/bncas.html

Calendar

Search

Hot Blogs (today's hits)

Tag Cloud

Categories

Feeds

Links

Navigation