org.openanzo.servlet.EncryptedTokenAuthenticator.java Source code

Java tutorial

Introduction

Here is the source code for org.openanzo.servlet.EncryptedTokenAuthenticator.java

Source

/*******************************************************************************
 * Copyright (c) 2008-2010 Cambridge Semantics Incorporated.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Created by:  Jordi Albornoz Mulligan ( <a href="mailto:jordi@cambridgesemantics.com">jordi@cambridgesemantics.com </a>)
 * 
 * Contributors:
 *     Mort Bay Consulting Pty. Ltd. - the Jetty FormAuthenticator is the basis of the Cambridge Semantics EncryptedTokenAuthenticator.
 *     Cambridge Semantics Incorporated - modified to use encrypted token method to avoid using session state.
 *     
 *  This code is based on Jetty's org.eclipse.jetty.security.FormAuthenticator class
 *  as modified by Cambridge Semantics Incorporated to implement the encrypted
 *  token authentication as described at:
 *  http://www.openanzo.org/projects/openanzo/wiki/AnzoJsSessionKeyAuthenticationDesign
 *  The original copyright statement from the FormAuthenticator is reproduced below.
 *  The FormAuthenticator's authors were listed in the source as:
 *  Greg Wilkins (gregw) and dan@greening.name
    
 *  ========================================================================
 *  Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
 *  ------------------------------------------------------------------------
 *  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 org.openanzo.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.time.DateUtils;
import org.eclipse.jetty.http.HttpHeaders;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.openanzo.exceptions.AnzoException;
import org.openanzo.exceptions.AnzoRuntimeException;
import org.openanzo.exceptions.ExceptionConstants;
import org.openanzo.exceptions.LogUtils;
import org.openanzo.rdf.utils.SerializationConstants;
import org.openanzo.security.keystore.ISecretKeystore;
import org.openanzo.services.AnzoPrincipal;
import org.openanzo.services.EncryptedTokenAuthenticatorConstants;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Encrypted Token Authentication Authenticator. This authenticator implements and authentication scheme similar to the typical J2EE Form-based authentication
 * except that rather than depending on the session to record authentication credentials, it encrypts the credentials inside a cookie.
 * 
 * The authentication scheme is described at: http://www.openanzo.org/projects/openanzo/wiki/AnzoJsSessionKeyAuthenticationDesign
 * 
 * @author Jordi Albornoz Mulligan (<a href="mailto:jordi@cambridgesemantics.com">jordi@cambridgesemantics.com</a>)
 */
public class EncryptedTokenAuthenticator extends BasicContext implements EncryptedTokenAuthenticatorConstants {

    private static final Logger log = LoggerFactory.getLogger(EncryptedTokenAuthenticator.class);

    private static final long serialVersionUID = 1L;

    private final static String AUTH_METHOD = "ANZO_ENCRYPTED_TOKEN";

    final static String TOKEN_DELIMITER = ";";

    /**
     * Name of a request attribute that will be set if the authentication cookie should be refreshed. That is, authentication was valid and the refresh window
     * has elapsed. The authenticator will set the cookie in this request attribute instead of directly adding the refreshed cookie to the response whenever the
     * 'customTokenRefresh' config property is true. The value of the request attribute will be the actual {@link Cookie} object containing the refreshed
     * cookie.
     * 
     * This is mainly useful for situations where the authenticator is being asked to skip refreshing the cookie. For example, if the servlets want certain
     * requests to not count as valid activity toward resetting the timeout. One useful scenario for that is cometd, which polls the server every 30 seconds or
     * so. We'd like to timeout the user even in light of a bunch of empty poll responses.
     */
    public final static String ANZO_REFRESH_COOKIE_ATTRIBUTE = "org.openanzo.standaloneEncryptedTokenAuthenticator.refreshCookie";

    private String _formErrorPage;

    private String _formErrorPath;

    private String _formLoginPage;

    private String _formLoginPath;

    private ISecretKeystore secretKeyEncoder;

    private long tokenTimeout = -1;

    private long tokenRefreshWindow = -1;

    /**
     * @see #ANZO_REFRESH_COOKIE_ATTRIBUTE
     */
    private Boolean customTokenRefresh;

    private ServerRealm realm;

    private Collection<PathSpec> protectedPathSpec = new ArrayList<PathSpec>();

