com.logiclander.jaasmine.authentication.http.JaasLoginFilter.java Source code

Java tutorial

Introduction

Here is the source code for com.logiclander.jaasmine.authentication.http.JaasLoginFilter.java

Source

/*
 * Copyright 2010 LogicLander
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.logiclander.jaasmine.authentication.http;

import java.io.IOException;

import javax.security.auth.Subject;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.logiclander.jaasmine.authentication.AuthenticationService;
import com.logiclander.jaasmine.authentication.SimpleAuthenticationService;

/**
 * Checks incoming ServletRequests and ServletResponses for authentication.
 *
 * This filter accepts the following init-params:
 * <UL>
 *  <LI>appName - the name of the application in the JAAS configuration.  This
 * parameter is optional.</LI>
 *  <LI>loginPath - if set, dispatch to this path in the web application.  This
 * can be a JSP, Servlet or HTML page.</LI>
 *  <LI>loginRedirect - if set, redirect to this URL for login processing and/or
 * credential gathering.  The value can be a relative or absolute URL.  If the
 * redirect is relative (that is, inside the web application), a servlet or
 * JSP must be mapped to the URL listed here in the web.xml file.</LI>
 *  <LI>loginServletName - the name of the Servlet that will be used to
 * collect user credentials.  This parameter is optional.</LI>
 *  <LI>setRemoteUserOnLogin - when "true", calls to
 * {@link javax.servlet.http.HttpServletRequest#getRemoteUser() getRemoteUser}
 * will return the user name that was used by the user to log in.</LI>
 * </UL>
 *
 * Requests that invoke this Filter must have parameters named {@code username}
 * and {@code password} set, otherwise the request cannot be processed.  This
 * Filter processes logins using the {@link SimpleAuthenticationService}.
 *
 * Instances of this class have a configurable commons-logging based logger
 * named
 * {@code com.logiclander.jaasmine.authentication.http.JaasLoginFilter}.
 */
public class JaasLoginFilter implements Filter {

    /** The logger for this instance. */
    private transient final Log logger = LogFactory.getLog(JaasLoginFilter.class);

    /** The default value for the appName, which is "JaasLoginServlet".*/
    private static final String DEFAULT_NAMED_LOGIN_DISPATCHER = "JaasLoginServlet";

    /** The default value for setRemoteUserOnLogin, which is "false".*/
    private static final String DEFAULT_SET_REMOTE_USER_ON_LOGIN = "false";

    /** Empty String flag used to stop some processing of login handling. */
    private static final String EMPTY_STRING = "";

    /**
     * The application name for the configuration to use in the JAAS file.
     */
    private String appName;

    private String filterName;

    /**
     * Dispatch to this path in the web context if set.  If this is set, the
     * loginRedirect and loginServletName are ignored.
     */
    private String loginPath;

    /**
     * Redirect to this URL if set.  This could be in or out of the web
     * application.  If this is set, the loginServletName is ignored.
     */
    private String loginRedirect;

    /**
     * The name of the Servlet to use for post login processing.
     */
    private String loginServletName;

    /**
     * Flag indicating whether or not to set the REMOTE_USER on a successful
     * login.
     */
    private boolean setRemoteUserOnLogin;

    private boolean isUsingBasicAuthentication;

