Combining CASFilter with Tomcat Realms using SecurityFilter

Introduction

For my web applications I could have reinvented the wheel and produced a homegrown authorisation method for use with CAS. The problem with this being that code I produced would undoubtedly contain multiple security holes and be fairly application specific, so instead I plumped for a different and hopefully more generic approach. I have configured an application to use two different servlet filters for authentication and authorisation respectively.

A major benefit of this is a web application can remain relatively unpolluted by the authorisation mechanism (save for isUserInRole(role) queries), much as CASFilter removes the burden of writing authentication code in the application.

Another benefit is that it allows you to deploy your app as a single, deployable unit (war file or expanded war directory structure) with no additional configuration of the server environment being necessary.

A Tomcat Realm is a "database" of usernames and passwords that identify valid users of a web application, plus an enumeration of the list of roles associated with each valid user. You can think of roles as similar to groups in Unix-like operating systems, because access to specific web application resources is granted to all users possessing a particular role (rather than enumerating the list of associated usernames). A particular user can have any number of roles associated with their username.

What I have achieved is to configure an application to use CASFilter for authentication and Tomcat Realms for role based authorisation. This is all accomplised by using SecurityFilter which is a framework that allows you to plugin any authentication mechanism that can then be used with a Tomcat Realm mechanism. SecurityFilter is a Java Servlet Filter that mimics container managed security. It looks just like container managed security to your web application, as you can call request.getRemoteUser(), request.isUserInRole() and request.getUserPrincipal() and get valid responses. The SecurityFilter configuration file follows the web.xml standard, which makes it easy to switch to SecurityFilter from container managed security, or switch back as your requirements or deployment environment details change. The SecurityFilter adds a layer on top of the application servers realm implementation. SecurityFilter is intended to be contained within your web application so that no further configuration of the server environment is necessary.

Described below are the steps necessary to get CASFilter, Tomcat realms and SecurityFilter working together.

  1. Setup, configure and ensure your Tomcat realm is working standalone
  2. Install and configure SecurityFilter to work with Tomcat realm
  3. Install and configure CASFilter and make the necessary modifications to SecurityFilter to use the CAS authentication

In my example I'm going to describe how to use Tomcat's JDBCRealm implementation as the source for authorisation information. This example will use a database but there are other realm implementations available that can access data from directory services (e.g. LDAP) using JNDI. For reference I am using Tomcat 4 but things shouldn't be too different from what I describe for Tomcat 5 configuration. Also I am using Oracle. I will assume the prior existence of the CAS server and enough familiarity with Java to know how to use Ant to recompile a java application. The following example is based upon using SecurityFilter 2.0.

As this is a layered architecture initially at least it is important that at each stage we confirm that each mechanism is working as intended.

Step 1. Setup, configure and ensure your Tomcat realm is working standalone

Quick summary:
  • Setup Tomcat JDBCRealm
  • Create database table and populate with test users and roles
  • Install database driver
  • Setup some test html files
  • Setup context and web.xml
  • Test

First you need to ensure that the JDBCRealm configuration is working in standalone. For the default Tomcat JDBCRealm mechanism to work it is necessary to install copy of your database drivers in:

$CATALINA_HOME/common/lib/

I setup a single "tomcat_user_role" table. Eventually the usernames in your role table will need to match the authenticated usernames that CASFilter returns. Note how there is no password column in tomcat_user_role table, this is intentional, we will configure the JDBCRealm to accept username=password at the moment. This may look insecure but later on when we add the CASFilter this is the trick to how we marry the CAS authentication with the realm implementation.


CREATE TABLE TOMCAT_USER_ROLE
(
 USER_NAME  VARCHAR2(15 BYTE)                  NOT NULL,
 ROLE_NAME  VARCHAR2(15 BYTE)                  NOT NULL
)

I added a "test" user with the role of "admin" to the above table in the database.

