<< Powering Through the Tour | Home | Moving attachments from one document to another >>

Writing a custom Tomcat login module (Realm)

In the project I am working on we needed to move the users from LDAP to a database table due to a switch from JBoss to Tomcat as the application server. It was a little complicated since the passwords was hashed with SHA1 in LDAP and we needed the cleartext passwords since we needed to encrypt it in another way. To avoid having users switch their password I wrote a custom login module, called a Realm, in Tomcat. It was really simple.

When logging in the login module will do the following:

  1. Check whether the username is registered in the database. If it is the user is authenticated against the database.
  2. If no username/password is found in the database we go to the LDAP server and try to authenticate the user there.
  3. If the user was authenticated using LDAP we update the database with the username and password supplied before returning a Principal.
  4. If we still haven't authenticated we return null to indicate that we could not authenticate the user.
Simple and effective.

Writing a custom Realm

A realm is defined by the org.apache.catalina.Realm interface. I opted to extend the org.apache.catalina.realm.RealmBase class instead of implementing the interface since it meant I only had to override one method:
public class LdapDbRealm extends RealmBase {
   
   // accessors for realm configuration (database) *******************
   public String getDbUsername() {
      return this.pDbUsername;
   }
   public void setDbUsername(String dbUsername) {
      this.pDbUsername = dbUsername;
   }
   ...
   ...
   
    // accessors for realm configuration (ldap) *******************
    public String getLdapPassword() {
      return this.pLdapPassword;
   }
   public void setLdapPassword(String ldapPassword) {
      this.pLdapPassword = ldapPassword;
   }
   ...
   ...
   
    // methods from super class ***************************************

   /**
    * This method is the work-horse of the class. The implementation goes 
    * through the follow steps:
    * <ol>
    * <li>Checks whether the username is registered in the database. If it is the 
    * user is authenticated against the database.</li>
    * <li>Next we go to the LDAP server and checks whether the user can be authenticated 
    * there.</li>
    * <li>If the user was authenticated using LDAP we update the database with the 
    * username and password supplied before returning a Principal.</li>
    * </ol>
    * 
    * @see org.apache.catalina.realm.RealmBase#authenticate(java.lang.String, java.lang.String)
    */
   public Principal authenticate(String username, String credentials) {
      ...
      ...
   }

}

The accessor methods are used to set information from the server.xml document where the Realm is configured for the Context:

<Context path="/guide" docBase="guide.war" debug="0" reloadable="true" crossContext="true">
   <Logger className="org.apache.catalina.logger.FileLogger"
      prefix="hgguide_log." suffix=".txt" verbosity="4" timestamp="true"/>

   <!-- configure login realm -->
   <Realm className="dk.horisontnet.guide.login.LdapDbRealm" 
      dbDriverName="COM.ibm.db2.jdbc.app.DB2Driver" 
      dbUrl="jdbc:db2:guide" 
      dbUsername="db_user" 
      dbPassword="password" 
      ldapUsername="cn=Manager, o=Example" 
      ldapPassword="password" 
      ldapSearchBase="ou=Users, ou=Guide, o=Example" 
      ldapUrl="ldap://localhost"
   />
   
   ...
   ...
   
</Context>

All the attributes to the <Realm /> tag should have a matching accessor pair in the Realm implementation so they can be set by Tomcat at startup. A caveat is that you need to define the Realm implementation in a MBean descriptor and add the descriptor to the ServerLifecycleListener at the top of the server.xml:

<Listener className="org.apache.catalina.mbeans.ServerLifecycleListener"
            debug="0" 
            descriptors="/com/example/realm/mbean-descriptors.xml" />

The descriptor file looks like this:

<?xml version="1.0"?>
<!DOCTYPE mbeans-descriptors PUBLIC
 "-//Apache Software Foundation//DTD Model MBeans Configuration File"
 "http://jakarta.apache.org/commons/dtds/mbeans-descriptors.dtd">
<mbeans-descriptors>
   <mbean name="LdapDbRealm" className="org.apache.catalina.mbeans.ClassNameMBean"
      description="Implementation of custom Realm"
      domain="Catalina"
      group="Realm"
      type="com.example.realm.LdapDbRealm">
      
      <attribute name="debug"
         description="The debugging detail level for this component"
         type="int"/>
                  
      <attribute   name="dbDriverName"
         description="The JNDI named JDBC DataSource for your database"
         type="java.lang.String"/>
         
      <attribute   name="dbDsName"
         description="The column in the user role table that names a role"
         type="java.lang.String"/>

      ...
      ...
   </mbean>
   
</mbean-descriptors>
                 

Installation

The realm class and the descriptor is installed in Tomcat by adding it to the server.xml as described above and by copying the class files to the %CATALINA_HOME%\server\classes directory together with the descriptor file. The path in the Listener tag (see above) should match the path of the descriptor.

Custom Principal and ClassCastException problems

I created my own java.security.Principal implementation that I return from the authenticate method of Realm implementation since I need some additional information about the user. I then tought that I simply could cast the java.lang.Principal returned by the HttpServletRequest.getUserPrincipal() method to my implementation class but i kept getting java.lang.ClassCastExceptions. :-(

The solution could be found in the Tomcat wiki and was to use reflection. This is due to the fact that the class is loaded by different classloaders and hence are different as seen by Tomcat.

Beware of runtime dependencies (added 8/Feb/2006)

I have just upgraded the Tomcat instance the Realm is used in from Tomcat 4.1.x to 5.5.x which proved difficult since the RealmBase class being extended has changed. Please see here for more information. The lesson is to make sure to compile the Realm against the actual version of Tomcat you are deploying on.

Tags :