    /**
     * {@inheritDoc}
     *
     * Checks the given FilterConfig for the init-params named appName and
     * loginServletName.  If these values are not in the FilterConfig, then
     * the default values are used.
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

        appName = filterConfig.getInitParameter("appName");
        if (appName == null || appName.isEmpty()) {
            appName = AuthenticationService.DEFAULT_JAASMINE_LOGIN_CONFIG;
        }

        loginPath = filterConfig.getInitParameter("loginPath");
        if (loginPath == null || loginPath.isEmpty()) {
            loginPath = EMPTY_STRING;
        }

        loginRedirect = filterConfig.getInitParameter("loginRedirect");
        if (loginRedirect == null || loginRedirect.isEmpty()) {
            loginRedirect = EMPTY_STRING;
        }

        loginServletName = filterConfig.getInitParameter("loginServletName");
        if (loginServletName == null || loginServletName.isEmpty()) {
            loginServletName = DEFAULT_NAMED_LOGIN_DISPATCHER;
        }

        String setRemoteUserOnLoginParam = filterConfig.getInitParameter("setRemoteUserOnLogin");
        if (setRemoteUserOnLoginParam == null || setRemoteUserOnLoginParam.isEmpty()) {
            setRemoteUserOnLoginParam = DEFAULT_SET_REMOTE_USER_ON_LOGIN;
        }

        setRemoteUserOnLogin = Boolean.parseBoolean(setRemoteUserOnLoginParam);

        filterName = filterConfig.getFilterName();

        isUsingBasicAuthentication = Boolean.valueOf(filterConfig.getInitParameter("setBasicAuth"));

        if (logger.isDebugEnabled()) {
            logger.debug(String.format("%s initialized", toString()));
            logger.debug(String.format("loginPath = %s", loginPath == EMPTY_STRING ? "Not set" : loginPath));
            logger.debug(
                    String.format("loginRedirect = %s", loginRedirect == EMPTY_STRING ? "Not set" : loginRedirect));
            logger.debug(String.format("loginServletName = %s", loginServletName));
            logger.debug(String.format("setRemoteUserOnLogin = %s", Boolean.toString(setRemoteUserOnLogin)));
        }
    }

    /**
     * This implementation will filter requests for credentials and determine if
     * processing of the FilterChain can proceed.  Filtering occurs as follows:
     * <OL>
     *  <LI>If the request is not an HttpServletRequest and the response is not
     * an HttpServletResponse, continue processing the filter chain (this almost
     * never happens)</LI>
     *  <LI>The HttpSession is checked for an attribute named
     * {@link AuthenticationService#SUBJECT_KEY
     * AuthenticationService.SUBJECT_KEY}</LI>
     *  <LI>If found, then processing the filter chain continues.</LI>
     *  <LI>If not found, then the request is checked for the {@code username}
     * and {@code password} parameters.  If these parameters are present, then
     * the SimpleAuthenticationService's login method is invoked with those
     * credentials.</LI>
     *  <LI>If a Subject is returned, it is saved to the HttpSession with the
     * key from above.</LI>
     * </OL>
     * When the login is successful, the ServletRequest is wrapped in a
     * {@link JaasmineHttpServletRequest}.  If it is unsuccessful, this filter
     * will send the request to a login processor as follows:
     * <OL>
     *  <LI>If {@code loginPath} is set, dispatch the request to that resource.
     * </LI>
     *  <LI>If (@code loginRedirect} is set, redirect the request to that URL.
     * </LI>
     *  <LI>Dispatch to {@code loginServletName} if neither of the above are
     * set./LI>
     * </OL>
     *
     * @param request the ServletRequest
     * @param response the ServletResponse
     * @param chain the FilterChain
     * @throws IOException if an I/O error occurs in the FilterChain
     * @throws ServletException if a processing error occurs in the FilterChain
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug(String.format("%s: entering doFilter", filterName));
        }

        if (!(request instanceof HttpServletRequest) && !(response instanceof HttpServletResponse)) {

            logger.debug("This is not an HTTP request");
            chain.doFilter(request, response);

        } else {

            HttpServletRequest httpReq = (HttpServletRequest) request;
            HttpServletResponse httpResp = (HttpServletResponse) response;

            Exception exception = null;

            if (logger.isDebugEnabled()) {
                logger.debug(String.format("Filtering request: %s%s", httpReq.getContextPath(),
                        httpReq.getServletPath()));
            }

            try {

                if (!hasRequestUri(httpReq)) {
                    cacheRequestUri(httpReq);
                }

                boolean canExecute = hasCredentials(httpReq);

                // Attempt to login the user and obtain a Subject.

                if (!canExecute) {
                    canExecute = login(httpReq);
                }

                if (canExecute) {

                    // The Subject was found which means the user has a valid
                    // credential (Subject).  Processing can continue.

                    // TODO: always wrap request, set to cached requestURI
                    HttpServletRequest sendOn = httpReq;

                    if (setRemoteUserOnLogin) {
                        sendOn = new JaasmineHttpServletRequest(httpReq, getSubject(httpReq));
                        logger.debug(String.format("Wrapping request in %s", sendOn.toString()));

                    }

                    chain.doFilter(sendOn, httpResp);

                } else {

                    // No Subject found, need to dispatch to someplace to gather
                    // the user's credentials and attempt a login on the
                    // next request.

                    RequestDispatcher loginDispatcher = null;
                    if (!loginPath.equals(EMPTY_STRING)) {

                        loginDispatcher = httpReq.getSession().getServletContext().getRequestDispatcher(loginPath);

                        if (logger.isDebugEnabled()) {
                            logger.debug(String.format("Dispatching login " + "request to path %s", loginPath));
                        }

                    } else if (!loginRedirect.equals(EMPTY_STRING)) {

                        if (logger.isDebugEnabled()) {
                            logger.debug(String.format("Redirectiong login " + "request to %s", loginRedirect));
                        }
                        httpResp.sendRedirect(loginRedirect);

                        // TODO: cache incoming requestURI

                        return;

                    } else if (isUsingBasicAuthentication) {

                        String s = "Basic realm=\"Jaasmine\"";
                        httpResp.setHeader("WWW-Authenticate", s);
                        httpResp.setStatus(401);

                        return;
                    } else {

                        loginDispatcher = httpReq.getSession().getServletContext()
                                .getNamedDispatcher(loginServletName);

                        if (logger.isDebugEnabled()) {
                            logger.debug(String.format("Dispatching login " + "request to named dispatcher %s",
                                    loginServletName));
                        }
                    }

                    if (loginDispatcher != null) {

                        loginDispatcher.forward(httpReq, httpResp);
                        return;

                    } else {

                        // Try to figure out what went wrong and send back
                        // a HELPFUL exception message.

                        String msg = "";

                        if (!loginPath.equals(EMPTY_STRING)) {

                            // First, is there a loginPath set, but nowhere to
                            // send it to?

                            msg = String.format(
                                    "loginPath set to %s, but no " + "resource is available to dispatch to",
                                    loginPath);
                        } else {

                            // Is JaasLoginServlet (or the servlet-name
                            // specified by loginServletName) not configured in
                            // the web.xml?

                            msg = String.format("Servlet named %s specified by "
                                    + "the loginServletName is not configured in " + "the web.xml",
                                    loginServletName);
                        }

                        throw new ServletException(msg);

                    }
                }

            } catch (IOException ex) {

                exception = ex;
                throw (ex);

            } catch (ServletException ex) {

                exception = ex;
                throw (ex);

            } finally {

                if (exception != null) {

                    if (logger.isErrorEnabled()) {
                        String msg = String.format("Caught exception in filter chain: %s", exception.getMessage());
                        logger.error(msg, exception);
                    }
                }

            }
        }
    }

    /**
     * Writes a log message using the configured logger at DEBUG level stating
     * that the Filter is destroyed.
     */
    @Override
    public void destroy() {

        if (logger.isDebugEnabled()) {
            logger.debug(String.format("%s destroyed", toString()));
        }

    }

