Monday, February 02, 2015

How to get the original client IP address when it is masked in the Http request by a load-balancer/proxy in a Spring web application...

It's quite common when apache web servers are behind a load balancer/proxy, the actual client IP address is replaced with that of the balancer/proxy. In those situations, the servers could be configured to replace the actual IP address of the client in the HTTP request with that of the proxy server and put the original IP address in a special HTTP request header "X-Forwarded-For". The header gets passed downstream to the apache web server and to the app server like Tomcat and would actually be available to the application via the HTTP header. Apache and Tomcat can as well be configured to log this original IP address.

If there is a need for the application to log the original IP address, the spring security logs the authentication details in DEBUG mode, anyway. But in this case, the IP address that gets logged by spring security will be the proxy IP address as it extracts this detail from the HTTP request that was already modified by the proxy server.

So, in order to log the original client IP Address, a custom authentication details class needs to be written and hooked into the spring security appropriately. The following are the two Spring security classes that are involved in this:

org.springframework.security.web.authentication.WebAuthenticationDetailsSource - it is a simple class which build thes authentication details object from the HTTPServletRequest
org.springframework.security.web.authentication.WebAuthenticationDetails - is the authentication details class that holds the remoteAddress and sessionId.

The two custom classes needed can be as simple as and similar to the above two. All you need to do is to correctly set the remoteAddress extracting it from the HTTP request header instead of from the HTTP Request.

Below are the two custom classes written in groovy. Java is no different, but will be little more code implementing the equals and hashCode methods. The import statements are omitted for brevity.

Custom Classes:
/**
 * CustomWebAuthenticationDetails.groovy
 * A custom WebAuthenticationDetails object to look at the X-FORWARDED-FOR
 * header to get the source remote IP address in case of load-balancer/proxy
 * which sets the source IP address in HTTP header and masks it in the actual
 * HTTPRequest.
 */
@EqualsAndHashCode
public class CustomWebAuthenticationDetails implements Serializable {
    private static final long serialVersionUID =
        SpringSecurityCoreVersion.SERIAL_VERSION_UID

    private final String remoteAddress
    private final String sessionId

    /**
     * Records the remote address and will also set the session Id if a session
     * already exists (it won't create one).
     *
     * @param request that the authentication request was received from
     */
    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        //get remoteAddress if set in the header. otherwise, get it from the request
        this.remoteAddress = request.getHeader('X-FORWARDED-FOR') ?: request.getRemoteAddr()

        HttpSession session = request.getSession(false)
        this.sessionId = session ? session.getId() : null
    }

    /** @see java.lang.Object#toString() */
    public String toString() {
        "${super.toString()}: RemoteIpAddress: ${remoteAddress};SessionId:${sessionId}"
    }
}

/**
 * CustomWebAuthenticationDetailsSource.groovy
 */
public class CustomWebAuthenticationDetailsSource
    implements AuthenticationDetailsSource<HttpServletRequest, CustomWebAuthenticationDetails> {

    /**
     * @see org.springframework.security.web.authentication.WebAuthenticationDetailsSource#buildDetails(javax.servlet.http.HttpServletRequest)
     */
    @Override
    public CustomWebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new CustomWebAuthenticationDetails(context)
    }
}

Spring Configuration:
Configure Spring secuirty to use CustomWebAuthenticationDetailsSource class instead of it's default WebAuthenticationDetailsSource
    <sec:form-login
        login-page="/"
        default-target-url="/home"
        always-use-default-target="true"
        authentication-details-source-ref="customWebAuthenticationDetailsSource"
        authentication-failure-url="/?login_error=true"/>
    <sec:anonymous enabled="false" />

    <bean id="customWebAuthenticationDetailsSource"
        class="com.giri.security.CustomWebAuthenticationDetailsSource"/>

Testing
When you use FireBug to test and examine the headers of the HTTP request, the "X-Forwarded-For" will not be seen as the browser request will not contain any trace of this. Both the request and header get modified by the apache server behind the load-balancer/proxy server and hence it can only be accessed from that point onwards, but not from the browser initiated request.