    /**
     * 
     * create a new EncryptedTokenAuthenticator
     * 
     * @param bundleContext
     *            bundle context
     * @param securityConstraint
     *            securityConstraint
     * @param realm
     *            security realm for authentication
     * @param encoder
     *            keystore
     * @param docRoot
     *            docroot for servlet
     * @param pathSpec
     *            unprotected paths for servlet
     * @param protectedPathSpec
     *            the protected paths for servlet
     */
    public EncryptedTokenAuthenticator(BundleContext bundleContext, SecurityConstraint securityConstraint,
            ServerRealm realm, ISecretKeystore encoder, String docRoot, Collection<PathSpec> pathSpec,
            Collection<PathSpec> protectedPathSpec) {
        this(bundleContext, securityConstraint, realm, encoder, docRoot, pathSpec, protectedPathSpec, false);
    }

    /**
     * 
     * create a new EncryptedTokenAuthenticator
     * 
     * @param bundleContext
     *            bundle context
     * @param securityConstraint
     *            securityConstraint
     * @param realm
     *            security realm for authentication
     * @param encoder
     *            keystore
     * @param docRoot
     *            docroot for servlet
     * @param pathSpec
     *            unprotected paths for servlet
     * @param protectedPathSpec
     *            the protected paths for servlet
     * @param retrieveDir
     *            return directory resources
     */
    public EncryptedTokenAuthenticator(BundleContext bundleContext, SecurityConstraint securityConstraint,
            ServerRealm realm, ISecretKeystore encoder, String docRoot, Collection<PathSpec> pathSpec,
            Collection<PathSpec> protectedPathSpec, boolean retrieveDir) {
        super(bundleContext, securityConstraint, docRoot, retrieveDir);
        if (encoder == null) {
            throw new AnzoRuntimeException(ExceptionConstants.CORE.NULL_PARAMETER, "encoder");
        }
        this.secretKeyEncoder = encoder;
        this.realm = realm;
        //this.pathSpec = pathSpec;
        this.protectedPathSpec = protectedPathSpec;
    }