    /**
     * @return the String representation of this JaasLoginFilter.
     */
    @Override
    public String toString() {
        return String.format("%s for %s", filterName, appName);
    }

    /**
     * @param req an HttpServletRequest
     * @return true if the session has been established AND the request URI is cached.
     */
    private boolean hasRequestUri(HttpServletRequest req) {

        HttpSession sess = req.getSession(false);
        if (sess == null) {
            return false;
        }

        String requestURI = (String) sess.getAttribute(AuthenticationService.REQUEST_URI_KEY);

        return (requestURI != null);
    }

    private void cacheRequestUri(HttpServletRequest req) {

        HttpSession sess = req.getSession();

        sess.setAttribute(AuthenticationService.REQUEST_URI_KEY, req.getRequestURI());

        if (req.getQueryString() != null) {
            sess.setAttribute(AuthenticationService.REQUST_QUERY_KEY, req.getQueryString());
        }
    }

    /**
     * @param req an HttpServletRequest
     * @return true if the credentials are present on the request.
     */
    private boolean hasCredentials(HttpServletRequest req) {

        return hasSubject(req);

    }

    /**
     * @param req an HttpServletRequest
     * @return true if the Subject is found on the request.
     */
    private boolean hasSubject(HttpServletRequest req) {

        HttpSession sess = req.getSession(false);
        if (sess == null) {
            return false;
        }

        Subject subj = (Subject) sess.getAttribute(AuthenticationService.SUBJECT_KEY);

        return (subj != null);
    }

    /**
     * @param request the HttpServletRequest.
     * @return true if the Subject is obtained from the SimpleLoginService.
     */
    private boolean login(HttpServletRequest request) {

        String username = request.getParameter("username");
        String password = request.getParameter("password");
        boolean subjectObtained = false;

        if (username == null && password == null) {

            String basic = request.getHeader("Authorization");

            if (basic != null) {

                String[] hTokens = basic.split(" ");

                String decoded = new String(Base64.decodeBase64(hTokens[1]));

                String[] aTokens = decoded.split(":");
                username = aTokens[0];

                // Allow for the possibility of a machine-based login w/out a
                // password
                if (aTokens.length > 1) {
                    password = aTokens[1];
                } else {
                    password = "";
                }
            }
        }

        if (username == null || username.isEmpty()) {

            logger.debug("username is missing");
            return subjectObtained;

        }

        //        if (password == null || password.isEmpty()) {
        //
        //            logger.debug("password is missing");
        //            return subjectObtained;
        //        }

        AuthenticationService as = new SimpleAuthenticationService(appName);
        Subject s = as.login(username, password.toCharArray());
        subjectObtained = (s != null);

        if (subjectObtained) {

            // Assuming that if we got here, we need to create the session
            HttpSession sess = request.getSession();
            sess.setAttribute(AuthenticationService.SUBJECT_KEY, s);
        }

        return subjectObtained;
    }

    /**
     * @param request the HttpServletRequest
     * @return the Subject obtained from a successful login.
     */
    private Subject getSubject(HttpServletRequest request) {

        HttpSession s = request.getSession();
        return (Subject) s.getAttribute(AuthenticationService.SUBJECT_KEY);
    }
}