INSERT INTO TOMCAT_USER_ROLE
( USER_NAME, ROLE_NAME ) VALUES ( 'test', 'admin');

For test purposes we now setup a context file so that the default Tomcat Realm mechanism knows where to find the database, this might look something like this (we won't actually need this later once SecurityFilter is configured):

$CATALINA_HOME/webapps/mywebapp.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context path="/mywebapp"
     docBase="mywebapp"
        debug="0"
        privileged="false"
        reloadable="false">
        <Realm className="org.apache.catalina.realm.JDBCRealm" debug="0"
              driverName="oracle.jdbc.driver.OracleDriver"
              connectionURL="jdbc:oracle:thin:@<hostname>:<port>:<sid>"
              connectionName="dbUsername"
              connectionPassword="dbPassword"
              userTable="tomcat_user_role" userNameCol="user_name" userCredCol="user_name"
               userRoleTable="tomcat_user_role" roleNameCol="role_name" />
</Context>

Configure your web.xml to make use of the realm information. This may look something like:

web.xml
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE web-app PUBLIC
  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <display-name>My secure webapp</display-name>
  <security-constraint>
          <web-resource-collection>
                  <web-resource-name>My secure webapp</web-resource-name>
                  <description> accessible by authenticated users of the admin role</description>
                  <url-pattern>/*</url-pattern>
                  <http-method>GET</http-method>
                  <http-method>POST</http-method>
                  <http-method>PUT</http-method>
                  <http-method>DELETE</http-method>
          </web-resource-collection>
          <auth-constraint>
                  <description>These roles are allowed access</description>
                  <role-name>admin</role-name>
          </auth-constraint>
 </security-constraint>

  <login-config>
          <auth-method>FORM</auth-method>
          <realm-name>My secure webapp</realm-name>
          <form-login-config>
                  <form-login-page>/login.html</form-login-page>
                  <form-error-page>/autherr.html</form-error-page>
          </form-login-config>
  </login-config>

  <security-role>
          <description>Only 'admin' role is allowed to access this web application</description>
          <role-name>admin</role-name>
  </security-role>

</web-app>

add a login page to your web application (login.html for testing purposes only):

login.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Login Page</title>
</head>
<body>
<h1>Login to Secure Web App</h1>
<p>
If you have been issued a username and password, key them in here now!
</p>
<form method="POST" action="j_security_check">
Username : <input type="text" size="15" maxlength="25" name="j_username"><br><br>
Password : <input type="password" size="15" maxlength="25" name="j_password"><br><br>
<input value="Login" type="submit">&nbsp;&nbsp;&nbsp;&nbsp;<input value="Clear" type="reset">
</form>
</body>
</html>

add an error page to your web application (autherr.html for testing purposes):

autherr.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Authentication Error!</title>
</head>
<body>
<h1>Authentication Error!</h1>
<p>Oops! You either keyed in the wrong username or password.</p>
<a href="javascript:history.back(1)">Try again ?</a>
</body>
</html>

and add a default welcome page (index.html):

index.html
<html>
<head><title>Welcome to SecurityFilter-Test</title></head>
<body>
<h1>Welcome to this Secure Webapp </h1>
<p>If you can see this, that means that you have been successfully authenticated and authorized.</p>
</body>
</html>

Restart you tomcat server and if all is well you should be able to test that the Tomcat JDBCRealm based authentication and authorisation is working.

If your Tomcat JDBCRealm authentication/authorisation is working as expected you can continue to the next step.

Step 2. Install and configure SecurityFilter to work with Tomcat realm

Quick summary:
  • Install SecurityFilter files and setup web.xml
  • Setup securityfilter-config.xml
  • Test

Obtain the binary download of the SecurityFilter software. Extract the following jar files from the securityfilter-catalina.war file and install them in your WEB-INF/lib directory.

commons-beanutils.jar
commons-collections.jar
commons-logging.jar
securityfilter.jar
commons-codec.jar
commons-digester.jar
jakarta-oro.jar
*catalina.jar (Tomcat 5 version; see problem this fixes below)

Add the SecurityFilter filter configuration to your web.xml file. You will no longer need the security configuration it contained for the standalone JDBCRealm test (you might like to comment this out in case you need to test the JDBCRealm in standalone mode in the future). You also no longer actually need the context file that the JDBCRealm defining the realm information that previous test used (but it does no harm if you leave it where it is). The SecurityFilter will now be handling the security configuration for you. web.xml should now look something like:

web.xml
<?xml version="1.0" encoding="ISO-8859-1" ?>

<!DOCTYPE web-app PUBLIC
  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <display-name>My secure webapp</display-name>
  <filter>
     <filter-name>Security Filter</filter-name>
     <filter-class>org.securityfilter.filter.SecurityFilter</filter-class>
     <init-param>
        <param-name>config</param-name>
        <param-value>/WEB-INF/securityfilter-config.xml</param-value>
        <description>Configuration file location (this is the default value)</description>
     </init-param>
     <init-param>
        <param-name>validate</param-name>
        <param-value>false</param-value>
        <description>Validate config file if set to true</description>
     </init-param>
     <init-param>
        <param-name>formPattern</param-name>
        <param-value>/logMeIn</param-value>
        <description>
           As an example a login form can define "logMeIn" as it action in place of the standard
           "j_security_check" which is a special flag user by app servers for container managed security.
        </description>
     </init-param>
  </filter>

  <!-- map all requests to the SecurityFilter -->

  <filter-mapping>
     <filter-name>Security Filter</filter-name>
     <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>

The SecurityFilter contains its configuration in a file called securityfilter-config.xml. Note that the realm section of this file is not the same format as that used in the webapp context file (shown above it in comments above for comparison in the example below).

securityfilter-config.xml
<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE securityfilter-config PUBLIC
   "-//SecurityFilter.org//DTD Security Filter Configuration 2.0//EN"
   "http://www.securityfilter.org/dtd/securityfilter-config_2_0.dtd">

<securityfilter-config>

 <security-constraint>
     <web-resource-collection>
        <web-resource-name>Secure Pages</web-resource-name>
        <url-pattern>/*</url-pattern>
     </web-resource-collection>
     <auth-constraint>
        <role-name>admin</role-name>
     </auth-constraint>
  </security-constraint>

  <login-config>
     <auth-method>FORM</auth-method>
     <form-login-config>
        <form-login-page>/login.html</form-login-page>
        <form-error-page>/autherr.html</form-error-page>
        <form-default-page>/index.html</form-default-page>
     </form-login-config>
  </login-config>

  <!--
  <Realm className="org.apache.catalina.realm.JDBCRealm" debug="0"
         driverName="oracle.jdbc.driver.OracleDriver"
         connectionURL="jdbc:oracle:thin:@<hostname>:<port>:<sid>"
         connectionName="dbUsername"
         connectionPassword="dbPassword"
         userTable="tomcat_user_role" 
         userNameCol="user_name" 
         userCredCol="user_name"
         userRoleTable="tomcat_user_role" 
         roleNameCol="role_name" />
  -->
  <!-- start with a Catalina realm adapter to wrap the Catalina realm defined below -->

  <realm className="org.securityfilter.realm.catalina.CatalinaRealmAdapter" />

  <realm className="org.apache.catalina.realm.JDBCRealm">
          <realm-param name="driverName" value="oracle.jdbc.driver.OracleDriver"/>
          <realm-param name="debug" value="0"/>
          <realm-param name="connectionURL" value="jdbc:oracle:thin:@<hostname>:<port>:<sid>"/>
          <realm-param name="connectionName" value="dbUsername"/>
          <realm-param name="connectionPassword" value="dbPassword"/>
          <realm-param name="userTable" value="tomcat_user_role"/>
          <realm-param name="userNameCol" value="user_name"/>
          <realm-param name="userCredCol" value="user_name"/>
          <realm-param name="userRoleTable" value="tomcat_user_role"/>
          <realm-param name="roleNameCol" value="role_name"/>
  </realm>

</securityfilter-config>

Note on Tomcat 4, I had noticed that I was getting "java.lang.NoClassDefFoundError: org/apache/catalina/LifecycleException" with the SecurityFilter due to that fact that my version did not contain the LifecycleException class. This is easily fixed by adding the Tomcat 5 catalina.jar (included with the SecurityFilter downloads) file to WEB-INF/lib (ugly but it works), alternatively you can always obtain the SecurityFilter source remove the lifecyle references from org.securityfilter.realm.catalina.CatalinaRealmAdapter and recompile the SecurityFilter.

Now restart Tomcat again, reload your browser and try to access index.html. You should get kicked out to login.html and login (username=password). Once you're sure that works as planned then you have successfully configured SecurityFilter to work with Tomcat JDBCRealm and you can now proceed to the next step where we install the CASFilter component.

3. Install and configure CASFilter and make the necessary modifications to SecurityFilter to use the CAS authentication

Quick summary:
  • Install CASFilter and modify web.xml
  • Add my modified SecurityFilter and new CASAuthenticator class to SecurityFilter source and recompile
  • Configure securityfilter-config to use CAS
  • Test

Obtain and install the necessary casclient.jar and associated files into WEB-INF/lib that CASFilter needs to work. Modify the web.xml to include the CASFilter settings. So it now looks something like the file below (obviously your CAS parameters will differ from those shown):

web.xml
<?xml version="1.0" encoding="ISO-8859-1" ?>

<!DOCTYPE web-app PUBLIC
  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

  <display-name>SecurityFilter Blank Application</display-name>

  <filter>
     <filter-name>CAS Filter</filter-name>

<filter-class>edu.yale.its.tp.cas.client.filter.CASFilter2</filter-class>
     <init-param>
        <param-name>edu.yale.its.tp.cas.client.filter.loginUrl</param-name>
        <param-value>https://secure.its.yale.edu/cas/login</param-value>
     </init-param>
     <init-param>

<param-name>edu.yale.its.tp.cas.client.filter.validateUrl</param-name>

<param-value>https://secure.its.yale.edu/cas/serviceValidate</param-value>
     </init-param>
     <init-param>

<param-name>edu.yale.its.tp.cas.client.filter.serverName</param-name>
        <param-value>your server name and port (e.g., www.yale.edu:8080)</param-value>
     </init-param>
  </filter>

  <filter>
     <filter-name>Security Filter</filter-name>
     <filter-class>org.securityfilter.filter.SecurityFilter</filter-class>
     <init-param>
        <param-name>config</param-name>
        <param-value>/WEB-INF/securityfilter-config.xml</param-value>
        <description>Configuration file location (this is the default value)</description>
     </init-param>
     <init-param>
        <param-name>validate</param-name>
        <param-value>false</param-value>
        <description>Validate config file if set to true</description>
     </init-param>
     <init-param>
        <param-name>formPattern</param-name>
        <param-value>/logMeIn</param-value>
        <description>
           As an example a login form can define "logMeIn" as it action in place of the standard
           "j_security_check" which is a special flag user by app servers for container managed security.
        </description>
     </init-param>
  </filter>

  <filter-mapping>
     <filter-name>CAS Filter</filter-name>
     <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- map all requests to the SecurityFilter, control what it does with configuration settings -->

  <filter-mapping>
     <filter-name>Security Filter</filter-name>
     <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>

The order of the filter-mappings in the web.xml is important. The CASFilter must occur before the SecurityFilter so that the SecurityFilter can pick up the necessary user identity from the session.

I had to make a small modification to SecurityFilter's org.securityfilter.config.SecurityConfig.AuthenticatorFactory in order for it to support CAS authentication. I have also added a new CASAuthenticator class to support CAS as an authentication method. See attached files at the bottom of this wiki page (for sanitys sake I've used the same package name as SecurityFilter uses).

You could just replace the securtityfilter.jar with the modified version of the file at the bottom of this page. To compile it yourself you'll need Ant and a java compiler, obtain the SecurityFilter source code. You need to recompile the default SecurityFilter to include the changes necessary for it to support CAS. Unzip the source code and copy AuthenticatorFactory.java and CASAuthenticator.java (attached see bottom of this page) into src directory in the appropriate place for the org.securityfilter.authenticator package.

Recompile SecurityFilter and install the modified securityfilter.jar file in your WEB-INF/lib directory.

You now need to modify the SecurityFilter securityfilter-config.xml file to indicate that you want it to now use CAS authentication:

securityfilter-config.xml
<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE securityfilter-config PUBLIC
   "-//SecurityFilter.org//DTD Security Filter Configuration 2.0//EN"
   "http://www.securityfilter.org/dtd/securityfilter-config_2_0.dtd">

<securityfilter-config>

 <security-constraint>
     <web-resource-collection>
        <web-resource-name>Secure Pages</web-resource-name>
        <url-pattern>/*</url-pattern>
     </web-resource-collection>
     <auth-constraint>
        <role-name>admin</role-name>
     </auth-constraint>
  </security-constraint>

  <login-config>
     <auth-method>CAS</auth-method>
     <form-login-config>
        <form-default-page>/index.jsp</form-default-page>
     </form-login-config>
  </login-config>

  <!--
  <Realm className="org.apache.catalina.realm.JDBCRealm" debug="0"
         driverName="oracle.jdbc.driver.OracleDriver"
         connectionURL="jdbc:oracle:thin:@<hostname>:<port>:<sid>"
         connectionName="dbUsername"
         connectionPassword="dbPassword"
         userTable="tomcat_user_role" userNameCol="user_name" userCredCol="user_name"
         userRoleTable="tomcat_user_role" roleNameCol="role_name" />
  -->
  <!-- start with a Catalina realm adapter to wrap the Catalina realm defined below -->
  <realm className="org.securityfilter.realm.catalina.CatalinaRealmAdapter" />

  <realm className="org.apache.catalina.realm.JDBCRealm">
          <realm-param name="driverName" value="oracle.jdbc.driver.OracleDriver"/>
          <realm-param name="debug" value="0"/>
          <realm-param name="connectionURL" value="jdbc:oracle:thin:@<hostname>:<port>:<sid>"/>
          <realm-param name="connectionName" value="dbUsername"/>
          <realm-param name="connectionPassword" value="dbPassword"/>
          <realm-param name="userTable" value="tomcat_user_role"/>
          <realm-param name="userNameCol" value="user_name"/>
          <realm-param name="userCredCol" value="user_name"/>
          <realm-param name="userRoleTable" value="tomcat_user_role"/>
          <realm-param name="roleNameCol" value="role_name"/>
  </realm>
</securityfilter-config>

All being well that should be it. When you next restart your web application you should be prompted to login via CAS and authorisation should be obtained from the Tomcat realm source. You can of course tune the configurations so that some files/directories require that you are only authenticated and other demand that you are additionally authorised.

Handy Hint

Incidentally, if you are using JSTL inside your JSPs in the web application you can obtain the "getRemoteUser()" using:

<c:out value="${pageContext.request.remoteUser}"/>

Alternatively you could make use of the Jakarta Request taglib.


Further sources of information:

I was greatly inspired by documentation I found in other places, so if some of this looks very familiar to you then it probably isn't a coincidence! Notable sources of further documentation include:

http://securityfilter.sourceforge.net/
http://cymulacrum.net/writings/secfil/t1.html
http://jasigch.princeton.edu:9000/display/CAS/Using+CASFilter
http://jakarta.apache.org/tomcat/tomcat-4.1-doc/realm-howto.html