    @Override
    public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (super.handleSecurity(request, response)) {
            return authenticate(request, response);
        } else {
            return false;
        }
    }

    /**
     * Set the login page
     * 
     * @param path
     *            path to login page
     */
    public void setLoginPage(String path) {
        if (path == null || path.trim().length() == 0) {
            _formLoginPage = "/authentication/login.html";
            _formLoginPath = "/authentication/login.html";
        } else {
            if (!path.startsWith("/")) {
                log.warn(LogUtils.LIFECYCLE_MARKER, "EncryptedTokenAuthenticator login page must start with /");
                path = "/" + path;
            }
            _formLoginPage = path;
            _formLoginPath = path;
            if (_formLoginPath.indexOf('?') > 0)
                _formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?'));
        }
    }

    /**
     * Get the login page
     * 
     * @return the login page
     */
    public String getLoginPage() {
        return _formLoginPage;
    }

    /**
     * Set the login error page
     * 
     * @param path
     *            the login error page
     */
    public void setErrorPage(String path) {
        if (path == null || path.trim().length() == 0) {
            _formErrorPath = "/authentication/error.html";
            _formErrorPage = "/authentication/error.html";
        } else {
            if (!path.startsWith("/")) {
                log.warn(LogUtils.LIFECYCLE_MARKER, "EncryptedTokenAuthenticator error page must start with /");
                path = "/" + path;
            }
            _formErrorPage = path;
            _formErrorPath = path;

            if (_formErrorPath != null && _formErrorPath.indexOf('?') > 0)
                _formErrorPath = _formErrorPath.substring(0, _formErrorPath.indexOf('?'));
        }
    }

    /**
     * Get the login error page
     * 
     * @return the login error page
     */
    public String getErrorPage() {
        return _formErrorPage;
    }

    /**
     * Perform form authentication. Called from SecurityHandler.
     * 
     * @param servletRequest
     *            request to authenticate
     * @param response
     *            response to send response data
     * 
     * @return UserPrincipal if authenticated else null.
     * @throws IOException
     */
    @SuppressWarnings("null")
    public boolean authenticate(HttpServletRequest servletRequest, HttpServletResponse response)
            throws IOException {
        // NOTE: Jetty will sometimes call this method with a null response parameter. In particular, from
        // the org.eclipse.jetty.Request#getUserPrincipal() method.
        boolean ret = false;
        Request request = Request.getRequest(servletRequest);
        String uri = request.getServletPath();
        boolean protectedPath = false;
        for (PathSpec spec : protectedPathSpec) {
            if (spec.matches(uri)) {
                protectedPath = true;
                break;
            }
        }
        // Setup some defaults. Unfortunately, this is the only place we really get to set these up since the
        // Authenticator interface doesn't have an 'init'-like method.
        if (tokenTimeout <= 0) {
            tokenTimeout = 30 * DateUtils.MILLIS_PER_MINUTE;
        }
        if (tokenRefreshWindow <= 0) {
            tokenRefreshWindow = 5 * DateUtils.MILLIS_PER_MINUTE;
        }
        if (customTokenRefresh == null) {
            customTokenRefresh = Boolean.FALSE;
        }

        // Now handle the request
        uri = request.getPathInfo();
        if (uri.endsWith(LOGIN_URI_SUFFIX)) {
            // Handle a request for authentication.

            ret = false; // We will entirely handle the request here so return false to stop processing the request.

            String username = request.getParameter(USERNAME_PARAMETER_NAME);
            String password = request.getParameter(PASSWORD_PARAMETER_NAME);

            if (username == null || password == null) {
                log.error(LogUtils.SECURITY_MARKER,
                        "Invalid anzo_authenticate request url:{} username:{} password:{}",
                        new Object[] { uri, username, password == null ? null : "XXXObscuredNonNullPasswordXXX" });
            }

            AnzoPrincipal userPrincipal = null;
            try {
                userPrincipal = realm.authenticate(username, password, request);
            } catch (Exception e) {
                // No matter what sort of failure occurs in the realm we want to make sure to send the  appropriate
                // error response or redirect. So we can't all exceptions here.
                log.debug(LogUtils.SECURITY_MARKER, "Failed authentication call to the realm.", e);
            }
            if (userPrincipal == null) {
                if (log.isDebugEnabled()) {
                    log.debug(LogUtils.SECURITY_MARKER, "Authentication request FAILED for {}",
                            StringUtil.printable(username));
                }
                request.setAuthentication(null);

                if (response != null) {
                    if (_formErrorPage == null || isRequestSentByXmlHttpRequest(request)) {
                        if (log.isDebugEnabled()) {
                            log.debug(LogUtils.SECURITY_MARKER,
                                    "Sending 403 Forbidden error due to invalid credentials for user {}", username);
                        }
                        response.sendError(HttpServletResponse.SC_FORBIDDEN);
                    } else {
                        String redirectPath = response
                                .encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _formErrorPage));
                        log.debug(LogUtils.SECURITY_MARKER, "Sending redirect to form error page {}", redirectPath);
                        response.setContentLength(0);
                        response.sendRedirect(redirectPath);
                    }
                }
            } else {
                // Authenticated OK
                if (log.isDebugEnabled()) {
                    log.debug(LogUtils.SECURITY_MARKER, "Authentication request OK for {}",
                            StringUtil.printable(username));
                }
                request.setAuthentication(new BasicUserAuthorization(userPrincipal, AUTH_METHOD));

                // Set the encrypted token
                if (response != null) {
                    try {
                        String token = createEncryptedToken(username, request.getRemoteAddr());
                        Cookie tokenCookie = new Cookie(ANZO_TOKEN_COOKIE_NAME, token);
                        tokenCookie.setPath("/");
                        response.addCookie(tokenCookie);
                        if (isRequestSentByXmlHttpRequest(request)) {
                            // XMLHttpRequests just want a response with the cookie, no fancy redirects or anything like that.
                            // just send back 200 in text.(Need to send back something else firefox reports an error)
                            response.setStatus(HttpServletResponse.SC_OK);
                            response.setContentType("text/plain");
                            PrintWriter out = response.getWriter();
                            out.print(HttpServletResponse.SC_OK);
                            out.flush();
                            out.close();
                        } else {
                            // Redirect to the URL to user wanted to get to initially, or "/" if there isn't any such URL.
                            // We get the URL from a query parameter in the HTTP Referer (sic) header.
                            String referer = request.getHeader(HttpHeaders.REFERER);
                            String redirectPath = null;
                            if (referer != null) {
                                HttpURI refererUri = new HttpURI(referer);
                                MultiMap<String> queryParams = new MultiMap<String>();
                                refererUri.decodeQueryTo(queryParams, null);
                                String desiredUrl = (String) queryParams.getValue(ANZO_URL_QUERY_PARAM, 0);
                                if (desiredUrl != null) {
                                    redirectPath = desiredUrl;
                                }
                            }
                            if (redirectPath == null) {
                                redirectPath = URIUtil.addPaths(request.getContextPath(), "/");
                            }
                            redirectPath = response.encodeRedirectURL(redirectPath);
                            log.debug(LogUtils.SECURITY_MARKER,
                                    "Sending redirect to root {} after successful login request.", redirectPath);
                            response.sendRedirect(redirectPath);
                        }

                    } catch (AnzoException cause) {
                        IOException ex = new IOException("Error creating encrypted authentication token.");
                        ex.initCause(cause);
                        throw ex;
                    }
                }
            }

        } else if (isLoginOrErrorPage(uri)) {
            // Don't authenticate authform or errorpage. Just let the system them out.
            ret = true;
        } else if (protectedPath) {
            // This is a regular request for a protected resource, so check whether there is a valid
            // encrypted token in the request.
            AnzoPrincipal userPrincipal = null;

            // Parse and validate the authentication token from the cookie
            Token token = null;
            long currentTime = System.currentTimeMillis();
            Cookie[] cookies = request.getCookies();
            Cookie tokenCookie = null;
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    String cookieName = cookie.getName();
                    if (ANZO_TOKEN_COOKIE_NAME.equals(cookieName)) {
                        tokenCookie = cookie;
                        try {
                            token = parseAnzoToken(cookie.getValue());
                            userPrincipal = validateAuthToken(token, realm, request.getRemoteAddr(), currentTime);
                        } catch (AnzoException e) {
                            log.debug(LogUtils.SECURITY_MARKER,
                                    "Error decrypting and parsing authentication token.", e);
                        }
                        break;
                    }
                }
            }

            if (userPrincipal == null) {
                // Invalid, expired, or non-existent token
                ret = false; // Don't serve the resource

                if (log.isDebugEnabled()) {
                    String msg = "Auth token ";
                    if (tokenCookie == null) {
                        msg += "MISSING";
                    } else {
                        msg += "INVALID";
                    }
                    log.debug(LogUtils.SECURITY_MARKER, msg + " for URL: {}",
                            StringUtil.printable(request.getRequestURI()));
                }
                if (response != null) {
                    Cookie cookie = new Cookie(ANZO_TOKEN_COOKIE_NAME, "");
                    cookie.setPath("/");
                    cookie.setMaxAge(0);
                    response.addCookie(cookie); // adding a cookie with MaxAge=0 tells the client to delete the cookie.
                    if (_formLoginPage == null || isRequestSentByXmlHttpRequest(request)) {
                        if (log.isDebugEnabled()) {
                            log.debug(LogUtils.SECURITY_MARKER,
                                    "Sending 403 Forbidden error due to invalid auth token. Token: {}", token);
                        }
                        response.sendError(HttpServletResponse.SC_FORBIDDEN);
                    } else {
                        // We save the URL the user tried to access into a query parameter in the redirect to the login page.
                        // That way the login page can send the user to the page they wanted after they finish logging in.
                        // First we must reconstruct the URL the user accessed.
                        String requestUrl = uri;
                        if (request.getQueryString() != null) {
                            requestUrl += "?" + request.getQueryString();
                        }
                        requestUrl = request.getScheme() + "://" + request.getServerName() + ":"
                                + request.getServerPort() + URIUtil.addPaths(request.getContextPath(), requestUrl);
                        // Now we add the requested URL as a query parameter to the login URL
                        MultiMap<String> loginPageUrlQueryParams = new MultiMap<String>();
                        loginPageUrlQueryParams.put(ANZO_URL_QUERY_PARAM, requestUrl);
                        String loginPageUrl = URIUtil.addPaths(request.getContextPath(), _formLoginPage);
                        try {
                            loginPageUrl = addQueryParametersToURI(loginPageUrl, loginPageUrlQueryParams);
                        } catch (URISyntaxException e) {
                            log.warn(LogUtils.SECURITY_MARKER,
                                    "Error creating login redirect URL. The user's attempted URL won't be saved for use after login.",
                                    e);
                        }
                        String redirectPath = response.encodeRedirectURL(loginPageUrl);
                        log.debug(LogUtils.SECURITY_MARKER,
                                "Sending redirect to form login page {} after request without adequate credentials.",
                                redirectPath);
                        response.setContentLength(0);
                        response.sendRedirect(redirectPath);
                    }
                }
            } else {
                // Properly authenticated
                if (log.isDebugEnabled()) {
                    log.debug(LogUtils.SECURITY_MARKER, "Auth token OK for '{}' for URL:{}",
                            StringUtil.printable(userPrincipal.getName()),
                            StringUtil.printable(request.getRequestURI()));
                }
                if (userPrincipal instanceof AnzoPrincipal) {
                    request.setAttribute(SerializationConstants.authenticationURI, (userPrincipal).getUserURI());
                }
                request.setAttribute(SerializationConstants.userPrincipal, userPrincipal);
                request.setAuthentication(new BasicUserAuthorization(userPrincipal, AUTH_METHOD));
                ret = true;

                // Check if the token is older than the refresh window. If so, create a new cookie with an updated timestamp.
                try {
                    if (currentTime < token.getTimestamp()
                            || (currentTime - token.getTimestamp() >= tokenRefreshWindow)) { // if current time is less than the token's time, we'll issue a new cookie. That should only ever happen upon overflow of the number of milliseconds from the epoch.
                        String cookieval = createEncryptedToken(token.getUsername(), token.getRemoteAddress());
                        Cookie newTokenCookie = new Cookie(ANZO_TOKEN_COOKIE_NAME, cookieval);
                        newTokenCookie.setPath("/");
                        if (customTokenRefresh) {
                            request.setAttribute(ANZO_REFRESH_COOKIE_ATTRIBUTE, newTokenCookie);
                        } else {
                            response.addCookie(newTokenCookie);
                        }
                    }
                } catch (AnzoException e) {
                    log.error(LogUtils.SECURITY_MARKER,
                            "Could NOT update timestamp on authentication token. Authentication session may end prematurely.",
                            e);
                }
            }
        } else {
            // This is NOT a protected resource so just let it be served.
            ret = true;
        }

        log.debug(LogUtils.SECURITY_MARKER, "Returning from 'authenticate' with {} for path {}", ret, uri);
        return ret;
    }

    /**
     * Checks if the given token is a valid authentication token. If so, it returns the Principal for the user which the token authenticates.
     * 
     * @param token
     *            the parsed token to validate
     * @param realm
     *            used to obtain the principal using the token username
     * @param requestRemoteAddress
     *            the IP address of the client which made supplied the token. It will be compared to the address embedded in the token
     * @param currentTime
     *            the current time in milliseconds to compare to the token's timestamp for checking if the token is expired.
     * @return the authenticated principal or null if the token is invalid.
     */
    AnzoPrincipal validateAuthToken(Token token, ServerRealm realm, String requestRemoteAddress, long currentTime) {
        AnzoPrincipal userPrincipal = null;
        // Validate that the IP address for which the token was created is the same as the one in the current request.
        if (token != null && token.getRemoteAddress().equals(requestRemoteAddress)) {
            // Validate that the token isn't older than the timeout period
            if (currentTime >= token.getTimestamp() && (currentTime - token.getTimestamp() < tokenTimeout)) {
                // The token is valid. Let's lookup the user information as a final check.
                userPrincipal = realm.getPrincipal(token.getUsername());
            } else {
                log.debug(LogUtils.SECURITY_MARKER,
                        "Auth token timestamp is expired: - tokenTimestamp:{} currentTime:{} difference:{} timeout:",
                        new Object[] { token.getTimestamp(), currentTime, currentTime - token.getTimestamp(),
                                tokenTimeout });
            }
        } else {
            log.debug(LogUtils.SECURITY_MARKER,
                    "Auth token remote address does not match - tokenAddress:{} requestAddress:{}",
                    (token != null) ? token.getRemoteAddress() : null, requestRemoteAddress);
        }
        return userPrincipal;
    }

    /**
     * Creates an encrypted token by combining the username, remote IP address, and the current timestamp (in milliseconds from the epoch) and then encrypting
     * the combined string. The string is of the format: "timestamp;remoteAddress;username". For example: <code>1208983023421;127.0.0.1;cooluser</code>
     * 
     * @param username
     *            the username of the authenticated user
     * @param remoteAddr
     *            the remote IP address from which the authentication request came.
     * @return the base64 representation of the encrypted token string.
     */
    String createEncryptedToken(String username, String remoteAddr) throws AnzoException {
        String timestamp = Long.toString(System.currentTimeMillis());
        StringBuilder str = new StringBuilder(username.length() + remoteAddr.length() + timestamp.length() + 2); // 2 for the colons
        str.append(timestamp);
        str.append(TOKEN_DELIMITER);
        str.append(remoteAddr);
        str.append(TOKEN_DELIMITER);
        str.append(username);

        String plaintoken = str.toString();
        String cyphertoken = this.secretKeyEncoder.encryptAndBase64EncodeString(plaintoken);
        return cyphertoken;
    }

    /**
     * Decrypts and parses the given authentication token.
     * 
     * @param token
     * @return null if the string couldn't be parsed properly.
     * @throws AnzoException
     */
    Token parseAnzoToken(String token) throws AnzoException {
        Token ret = null;
        String plaintoken = this.secretKeyEncoder.decryptAndBase64DecodeString(token);
        int firstDelimiter = plaintoken.indexOf(TOKEN_DELIMITER);
        if (firstDelimiter > 0) {
            String timestr = plaintoken.substring(0, firstDelimiter);
            try {
                long timestamp = Long.parseLong(timestr);
                int secondDelimiter = plaintoken.indexOf(TOKEN_DELIMITER, firstDelimiter + 1);
                if (secondDelimiter > firstDelimiter + 1) {
                    String remoteAddr = plaintoken.substring(firstDelimiter + 1, secondDelimiter);
                    if (secondDelimiter < plaintoken.length() - 1) {
                        String username = plaintoken.substring(secondDelimiter + 1);
                        ret = new Token(username, timestamp, remoteAddr);
                    }
                }
            } catch (NumberFormatException e) {
                log.trace(LogUtils.SECURITY_MARKER, "Invalid timestamp in authentication token.");
            }
        }
        return ret;
    }

    private boolean isLoginOrErrorPage(String pathInContext) {
        return pathInContext != null && ((_formErrorPath != null && pathInContext.endsWith(_formErrorPath))
                || (_formLoginPath != null && pathInContext.endsWith(_formLoginPath)));
    }

    private boolean isRequestSentByXmlHttpRequest(HttpServletRequest request) {
        String val = request.getHeader("X-Requested-With");
        return "XMLHttpRequest".equals(val);
    }

    static class Token {
        private String username;

        private long timestamp;

        private String remoteAddress;

        public Token(String username, long timestamp, String remoteAddress) {
            this.username = username;
            this.timestamp = timestamp;
            this.remoteAddress = remoteAddress;
        }

        public String getUsername() {
            return username;
        }

        public long getTimestamp() {
            return timestamp;
        }

        public String getRemoteAddress() {
            return remoteAddress;
        }

        @Override
        public String toString() {
            ToStringBuilder builder = new ToStringBuilder(this).append(username).append(timestamp)
                    .append(remoteAddress);
            return builder.toString();
        }
    }

    /**
     * Get the authentication token timeout period.
     * 
     * @return the token timeout in milliseconds.
     */
    public long getTokenTimeout() {
        return tokenTimeout;
    }

    /**
     * Set the authentication token timeout period.
     * 
     * @param tokenTimeout
     *            the token timeout in milliseconds to set.
     */
    public void setTokenTimeout(long tokenTimeout) {
        this.tokenTimeout = tokenTimeout;
    }

    /**
     * Get the authentication token refresh timeout period. That is, the window of time when we avoid creating a new token to save on bandwidth and encryption
     * operations.
     * 
     * @return the authentication token refresh timeout period in milliseconds
     */
    public long getTokenRefreshWindow() {
        return tokenRefreshWindow;
    }

    /**
     * Set the authentication token refresh timeout period. That is, the window of time when we avoid creating a new token to save on bandwidth and encryption
     * operations.
     * 
     * @param tokenRefreshWindow
     *            the authentication token refresh timeout period in milliseconds to set.
     */
    public void setTokenRefreshWindow(long tokenRefreshWindow) {
        this.tokenRefreshWindow = tokenRefreshWindow;
    }

    /**
     * If true, the authenticator will not refresh the cookie when the refresh window elapses. Instead, it will place the cookie that would have been used to
     * refresh the token in a request attribute called {@link #ANZO_REFRESH_COOKIE_ATTRIBUTE}.
     * 
     * If false, the authenticator will refresh the authentication token by adding a cookie directly to the response and will not fill the
     * {@link #ANZO_REFRESH_COOKIE_ATTRIBUTE} request attribute.
     * 
     * Default is false. But will return null if it hasn't been set so that an explicit 'false' can be distinguished from a default 'false'.
     * 
     * @see #ANZO_REFRESH_COOKIE_ATTRIBUTE
     * @return true if a custom token refresh value should be used
     * 
     */
    public Boolean getCustomTokenRefresh() {
        return customTokenRefresh;
    }

    /**
     * Sets the customTokenRefresh mode.
     * 
     * @see #getCustomTokenRefresh()
     * @param customTokenRefresh
     *            the customTokenRefresh mode to set
     */
    public void setCustomTokenRefresh(Boolean customTokenRefresh) {
        this.customTokenRefresh = customTokenRefresh;
    }

    /**
     * @return the docRoot
     */
    public String getDocRoot() {
        return docRoot;
    }

    /**
     * @param docRoot
     *            the docRoot to set
     */
    public void setDocRoot(String docRoot) {
        this.docRoot = docRoot;
    }

    /**
     * Adds the give parameters to a URI string in the URI's query portion. It will add the '?' if needed, and will simply add the arguments if the URI already
     * has a query portion. It will also allow URIs with fragment portions (ex. '#foo') and place the query fragment and parameters in the appropriate place. It
     * will also escape any special URI characters in the parameter names or values.
     * 
     * This method assumes that the query string is in x-www-form-urlencoded format.
     * 
     * @param uri
     *            the URI string to modify
     * @param parameters
     *            the map with the key/value parameters to add to the query portion of the URI
     * @return a String URI with the parameters added to the given URI.
     * @throws URISyntaxException
     */
    public static String addQueryParametersToURI(String uri, MultiMap<String> parameters)
            throws URISyntaxException {
        URI inUri = new URI(uri);

        String paramStr = UrlEncoded.encode(parameters, null, false);
        String newQuery = inUri.getQuery() == null ? paramStr : inUri.getQuery() + "&" + paramStr;
        StringBuilder outUri = new StringBuilder();
        if (inUri.getScheme() != null) {
            outUri.append(inUri.getScheme());
            outUri.append(':');
        }
        if (inUri.getRawAuthority() != null) {
            outUri.append("//");
            outUri.append(inUri.getRawAuthority());
        }
        if (inUri.getRawPath() != null) {
            outUri.append(inUri.getRawPath());
        }
        if (StringUtils.isNotEmpty(newQuery)) {
            outUri.append('?');
            outUri.append(newQuery);
        }
        if (inUri.getRawFragment() != null) {
            outUri.append("#");
            outUri.append(inUri.getRawFragment());
        }
        return outUri.toString();
    }

    /**
     * Log the request to DEBUG level. WARNING: This should be used sparingly, if at all. Logging the entire request may expose sensitive information such as
     * passwords in the log files.
     * 
     * @param request
     */
    void logRequest(Request request) {
        if (log.isDebugEnabled()) {
            StringBuilder msg = new StringBuilder();
            msg.append("Request for ");
            msg.append(request.getRequestURL());
            msg.append("\nHeaders:");
            Enumeration<?> names = request.getHeaderNames();
            while (names.hasMoreElements()) {
                String name = (String) names.nextElement();
                msg.append("\n");
                msg.append(name);
                msg.append(" : ");
                Enumeration<?> headers = request.getHeaders(name);
                while (headers.hasMoreElements()) {
                    msg.append((String) headers.nextElement());
                    if (headers.hasMoreElements()) {
                        msg.append(",");
                    }
                }
            }
            msg.append("\nBody\n");
            Reader reader = null;
            try {
                reader = request.getReader();
                String body = IOUtils.toString(reader);
                msg.append(body);
            } catch (IOException e) {
                msg.append(e.getMessage());
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        log.debug(LogUtils.SECURITY_MARKER, "Error closing request reader while logging", e);
                    }
                }
            }
            msg.append("\nDone logging request.");
            log.debug(LogUtils.SECURITY_MARKER, msg.toString());
        }
    }
}