Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava
Creative Commons License

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?

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.

<?xml version='1.0' encoding='utf-8'?>
<Context>
  <Valve className="waffle.apache.MixedAuthenticator" principalFormat="fqn" roleFormat="both" />
  <Realm className="waffle.apache.WindowsRealm" />
</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.

<security-role>
  <role-name>Everyone</role-name>
</security-role>

Restrict access to website resources.

<security-constraint>
  <display-name>Waffle Security Constraint</display-name>
  <web-resource-collection>
    <web-resource-name>Protected Area</web-resource-name>
    <url-pattern>/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>Everyone</role-name>
  </auth-constraint>
</security-constraint>

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

<security-constraint>
  <display-name>Login Page</display-name>
  <web-resource-collection>
    <web-resource-name>Unprotected Login Page</web-resource-name>
    <url-pattern>/login.jsp</url-pattern>
  </web-resource-collection>
</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.

<login-config>
   <form-login-config>
      <form-login-page>/login.jsp</form-login-page>
      <form-error-page>/error.html</form-error-page>
   </form-login-config>
</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.

<form method="POST" name="loginform" action="index.jsp?j_security_check">
    <table style="vertical-align: middle;">
        <tr>
            <td>Username:</td>
            <td><input type="text" name="j_username" /></td>
        </tr>
        <tr>
            <td>Password:</td>
            <td><input type="password" name="j_password" /></td>
        </tr>
        <tr>
            <td><input type="submit" value="Login" /></td>
        </tr>
    </table>
    </form>
    <hr>
    <form method="POST" name="loginform" action="index.jsp?j_negotiate_check">
    <input type="submit" value="Login w/ Current Windows Credentials" />
</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 https://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.

@Override
protected boolean authenticate(Request request, Response response, LoginConfig loginConfig) {

    String queryString = request.getQueryString();
    boolean negotiateCheck = (queryString != null && queryString.equals("j_negotiate_check"));
    boolean securityCheck = (queryString != null && queryString.equals("j_security_check"));

    Principal principal = request.getUserPrincipal();

    AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);
    boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();

    if (principal != null && ! ntlmPost) {
        return true;
    } else if (negotiateCheck) {
        if (! authorizationHeader.isNull()) {
            return negotiate(request, response, authorizationHeader);
        } else {
            sendUnauthorized(response);
            return false;
        }
    } else if (securityCheck) {
        boolean postResult = post(request, response, loginConfig);
        if (postResult) {
            redirectTo(request, response, request.getServletPath());
        } else {
            redirectTo(request, response, loginConfig.getErrorPage());
        }
        return postResult;
    } else {
        redirectTo(request, response, loginConfig.getLoginPage());
        return false;
    }
}

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

private boolean post(Request request, Response response, LoginConfig loginConfig) {
    String username = request.getParameter("j_username");
    String password = request.getParameter("j_password");
    IWindowsIdentity windowsIdentity = null;
    try {
        windowsIdentity = _auth.logonUser(username, password);
    } catch (Exception e) {
        return false;
    }

    WindowsPrincipal windowsPrincipal = new WindowsPrincipal(windowsIdentity, context.getRealm(), _principalFormat, _roleFormat);
    register(request, response, windowsPrincipal, "FORM", windowsPrincipal.getName(), null);
    return true;
}