Configuring basic authentication for search

This article does not apply to the SXC version of Funnelback. A custom plugin that implements this functionality is required within the SXC.

Background

Funnelback uses basic authentication for access to the administration interface but there is currently no feature for enabling this for the search endpoints.

This article shows you how to configure a search endpoint with basic authentication on a collection level.

Configure the basic auth settings

To configure the username and password for the basic authentication the following settings need to be configured in collection.cfg:

collection.cfg
custom.basic_auth_username=<BASIC_AUTH_USERNAME>
custom.basic_auth_password=<BASIC_AUTH_PASSWORD>

It’s possible you have applications like a monitoring service that can’t pass through the basic auth details but still need access to the search endpoint. To accommodate this a list of IP addresses that will be whitelisted and bypass the basic auth are also configured.

Multiple ip addresses can be separated by a comma. e.g.

collection.cfg
custom.whitelist=10.10.10.1,127.0.0.1

Configure the basic auth script

Using a custom servlet filter hook, we can gain access to the incoming request and headers to determine if a valid username and password have been passed through in the correct basic auth format.

This script must be placed in the /opt/funnelback/conf/<COLLECTION_ID>/ folder with the name GroovyServletFilterHookPublicUIImpl.groovy

Below is an example of a script we can use with this method to achieve this:

GroovyServletFilterHookPublicUIImpl.groovy
import com.funnelback.springmvc.web.filter.GroovyServletFilterHook;
import com.funnelback.publicui.search.web.filters.utils.InterceptableHttpServletResponseWrapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.commons.io.output.TeeOutputStream;
import com.funnelback.common.config.*;

public class GroovyServletFilterHookPublicUIImpl extends GroovyServletFilterHook {

    private ByteArrayOutputStream baos = null;
    private config = new NoOptionsConfig(new File("/opt/funnelback"), "<COLLECTION_ID>");

    public ServletResponse preFilterResponse(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String path = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
        String remoteIP = httpRequest.getRemoteAddr();

        //uncomment to enable for the suggest.json
        //if (path.startsWith("/suggest.")) return response;

        //Allow redirects for click tracking
        if (path.startsWith("/redirect")) return response;

        Enumeration<String> headerNames = httpRequest.getHeaderNames();

        String authorizationValue = getHeaderValue("Authorization", headerNames, request);
        OAuthCredentials oAuthCredentials = parseAuthorizationValue(authorizationValue);

        if (!isValidCredentials(oAuthCredentials) && !isWhitelistedIP(remoteIP)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.sendError(401);
        }

        return response;
    }

    /*
    Description: get the value of a parameter from the request header
    */
    private String getHeaderValue(String keyTarget, Enumeration<String> headerNames, ServletRequest request) {
      //Iterate over headers and return matching header value
      if (headerNames != null) {
        while (headerNames.hasMoreElements()) {
          String key = (String) headerNames.nextElement();
          String value = request.getHeader(key);

          if (keyTarget == key) return value;
        }
      }

      return "";
    }

    /*
    Description
      Parse an OAuth header and return the username and password.
      Expected format of the authorizationValue should be similar to:
        Basic YnJva2Vyc3NlYXJjaDpzb21lc2VjdXJlcGFzc3dvcmQ=
      The second part of this value is a base64 username and passowrd in the following format:
        username:password
    */
    private OAuthCredentials parseAuthorizationValue(String authorizationValue) {

      OAuthCredentials oAuthCredentials = new OAuthCredentials();

      try {
        if (authorizationValue != null && authorizationValue.startsWith("Basic")) {
          String base64Credentials = authorizationValue.substring("Basic".length()).trim();
          String credentials = new String(Base64.getDecoder().decode(base64Credentials), "UTF-8");

          // credentials = username:password
          final String[] values = credentials.split(":",2);

          oAuthCredentials.username = values[0];
          oAuthCredentials.password = values[1];
        }
      } catch (all) {
        //Invalid OAuth format (TODO: log to modern ui)
      }

      return oAuthCredentials;
    }

    private boolean isValidCredentials(OAuthCredentials oAuthCredentials) {
      if (oAuthCredentials.username != null &&
          oAuthCredentials.username != "" &&
          oAuthCredentials.username == config.value("custom.basic_auth_username") &&
          oAuthCredentials.username != null &&
          oAuthCredentials.username != "" &&
          oAuthCredentials.password == config.value("custom.basic_auth_password"))
           return true;

      return false;
    }

    private boolean isWhitelistedIP(String remoteIP) {
      if (config.value("custom.whitelist") != null) {
        if (config.value("custom.whitelist").tokenize(',').contains(remoteIP))
          return true;
      }

      return false;
    }
}

//Data type for storing OAuth username and password
public class OAuthCredentials {
  String username;
  String password;
}

Testing the authentication

Now that we’ve configured everything above, all requests that comes to Funnelback need to contain a basic auth header with a valid username and password.

The format of the basic auth header looks like the following:

Authorization: Basic <username:password>

The username:password combination needs to be base64 encoded (including the colon). For example, assuming we’d configured the collection.cfg with:

collection.cfg
custom.basic_auth_username=thesearchusername
custom.basic_auth_password=kA:Ya/OZ@3z,

The resulting header would look like:

Authorization: Basic dGhlc2VhcmNodXNlcm5hbWU6a0E6WWEvT1pAM3os

If you don’t have a live application there are a variety of tools you can use to add headers to your request like CURL or Postman.