Configuring basic authentication for search

Managed by | Updated .

Background

Funnelback uses basic authentication for access to the Admin UI, 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

As best practice, any configuration settings for our script need to be configured in the collection.cfg file. These will be used in the script below.

To configure the username and password for the basic auth, the following settings need to be configured:

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. In this case, we can configure a list of IP addresses that will be whitelisted and bypass the basic auth.

Multiple ip addresses can be separated by a comma. ie

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 basic auth

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:

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.

Further reading

  • https://docs.funnelback.com/15.12/more/extra/custom_servlet_filter_hook.html
  • https://www.base64encode.org/
  • https://www.getpostman.com/
Was this artcle helpful?

Tags
Type:
Features: Frontend > Hook scripts

Comments