Creating a simple JASPIC auth module

One of the problems I have with several web frameworks or even tools that I have used such as Sonar and Jenkins is that they deemed it necessary to develop their own authentication system or use frameworks such as Apache Shiro, Acegi/Spring Security and the ilk as they put authentication responsibility in the hands of the application rather than the container.

Granted the typical container managed authentication is very limited to whatever the container vendor provided or having proprietary extensions such as WebSphere’s Trust Association Interceptors (TAI). Since Java EE 6, a standard way of building this has been created called Java Authentication Service Provider Interface for Containers (JASPIC). This blog post talks about creating a simple JASPIC ServerAuthModule that will use HTTP Headers to contain the authentication data much like SiteMinder does.

Some reverse proxy servers when used with client authentication such as site minder or in my intranet I have Apache using Client Side SSL certificates which pass the certificate user’s e-mail address as the user name in an HTTP Header. This allows me to pass the username to applications such as Jenkins (Reverse Proxy Auth Plugin, Redmine (HTTP Authentication Plugin) and Sonar (Reverse Proxy Authenticator).

Since I did create my own plugin that would do it for one product, namely the Reverse Proxy Authenticator plugin for Sonar I decided I should do one for Glassfish using a hardcoded header that can easily be placed as an option but kept off to keep things simple.   Without further adieu the code where I added comments to explain how JASPIC works:

public class HttpHeaderAuthModule implements ServerAuthModule {

    /**
     * Supported message types. For our case we only need to deal with HTTP
     * servlet request and responses. On Java EE 7 this will handle WebSockets
     * as well.
     */
    private static final Class[] SUPPORTED_MESSAGE_TYPES = new Class[] {
        HttpServletRequest.class, HttpServletResponse.class };

    /**
     * Callback handler that is passed in initialize by the container. This
     * processes the callbacks which are objects that populate the "subject".
     */
    private CallbackHandler handler;

    /**
     * Does nothing of note for what we need.
     */
    @Override
    public void cleanSubject(final MessageInfo messageInfo,
            final Subject subject) throws AuthException {
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Class[] getSupportedMessageTypes() {
        return SUPPORTED_MESSAGE_TYPES;
    }

    /**
     * Initializes the module.  Allows you to pass in options.
     * @param requestPolicy
     *            request policy, ignored
     * @param responsePolicy
     *            response policy, ignored
     * @param h
     *            callback handler
     * @param options
     *            options
     */
    @Override
    public void initialize(final MessagePolicy requestPolicy,
            MessagePolicy responsePolicy, CallbackHandler h, Map options)
                    throws AuthException {
        handler = h;
    }

    /**
     * @return AuthStatus.SEND_SUCCESS
     */
    @Override
    public AuthStatus secureResponse(final MessageInfo paramMessageInfo,
            final Subject subject) throws AuthException {
        return AuthStatus.SEND_SUCCESS;
    }

    /**
     * Validation occurs here.
     */
    @Override
    public AuthStatus validateRequest(final MessageInfo messageInfo,
            final Subject client, final Subject serviceSubject)
                    throws AuthException {

        // Take the request from the messageInfo structure.
        final HttpServletRequest req = (HttpServletRequest) messageInfo
                .getRequestMessage();
        try {
            // Get the user name from the header.  If not there then fail authentication.
            final String userName = req.getHeader("X-Forwarded-User");
            if (userName == null) {
                return AuthStatus.FAILURE;
            }

            // Store the user name that was in the header and also set a group.
            handler.handle(new Callback[] {
                    new CallerPrincipalCallback(client, userName),
                    new GroupPrincipalCallback(client, new String[] { "users" }) });
            return AuthStatus.SUCCESS;
        } catch (final Exception e) {
            throw new AuthException(e.getMessage());
        }
    }

}

That’s about it code wise. Now to deploy it is container specific, in Glassfish’s case we place it in the lib folder and refer to it in the Message Security section of the server-config as another provider (I may detail the steps for this in a future blog post).

NOTE: In terms of making it work with the application, one thing to watch out for is the groups returned by the module is not mapped to the application roles by default. Unfortunately the mapping at the moment is container specific in Glassfish’s case I create a WEB-INF/glassfish-web.xml to do the mapping as follows:



        
                users
                users
        

Other application servers would have their own descriptor or can be configured during deployment like in WebSphere.

Hopefully with this, we can avoid making the applications more complicated by pushing the authentication outside of the application and giving it to the container. It will make a better user experience as well as you can have single sign-on (not just keeping the passwords in sync with LDAP).

A github project with this and OAuth 2.0 Login with OpenID Connect Discovery (used by Google) will be coming when I make the time to push it up. However, feel free to use the code above.