Introduction
This article is about securing your Tomcat or JBoss web pages, granting access using an Active Domain Controller, without the need of administrator credentials configured for LDAP.
It is basically about authenticating users for open source web applications not yet prepared for user authorization (or having a legacy authorization system which could be disabled), to be hosted on an Apache Tomcat or a JBoss web application server, while also validating some other several already made applications, all against LDAP service.
Background
There is a ton of information around, describing how to configure LDAP authentication on Linux operating system, it can be also found great documentation about configuring Apache to map users against LDAP and even installing OpenLDAP server, but all those solutions, so far, requires administrator rights from LDAP side, included on the web server configuration, which is not always available nor desirable, plus takes a lot of time to understand and deploy.
It was pretty difficult to find a good and simple guide to create something that adds security to Tomcat/JBoss url path's and also including the validation against LDAP. It was clear that an AuthorizationFilter mapping using servlet api shall be written, including the use of some javax.naming classes to build a DirContext using LdapCtxFactory.getLdapCtxInstance(), having no exception in the process will lead to username/password combination to give access to the given URL path for the user being authenticaticated.
After figuring out the solution and having the code up and running, it was decided to share the experience of writing an authorization filter and also adding some logic in the middle, especially the validation against an Active Domain directory service, including a login page alltogether. Hope this article can guide the coder on writing a filter mapping using java servlet and also, give you a shortcut on development and deployement tasks.
Solution
The code was written as a servlet filter, in which access is forbidden to any page but the login page, as users shall have a public point to hit prior to be authenticated. Clearly the coder can configure as much filters as it is required, even extending to other filters or pages to show error for authentication process.
Once added the filter-mapping in the web.xml, the initialization code on the filter reads the parameters from the web.xml as well, thus, having a pretty flexible product, in which we can utilize a given user for fixed system validation (always same user system-wide) or define a dynamic authorization including a web login page, so users can be authenticated and granted access depending on credentials inserted on that login web page.
Using the code
We will start describing how a filter works - in a very short wording - and how to configure the mapping to hit the correct java code, as that is the first step on our project.
Basically a filter is a detour on the request->response action of the normal http cycle and can point to a code written on some predefined class or a tailor made one, which is executed everytime a user request a resource page on that given url path or any path under the url mapping of the defined filter. So instead of simple go down the route and return the page, the web application server, executes this class which in turn return a success or cancellation result. There are many types of filters for the same path and thus, you'll have it chained one after another, depending on your <url-pattern>/*</url-pattern> definition, so if your filter successfully grants the acces, you'll simply give the green light to continue over the filter chain and if something fails, it is just cutted the process returning an http error. You can get plenty of information on Apache Tomcat filters on the following pages.
Now that we have clear that deviation concept, we will go thorugh the web.xml configuration and how to point to the correct source code to actually implement the filter and finally implement the corresponding authentication routines to return the filter process as a success or deny result. Afterwards, we will modify the given code to include a login page to allow users to authenticate dynamically, which was our original interest.
Configuration
The trick here is to use some helpers avoiding manually adding libraries and configuration from scratch (like Eclipse and Maven - although you can add everything manually if you're such an expert coder which is out of the scope of this article). So, we will simply start a Dynamic Web Project to later convert to a Maven one or create a Maven Project from scratch using maven-archetype-webapp or any other archetype of your preference (some parts may be slightly different in the latter case).
You'll have a structure similar to the following one on your Eclipse, in where src will have the classes for the authorization filter java code and WebContent will host your pages and configuration that will be finally deployed under your webapp folder, shown in our project under Deployed Resources. It is highly recommended that you also add an Apache Tomcat server to test locally, so you can easily debug and learn how the cookie crumbles.
The web.xml file is where the magic starts to happen, having already added the servlet-api jar to your pom.xml it is only required to configure the authorization filter which it is about to be named AuthorizeFilter, securing your application root folder and subfolders, due to the url-pattern definition for '/*' and finally, the filter-mapping should know which class is holding the implementation of the filter, we will create that code in org/web/servlets/filters/Authorize.java file.
<!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>MyAuthorizeProject</display-name>
<filter>
<filter-name>AuthorizeFilter</filter-name>
<filter-class>org.web.servlets.filters.Authorize</filter-class>
<!-- init-param are going to be here -->
</filter>
<filter-mapping>
<filter-name>AuthorizeFilter</filter-name>
<url-pattern>
Filter Source
Now we will go directly to the coding stuff on the already named org/web/servlets/filters/Authorize.java file. First of all, we will require a class implementing javax.servlet.Filter, which will force us to define and implements the following methods: init()
, doFilter()
and destroy()
. The initialization method will help us read the parameters from web.xml init parameters section marked above, as the init()
method is called while Apache Tomcat server starts, thus holding that configuration whilst the web application is up and running. The destroy()
method is obviously called to free any resource requested or reserved during initialization or during the web application life cycle. Finally, the doFilter()
method is called everytime the application server hits a query on the url path defined, in our case example, always! It may look obvious, but don't confuse our application root folder with the main RootDirectory of your web server, which are different things, although you can make it both the same with a bit of work (configuring httpd.conf or simply adding an index page using an http-equiv url refresh to our given web application root folder example). So here it goes the skeleton for the filter example in java source code.
package org.web.servlets.filters;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
public class Authorize implements Filter {
boolean authenticationGranted = false;
public void init(FilterConfig filterConfiguration) throws ServletException {
if (filterConfiguration.getInitParameter("grantedAccess").toLowerCase().equals("true"))
authenticationGranted = true;
System.out.println("Authorize filter started having grant '" +
authenticationGranted ? "always" : "never" + "'.");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain servletFilterChain) throws IOException, ServletException {
System.out.println("Performing authorization...");
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpSession session = request.getSession();
if (authenticationGranted) {
System.out.println("Authorization granted.");
servletFilterChain.doFilter(servletRequest, servletResponse);
} else {
System.out.println("Authorization denied!");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
public void destroy() {
System.out.println("Authorize filter destroyed.");
}
}
This java code assumes that you will have defined a grantedAccess parameter in the corresponding configuration init section of the filter, as we are about to see on the next xml code. It is read as a string, case insensitive and transformed to a boolean value to always grant access or never, so far we are just having a concept to later evolve to dynamic authentication.
<init-param>
<param-name>grantedAccess</param-name>
<param-value>true</param-value>
</init-param>
Now you're ready to start playing with the code, just change value for grantedAccess parameter and do check how your filter code grant or denies access depending on your initial configuration. Once that code is working, you can step into the user authentication, but won't be too difficult, just extra parameters for ldap server and username/password credential and the right routines for ldap authentication.
LDAP user authentication
Now that we already know how to configure, write and run a java filter, we will add the real code for authentication against an Active Domain using a standard java class, in this case we are going to use com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance()
to check username and password combination against an ldap server, although this could change in the future or even evolve to some other factory class; the important part here, is where to add the grant code and what to give as a result to the filter chain. The following code will go directly into the doFilter()
method of the java filter implementation, so the code can decide if the user is known by the LDAP or not, thus, granting or denying access to the requested resource respectively. It is extremely important that coder double check that JDK is set in the project properties within Build Java Path, as com.sun.jndi.ldap.LdapCtxFactory
is going to be found only for jdk.
String ldapContext = String.format("ldap://%s", ldapHostname);
Hashtable<String, String> ldapUserProperties = new Hashtable<String, String>();
ldapUserProperties.put(Context.SECURITY_PRINCIPAL, ldapUsername);
ldapUserProperties.put(Context.SECURITY_CREDENTIALS, ldapPassword);
try {
DirContext directoryContext = LdapCtxFactory.getLdapCtxInstance(ldapContext, ldapUserProperties);
authenticationGranted = true;
} catch (NamingException e) {
System.out.println("Authentication failed!");
}
Clearly this code will require the variables ldapServer, ldapUsername and ldapPassword to be defined somehow as init-param's and also read by the init()
method on the filter java code, while also defined within the class. Some import's are going to be required as well, but just ommited by now to only present the important parts of the solution.
At this point, if everything went well, we are having a complete filter project example that takes username and password combination from web.xml and grants or denies depending on the configured user. So far, we are having pretty much the same as all the examples around, but we've learned how to write a filter from scratch - huh?
Anyway, we are not just have the code working, we also know how to read configuration variables and initialize our class, so we will go further and teach our filter, how to decide whether to grant access to a login page, get the username and password combination from that login page and authenticate agains our LDAP service that user dynamically... after that, will then be cooking on gas!
Adding a login page
In this section, we are going to see how to catch the url the user is navigating to, as we need to allow unauthorized user to always hit our login page, in which we will ask for username and password combination and authenticate the user against the LDAP, after that process, we will give access to the system or just return an authorization error; see also that we shall remember the original url the user was navigating, as if by any chance our user is having a full path to hit, we will have to redirect to the login page and after sucessful authentication (in our project is also an authorization as well), we will have to redirect back the user to the original url.
The following code goes inside our Authorize java class and decides whether to redirect to login page or not; depending on method utilized (GET or POST), it will redirect back if authorized to the original url on POST. There are a couple of extra variables just to have an already validated user in our session and the login html page we will use. Whithin the login page, we shall include a form and also have to take care on the parameter names within that form, as shall maintain the same names we will extract from the request on the POST. We are going to refactor the last code into a method called performLdapAuthentication()
to have a clear coding.
String authorizedUsername = (String) session.getAttribute("authorizedUsername");
if (authorizedUsername != null) {
System.out.println("User '" + authorizedUsername + "' was previously authorized!");
authenticationGranted = true;
} else {
if (request.getServletPath().equals(loginUrlPath)) {
if (request.getMethod().equals("GET")) {
authenticationGranted = true;
} else if (request.getMethod().equals("POST")) {
String username = request.getParameter("_login_username");
String password = request.getParameter("_login_password");
if (performLdapAuthentication(username, password)) {
session.setAttribute("authorizedUsername", ldapUsername);
authenticationGranted = true;
System.out.println("User '" + authorizedUsername + "' is now authorized!");
} else {
redirectUri = String.format("%s%s", request.getContextPath(), loginUrlPath);
session.setAttribute("_login_authentication_error",
"Authentication error, username or password invalid!");
}
}
} else {
if (loginUrlPath != null) {
redirectUri = String.format("%s%s", request.getContextPath(), loginUrlPath);
session.setAttribute("_login_original_request_url", request.getRequestURL());
} else {
if (performLdapAuthentication(ldapUsername, ldapPassword)) {
authorizedUsername = ldapUsername;
session.setAttribute("authorizedUsername", ldapUsername);
authenticationGranted = true;
System.out.println("User '" + authorizedUsername + "' is now authorized!");
}
}
}
}
if (authenticationGranted) {
servletFilterChain.doFilter(servletRequest, servletResponse);
} else {
if (redirectUri == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} else {
response.sendRedirect(redirectUri);
}
}
For all this to work, we have to also define the url resource for login, which in turn will present to the user an html page to insert username and password but also will decide whether or not it will redirect back to the original url. This is created on a new class that implements HttpServlet
, which defines two methods: doGet()
and doPost()
, for the GET action and POST action respectively. So once the user hits the login url, the Authorize filter gives permission to get the login resource, which at the time navigates to our login.html page by the action of the doGet()
method, that login page will request username and password combination and will POST back to our servlet which will hit again our Authorize java class, the authentication will be performed against LDAP and after result - either granting or denying - the Authorize code will allow the filter to continue its chain or just return an HTTP error. In the last case, we will navigate again to the login url with an error to show, in the former case, the navigation down the chain will put us again in the Login class, but whithin the doPost()
method insted, which will redirect back to the original url saved.
Here it goes the login.java class implementing the two methods for GET and POST and the required definition for login resource, using an annotation for that. It is extremly important that you set this same path on the configuration for login-url-path, or the redirection to login won't work, actually the parameter on the web.xml is not there to define the login path, but to just activate it, was set as a string and not a boolean just to have this explained.
package org.web.servlets;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet("/login")
public class Login extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.getRequestDispatcher("/WEB-INF/login.jsp").forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession();
String originalRequestUrl = session.getAttribute("_login_original_request_url").toString();
if (originalRequestUrl != null && originalRequestUrl.length() > 0) {
session.removeAttribute("_login_original_request_url");
response.sendRedirect(originalRequestUrl);
}
}
}
This is all about the servlet filter, the login resource and how to work with the login page, once you got all this understood, you will be able to create any other filter, including authentication and authorization, which it was bounded together in this project, although it could be easily extended to provides user mapping for authorization on different path's and action's.
Points of Interest
Understanding how a Filter works was not a difficult task, the learning curve is pretty fast and straigthforward, although it is important to realize how powerful is this feature on the web application servers.
Interworking a Tomcat or JBoss over Linux with an Active Domain Directory over Windows © it wasn't a simple work, the 389 port forwarding was proven as enough, although the information available is normally including a lot of parameters to query the ldap and our project shall remain simple, so finding out the minimal effort to only authenticate a user required a bit of time.
As a final note, our project will have to include the same authentication mechanism but from Windows .NET over C# - although that will probably be for another writing in the future.
History
The first version of the product provides system wide and login page for authentication.
It is planned the evolution for mapping parameters from LDAP and authorizing on the Web Server.