Tomcat Single Sign-On mixed with Form Authentication w/ Waffle

Back | waffle, jna, security, java, active directory | 5/27/2010 |

waffle

Most Tomcat users begin by implement Form-based authentication. Those deploying applications into enterprises soon discover that those enterprises use an Active Directory and have single sign-on on all intranet sites. They eventually find Waffle, but don’t want to take the ability to do form-based logon away.

How do we give users a way to logon either way?

image

You can accomplish this with the Waffle MixedAuthenticator.

Configure Tomcat

Download and Copy Files

Download Waffle 1.3 and copy waffle-jna.jar, jna.jar and platform.jar to Tomcat's lib directory.

Configure Mixed Authenticator Valve

Add a valve and a realm to the application context. For an application, modify META-INF\context.xml.

  1. <?xml version='1.0' encoding='utf-8'?>
  2. <Context>
  3.   <Valve className="waffle.apache.MixedAuthenticator" principalFormat="fqn" roleFormat="both" />
  4.   <Realm className="waffle.apache.WindowsRealm" />
  5. </Context>

Security Roles and Constraints

Configure security roles in WEB-INF\web.xml. The Waffle Mixed Authenticator adds all user's security groups (including nested and domain groups) as roles during authentication.

  1. <security-role>
  2.   <role-name>Everyone</role-name>
  3. </security-role>

Restrict access to website resources.

  1. <security-constraint>
  2.     <display-name>Waffle Security Constraint</display-name>
  3.     <web-resource-collection>
  4.       <web-resource-name>Protected Area</web-resource-name>
  5.       <url-pattern>/*</url-pattern>
  6.     </web-resource-collection>
  7.     <auth-constraint>
  8.       <role-name>Everyone</role-name>
  9.     </auth-constraint>
  10.   </security-constraint>

Add a second security constraint that leaves the login page unprotected.

  1. <security-constraint>
  2.     <display-name>Login Page</display-name>
  3.     <web-resource-collection>
  4.       <web-resource-name>Unprotected Login Page</web-resource-name>
  5.       <url-pattern>/login.jsp</url-pattern>
  6.     </web-resource-collection>
  7.   </security-constraint>

Configure Form Login

Configure Form Login parameters with the location of the login page (repeated from the security constraint above) and an error page for failed logins. Modify WEB-INF\web.xml.

  1. <login-config>
  2.    <form-login-config>
  3.       <form-login-page>/login.jsp</form-login-page>  
  4.       <form-error-page>/error.html</form-error-page>  
  5.    </form-login-config>
  6. </login-config>

Login Page

Create a login page based on the following code. There're two requirements for the login form. The form-based authentication must post to any valid location with the j_security_check parameter. The destination page will be loaded after a successful login. The single sign-on form must similarly post to any valid location with the j_negotiate_check parameter in the query string.

Here’s a rudimentary example that lands an authenticated user on index.jsp.

  1. <form method="POST" name="loginform" action="index.jsp?j_security_check">
  2.     <table style="vertical-align: middle;">
  3.         <tr>
  4.             <td>Username:</td>
  5.             <td><input type="text" name="j_username" /></td>
  6.         </tr>
  7.         <tr>
  8.             <td>Password:</td>
  9.             <td><input type="password" name="j_password" /></td>
  10.         </tr>
  11.         <tr>
  12.             <td><input type="submit" value="Login" /></td>
  13.         </tr>
  14.     </table>
  15.     </form>
  16.     <hr>
  17.     <form method="POST" name="loginform" action="index.jsp?j_negotiate_check">
  18.     <input type="submit" value="Login w/ Current Windows Credentials" />
  19.     </form>

Demo

A demo application can be found in the Waffle distribution in the Samples\Tomcat\waffle-mixed directory. Copy the entire directory into Tomcat's webapps directory and navigate to http://localhost:8080/waffle-mixed/. Pick your method of login!

How does it Work?

Implementation details follow. Read at your own risk.

From the unauthenticated login page we are making two possible requests: one will trigger Single Sign-On and another will trigger form-based authentication. To do single sign-on we will need access to the request/response objects and to do forms authentication we will need access to the realms interface. The place where we have both is in org.apache.catalina.Authenticator.

  1. @Override
  2. protected boolean authenticate(Request request, Response response, LoginConfig loginConfig) {
  3.  
  4.     String queryString = request.getQueryString();
  5.     boolean negotiateCheck = (queryString != null && queryString.equals("j_negotiate_check"));
  6.     boolean securityCheck = (queryString != null && queryString.equals("j_security_check"));
  7.  
  8.     Principal principal = request.getUserPrincipal();
  9.     
  10.     AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);        
  11.     boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
  12.  
  13.     if (principal != null && ! ntlmPost) {
  14.         return true;
  15.     } else if (negotiateCheck) {
  16.         if (! authorizationHeader.isNull()) {
  17.             return negotiate(request, response, authorizationHeader);
  18.         } else {
  19.             sendUnauthorized(response);
  20.             return false;
  21.         }
  22.     } else if (securityCheck) {
  23.         boolean postResult = post(request, response, loginConfig);
  24.         if (postResult) {
  25.             redirectTo(request, response, request.getServletPath());
  26.         } else {
  27.             redirectTo(request, response, loginConfig.getErrorPage());
  28.         }
  29.         return postResult;
  30.     } else {
  31.         redirectTo(request, response, loginConfig.getLoginPage());
  32.         return false;
  33.     }
  34. }

Negotiate mimics the behavior of NegotiateAuthenticator, while form post follows the standard Authenticator registration process.

  1. private boolean post(Request request, Response response, LoginConfig loginConfig) {        
  2.     String username = request.getParameter("j_username");
  3.     String password = request.getParameter("j_password");
  4.     IWindowsIdentity windowsIdentity = null;
  5.     try {
  6.         windowsIdentity = _auth.logonUser(username, password);
  7.     } catch (Exception e) {
  8.         return false;
  9.     }
  10.     
  11.     WindowsPrincipal windowsPrincipal = new WindowsPrincipal(windowsIdentity, context.getRealm(), _principalFormat, _roleFormat);
  12.     register(request, response, windowsPrincipal, "FORM", windowsPrincipal.getName(), null);
  13.     return true;
  14. }

Links

How can we use this for other application servers?



For example :weblogic
Irfan @ Thursday, 23 September 2010 Reply
Please ask the questions on the waffle website.
dB. @ Thursday, 23 September 2010 Reply