Recently, while rolling out a single sign-on solution for a client, I have stumbled across an interesting problem.
Here’s the situation. We had a bunch of web applications that needed to be deployed onto Apache Tomcat (v6.0.29) server. Each application needed to be secured using FORM based authentication. We were using Atlassian Crowd identity management application for managing our users and providing the authentication and authorisation services.
The integration of the Tomcat server with Crowd was straight forward. Using Tomcat’s JAASRealm implementation and Crowd’s third party CrowdLoginModule JAAS module, we managed to successfully get our users authenticated using BASIC authentication method.
Enabling single sign-on from within Tomcat was also very easy. We used Tomcat’s SingleSignOn valve, which uses JSESSIONIDSSO cookie to keep track of the user session across multiple applications.
Once we got everything working using BASIC authentication, we decided to change to FORM based authentication using our corporate login page. This should have been easy but turned out to be tricky.
As you undoubtedly aware, this is how you would normally configure a web application’s login page inside the web.xml file:
Unfortunately, the login page you declare in this section always represents an application context relative path (as per the J2EE
servlet specification). So, for example, if you access your application using this URL:
the container will look for the login page declared above, here:
What this really boils down to is if you have multiple applications that all require user login, each one has to package its own copy of the login page inside the web archive. Clearly, this solution is less than ideal. The login page should really be deployed as a separate resource somewhere in the common location and accessed by all applications that need it. Any changes that may be required to it, can then be done in one place and automatically propagated to all applications that use it as soon as the login page is redeployed.
Declaring absolute paths within the form-login-page element is not possible, so how do we get around this problem? The way that I solved this problem is as follows.
Login Form Servlet
I am sure by now you have realised that to load a login form from outside of the application context, we need to use either a Filter or a Servlet. For the purpose of this example, I will use a servlet.
The login form servlet is responsibly for returning a login form located in a common location on your server. Here’s how this servlet can be implemented:
Now, we need to wire this servlet into the web.xml so that login form requests go to it. Here’s how to do it:
Now, whenever the container needs to authenticate a user, it will redirect the request to
/sso/login which will invoke
the LoginFormServlet that will subsequently load the
file:///path/to/login.html file and stream it to the user’s
browser. Provided that the HTML contains
<form action="j_security_check"> instruction, the user name and
password values will be sent to Tomcat’s authentication provider and the user will be properly authenticated with the right
JSESSIONID cookie created.
The above solution works but each application deployed to Tomcat is still required to define the servlet, its mapping and of course the login config inside the web.xml file. This is not a problem but I did wonder if there was a better way. And sure enough there is.
Tomcat’s conf directory contains various configuration files that dictate the behaviour of the server and each application that it is executing. One of the configuration files is web.xml deployment descriptor. When a web application is deployed onto Tomcat, this file is processed first followed by the WEB-INF/web.xml file defined inside the application’s war file. It is the combination of instructions found in both files that constitute the operational context of each application.
Hence, to centralise the authentication of users for all applications within a single instance of a Tomcat server, all I had to do was to move the definition of the servlet, the servlet mapping and the login config into the Tomcat’s global web.xml file.
I took this a step further and defined the security contraints within the global web.xml as well to ensure that even if the application’s developers did not include any constraints, the access to the application will continue to be enforced using the global definition. Here it is:
Of course as soon as I did this, I realised that some of our HTTP accessible back-end applications (e.g. web services, Spring HTTP invoker proxies, etc) that do not require authentication may break and sure enough they did.
To fix this problem, I took advantage of the way Tomcat loads and treats the global and application local web.xml files. By adding a security constraint to the application’s web.xml file, I could effectively override the “all” constraint defined in the global web.xml file, thus allowing unauthenticated access to these applications (I did briefly consider moving these applications to another Tomcat server which does not require authentication of all its applications but in the end I decided that it was not worth the hassle).
For example, here is an application local constraint that allows unauthenticated access to
/services. This constraint,
which is defined inside the application’s WEB-INF/web.xml file effectively overrides the above catch-all declaration.
And that’s it. Using this approach I managed to successfully implement a single sign-on solution for all of my client’s applications without having to duplicate the login form code in each web archive.