home / 2019.02.11 21:00 / apache spark / authentication / servlet filter

Spark UI Authentication

Following is a small filter to be used to authenticate users that want to access a Spark cluster, the master of ther worker nodes, through Spark's web UI. The filter in this post is not just verifying that a correct authentication token is present on the requests made to the Spark UI, it is actually trying to provide a minimal authentication mechanism (authentication form plus token generation and token verification), all in one small Java class. The post also details how to install the authentication filter across your Spark cluster and contains a small bonus at the end. The solution explored here is no appropriate for a real cluster deployment, but could be adapted to become an actual authentication solution that can scale across clusters.

Description of the old-school approach

Spark gives us the option of implementing authentication using a servlet filter. This class is part of a chain of classes that intercept the requests sent to the Spark UI server before Spark actually builds the view that is served to the browser. A filter on this chain can do some work and the pass the request/response pair to the next filter in the chain, but it can also write the response and end the chain if we want to.

We will implement a filter that decides is a user is authorized to access the Spark UI based on a session token. If the request made to Spark UI does not contain a valid token, we will deny access to the UI. But this solves only part of the problem, the one where we only permit access to the UI to the people that are supposed to have access. We also need a mechanism to allow users to obtain that access.

Obtaining access is usually done by providing credentials through a HTML form. The server then replies with a token that the client should save and include in all subsequent requests. An old-school strategy to make the client remember stuff and send that stuff back to the server are cookies. If the server provides the token in a cookie, the token will then be sent to the server and can be used to identify the users accessing the UI.

And that is exactly what we will implement. We will have a filter that checks is the request contains a token, as a cookie. If that token is present, we will validate it. If it is valid, we will let the user access the UI. If the token is not valid, we will ask the user to log in again. If the token is not present at all, we will ask the user to log in. We will provide a HTML form for the user to log in, by writing the HTML form on the response.

Another security mechanism we will include in this exercise is session expiration. We will use both cookie expiration settings, and an expiration date in the token, to validate that a token used by the client is still valid. This will be done without storing session data on the server (a good approach when working with a distributed application). And the last step we'll take will be to encrypt the token to prevent users from forging it and extending their sessions.

Setting up a Java project

We can create a Maven project, and we'll need a dependency to the Java servlet API. We can mark that dependency as "provided" since the Spark installation will have the servlet API in its JARs.

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cacoveanu</groupId> <artifactId>sparkauth</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies> </project>

You will build the project with mvn install and copy the resulting jar into your Spark installation to test out the filter (see the deployment section).

Implementing the actual filter

Filter main logic

The javax.servlet.Filter interface requires you to implement three methods, init, doFilter and destroy. The doFilter method is the main logic of our filter. This method gets called on every request made to the Spark UI.

public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException { Map<String, String> headers = getHeaders((HttpServletRequest) servletRequest); if (isAuthenticated(headers)) { filterChain.doFilter(servletRequest, servletResponse); } else if (isLoggingIn(servletRequest)) { Map<String, String> parameters = getPostParameters(readRequest(servletRequest)); if (areCredentialsCorrect(parameters)) { Date expiration = getMinutesFromNow(expirationMinutes); String token = createToken(expiration); String cookie = createCookieString("auth", token, expiration); ((HttpServletResponse) servletResponse).setHeader("Set-Cookie", cookie); ((HttpServletResponse) servletResponse).sendRedirect("/"); } else { servletResponse.getWriter().print(getLoginForm()); } } else { servletResponse.getWriter().print(getLoginForm()); } }

The logic in this method follows three main branches:

Verifying if a user is authenticated

A user is authenticated when the requests they make have a valid token in the headers. A token is valid if the expiration date it contains is after the current date on the server. The verification mechanism we implemented will look in two places for the token. We can provide the token as a simple header named "auth", or the token can be presented in a "Cookie" header. A cookie header can contain a list of cookies for the current domain, so we need to parse all cookies and find a cookie named "auth", that should contain the token.

private static String getAuthenticationToken(String name, Map<String, String> headers) { if (headers.containsKey(name)) { return headers.get(name); } else if (headers.containsKey("Cookie")) { Map<String, String> cookies = parseMap(headers.get("Cookie"), "; ", "="); if (cookies.containsKey(name)) return cookies.get(name); } return null; } private Boolean isAuthenticated(Map<String, String> headers) { String token = getAuthenticationToken("auth", headers); if (token != null) { Date expiration = parseToken(token); Date now = new Date(); if (now.before(expiration)) { return true; } } return false; }

Providing a Login Form

If the user is not logged in, we can give them the options of logging in. Since we have access to the response in our filter, we can write anything we want to the response body. One of those things can be a html login form. If that is what we return, the browser will display it. The action we use for the form is under the /login URL. That way, when credentials are sent to our server, they are sent under this URL, so our filter will know that the user is trying to log in.

private String getLoginForm() { return "<html><form action=\"/login\" method=\"post\">" + "<label for=\"username\"><b>Username</b></label>" + "<input type=\"text\" placeholder=\"Enter Username\" name=\"username\" required>" + "<label for=\"password\"><b>Password</b></label>" + "<input type=\"password\" placeholder=\"Enter Password\" name=\"password\" required>" + "<button type=\"submit\">Login</button>" + "<form></html>"; }

Verifying Login Credentials

In our main filter method, one of the paths our logic takes it the log in process. We verify if we are in that process with the isLoggingIn(servletRequest) method call. That method will check if the request URL ends in "/login". That is the way we recognize a login scenario.

private boolean isLoggingIn(ServletRequest servletRequest) { String url = ((HttpServletRequest)servletRequest) .getRequestURL().toString(); return url.endsWith("/login"); }

Once we know we are in the login scenario, we must obtain the credentials from the request body. We do that with the getPostParameters method, which uses the more generic method parseMap. We end up with a String to String map of post parameters.

private static Map<String, String> getPostParameters(String requestBody) { return parseMap(requestBody, "\\&", "="); } private static Map<String, String> parseMap( String data, String entrySeparator, String keyValueSeparator ) { return Arrays.stream(data.split(entrySeparator)) .map(p -> p.split(keyValueSeparator)) .collect(Collectors.toMap(a -> a[0], a-> a[1])); }

We finally verify that we have the required parameters username and password and we check that their values correspond to the hardcoded values in our filter.

private boolean areCredentialsCorrect(Map<String, String> parameters) { return parameters.containsKey("username") && parameters.containsKey("password") && parameters.get("username").equals(username) && parameters.get("password").equals(password); }

Generating a Token

Date expiration = getMinutesFromNow(expirationMinutes); String token = createToken(expiration); String cookie = createCookieString("auth", token, expiration); ((HttpServletResponse) servletResponse).setHeader("Set-Cookie", cookie); ((HttpServletResponse) servletResponse).sendRedirect("/");

After the credentials have been successfully validated, we are at the step where we must provide a session token to our user. Our token will only be valid for a number of minutes, which can be defined as a parameter of our filter. The getMinutesFromNow method will return the token expiration date based on the number of minutes a token should be valid. We take that date and create our token with the createToken method.

private String createToken(Date date) { String payload = toUtcString(date); return encrypt(payload); } private static String toUtcString(Date date) { return date.toInstant().toString(); }

The token is encrypted, to prevent the possibility of changing the expiration date of a session (see the encryption section). We then include in an cookie called "auth", and we create a Set-Cookie header on the response with the createCookieString method.

private static String createCookieString( String name, String value, Date expiration ) { String cookie = name + "=" + value; if (expiration != null) { return cookie + "; Expires=" + toGMTString(expiration); } else { return cookie; } }

We add this header to instruct the client, usually a browser, to store this cookie and use it on all subsequent requests to the domain our server is running on. The last step after a successfull login is to include a redirect instruction on the response, to instruct the browser to load the main Spark UI page.

Encryption and Decryption

As mentioned above, we want to keep the expiration date of a session secure. We could store session IDs and session expiration dates on the server, but this would complicate our code. We would need to store this information in a data structure that can be safely accessed and updated concurrently, because we can expect our filter to run on multiple threads at the same time (standard with a web server that can service multiple requests at the same time). The solution we chose was to make the expiration date part of the token that users store on their clients, and send on each call to our server. But since the tokens are stored by the clients, the clients can also edit them. It's easy to change the expiration date of a cookie, so we can't rely on that mechanism to expire the session after the alloted time has elapsed. In order to prevent tampering with the contents of our token, we will encrypt that token. We use the following code for encryption and decryption:

private String encrypt(String plainText) { String encryptedText = ""; try { cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); byte[] cipherText = cipher.doFinal(plainText.getBytes("UTF8")); Base64.Encoder encoder = Base64.getEncoder(); encryptedText = encoder.encodeToString(cipherText); } catch (Exception e) { System.err.println("Encrypt Exception : " + e.getMessage()); } return encryptedText; } private String decrypt(String encryptedText) { String decryptedText = ""; try { Base64.Decoder decoder = Base64.getDecoder(); byte[] cipherText = decoder.decode(encryptedText.getBytes("UTF8")); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); decryptedText = new String(cipher.doFinal(cipherText), "UTF-8"); } catch (Exception e) { System.err.println("decrypt Exception : " + e.getMessage()); } return decryptedText; }

Initializing the Filter

One last step we need to do to have a functioning solution is to set up the initialization of our filter, through the init method.

public void init(FilterConfig filterConfig) throws ServletException { String encryptionKey = filterConfig.getInitParameter("encryptionKey"); username = filterConfig.getInitParameter("username"); password = filterConfig.getInitParameter("password"); expirationMinutes = Integer.parseInt(filterConfig.getInitParameter("expirationMinutes")); try { cipher = Cipher.getInstance(cipherTransformation); byte[] key = encryptionKey.getBytes(characterEncoding); secretKey = new SecretKeySpec(key, aesEncryptionAlgorithem); ivParameterSpec = new IvParameterSpec(key); } catch (Exception e) { throw new ServletException(e); } }

The values for these parameters will come from Spark's properties files. We need the accepted username and password, hardcoded (in the config files) for now. We also need an encryption key. If, for any reason, we will need to invalidate all active sessions, we can change the encryption key in the configuration files and restart our Spark services. And we can also configure the lifetime of a token, through the number of minutes a token will be valid. We also initialize some of the classes needed for the encryption and decryption code.

Deploying the Filter

We first need to build our jar with mvn install. Then, we copy the jar to the machine running the Spark master. Make sure the Spark master is offline. Copy your jar to the $SPARK_HOME/jars folder.

Next, we add the configuration. Copy the $SPARK_HOME/conf/spark-defaults.conf.template file to $SPARK_HOME/conf/spark-defaults.conf and edit $SPARK_HOME/conf/spark-defaults.conf to add the following properties:

spark.ui.filters=com.cacoveanu.spark.authentication.SparkAuthFilter spark.com.cacoveanu.spark.authentication.SparkAuthFilter.param.encryptionKey=tV5XPijhZiclsAmi spark.com.cacoveanu.spark.authentication.SparkAuthFilter.param.username=admin spark.com.cacoveanu.spark.authentication.SparkAuthFilter.param.password=pass spark.com.cacoveanu.spark.authentication.SparkAuthFilter.param.expirationMinutes=5

Of course, change the values of the properties. For the encryption key, use a 16 character random string.

We can now start the Spark master with the $SPARK_HOME/sbin/start-master.sh command. If we navigate in the browser to the Spark UI at http://spark-master-url:8080/, we should be greeted by the login page.

Spark UI cookie-based authentication form

Following the same steps on all your Spark workers will apply the filter to them as well.

Problems and Further Development

First thing that should be mentioned for any web application that requires login is that you need to enable HTTPS. Otherwise, your credentials will be provided in plaintext, so anyone watching the network between the client and the server will obtain them. This applies to both the cookie implementation and the basic authentication implementation.

Another mention and problem to solve in the future is that when deploying in an actual cluster, you will need to login on all the machines you connect to across the cluster when you access them for the first time. To fix this, you will need a better implementation of this filter, some kind of single sign-on setup for your Spark cluster.

Description of the new-school approach

We have another, simpler, option to implement our authentication filter, the "new-school" approach, I call it. Instead of making a filter that relies on cookies to identify users, we can use the basic authentication mechanism. Basic athentication is a simple web access control mechanism that relies on headers to provide a username and a password. The steps in this new filter are:

Basic authentication works based on every request sent to the server having a header called "Authorization" that contains the username and password, separated by a colon and encoded in Base64 (not encrypted!). If that header is present, the credentials can be verified. If they are correct, the user is authenticated and the UI can be accessed. If the header is missing, or the credentials are not correct, the user is not authenticated and the server will signal this (ask for authentication) by adding a header named "WWW-Authenticate" to the response. This header will let the client know they need to use basic authentication to access the page. Most modern browsers support this authentication method and will store the "Authorization" header for that domain and add it to every request automatically.

Implementation of filter using Basic Authentication

Our new doFilter method is very simple now, because we only have two logic paths that we must handle. We are no longer responsible for providing a method for the user to authenticate, the browsers will provide that functionality. We only need to check if the user is authenticated, based on the relevant header, or if they are not authenticated we must ask them to provide their credentials. This is done by adding a header named WWW-Authenticate to the response. The value of that header contains the realm under which they are asked to authenticate. This realm can be any string and it is used to identify the application, so the users know what credentials to provide. We must also mark the status of the response as 401.

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { Map<String, String> headers = getHeaders((HttpServletRequest) servletRequest); if (isAuthenticated(headers)) { filterChain.doFilter(servletRequest, servletResponse); } else { ((HttpServletResponse) servletResponse).setHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\""); ((HttpServletResponse) servletResponse).setStatus(401); } }

To verify if a user is authenticated, we look at the Authorization header, if it exists. This header should start with the word Basic, a space and then a base64 encoded string. We decode the base64 string and expect to find the username and password fields separated by a colon (usernames used for basic authentication can't contain colons). We then check that the username and password are equal to the hard-coded values we expect.

private Boolean isAuthenticated(Map<String, String> headers) { if (headers.containsKey("Authorization")) { String token = headers.get("Authorization"); String userPass = token.substring("Basic ".length()); String res = new String(DatatypeConverter.parseBase64Binary(userPass)); String username = res.substring(0, res.indexOf(':')); String password = res.substring(res.indexOf(':') + 1); return this.username.equals(username) && this.password.equals(password); } return false; }

That is all the login in this filter. Even the initialization is simpler, we only require three parameters, the username, the password and the name of the realm.

public void init(FilterConfig filterConfig) throws ServletException { username = filterConfig.getInitParameter("username"); password = filterConfig.getInitParameter("password"); realm = filterConfig.getInitParameter("realm"); }

The browser will present it's standard form when you try to connect to the Spark UI. You can see the realm you configured in the form name.

Spark UI basic authentication window

The same problems and further development points apply to this sollution as well. While the solution is simpler, you have less control. You can't write your own login form and are dependent on the login window the browsers present. You also can't determine the expiration time of the session, this time is also decided by the browser and may vary between browsers, leading to a more inconsistent experience.

Bonus: Fully Styled Web Page

The login page we serve can be a fully styled HTML page, and can even include images. These images can be embedded into the HTML code if they are encoded in Base64.

Convert your image to a base64 string using PowerShell:

[convert]::ToBase64String((get-content filename.png -encoding byte)) >> filename.txt

Or convert your image to a base64 string on Linux:

base64 filename.png > filename.txt

Then you can embed the image directly in your HTML:

<img src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAA1UlEQVR42u3YsQ3AMAwDQe6/dLKEEBHKPeDe5nXOo6piAiACAkRAgAgIEAEBIiACAkRAgAgIEAEBorMgSUbP7+4HBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEDOgggIEAEBIiACAkRAgOgLkK2vhPoBh3cBAgQIECBAgAABAgQIECBAui94ZWggQIAAAQIECBAgQIAAAQJkB2T6wVtnbRcgQIAAAQIECBAgQIAAAQJETV82JgAiIEAEBIiAABEQIAIiIEAEBIiAABEQIGrpBQnC/nlQxzkqAAAAAElFTkSuQmCC ">

Replace everything after <IMG SRC="data:image/gif;base64, with the base64 data for your image.

With this simple method we could serve html with embedded images from inside our filter.

Full Code

Cookie-based authentication filter.

package com.cacoveanu.spark.authentication; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; public class SparkAuthFilter implements javax.servlet.Filter { private static final String characterEncoding = "UTF-8"; private static final String cipherTransformation = "AES/CBC/PKCS5PADDING"; private static final String aesEncryptionAlgorithem = "AES"; private Cipher cipher; private SecretKeySpec secretKey; private IvParameterSpec ivParameterSpec; private String username; private String password; private Integer expirationMinutes; private static String createCookieString( String name, String value, Date expiration ) { String cookie = name + "=" + value; if (expiration != null) { return cookie + "; Expires=" + toGMTString(expiration); } else { return cookie; } } public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException { Map<String, String> headers = getHeaders((HttpServletRequest) servletRequest); if (isAuthenticated(headers)) { filterChain.doFilter(servletRequest, servletResponse); } else if (isLoggingIn(servletRequest)) { Map<String, String> parameters = getPostParameters(readRequest(servletRequest)); if (areCredentialsCorrect(parameters)) { Date expiration = getMinutesFromNow(expirationMinutes); String token = createToken(expiration); String cookie = createCookieString("auth", token, expiration); ((HttpServletResponse) servletResponse).setHeader("Set-Cookie", cookie); ((HttpServletResponse) servletResponse).sendRedirect("/"); } else { servletResponse.getWriter().print(getLoginForm()); } } else { servletResponse.getWriter().print(getLoginForm()); } } private boolean areCredentialsCorrect(Map<String, String> parameters) { return parameters.containsKey("username") && parameters.containsKey("password") && parameters.get("username").equals(username) && parameters.get("password").equals(password) ; } private boolean isLoggingIn(ServletRequest servletRequest) { String url = ((HttpServletRequest)servletRequest) .getRequestURL().toString(); return url.endsWith("/login"); } private static Map<String, String> getPostParameters( String requestBody ) { return parseMap(requestBody, "\\&", "="); } private static Map<String, String> parseMap( String data, String entrySeparator, String keyValueSeparator ) { return Arrays.stream(data.split(entrySeparator)) .map(p -> p.split(keyValueSeparator)) .collect(Collectors.toMap(a -> a[0], a-> a[1])); } private String readRequest( ServletRequest servletRequest ) throws IOException { java.util.Scanner s = new java.util.Scanner( servletRequest.getInputStream() ).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } private String getLoginForm() { return "<html><form action=\"/login\" method=\"post\">\n" + " <label for=\"username\"><b>Username</b></label>\n" + " <input type=\"text\" placeholder=\"Enter Username\" name=\"username\" required>\n" + "\n" + " <label for=\"password\"><b>Password</b></label>\n" + " <input type=\"password\" placeholder=\"Enter Password\" name=\"password\" required>\n" + "\n" + " <button type=\"submit\">Login</button>\n" + "<form></html>"; } private static String getAuthenticationToken( String name, Map<String, String> headers ) { if (headers.containsKey(name)) { return headers.get(name); } else if (headers.containsKey("Cookie")) { Map<String, String> cookies = parseMap(headers.get("Cookie"), "; ", "="); if (cookies.containsKey(name)) return cookies.get(name); } return null; } private Boolean isAuthenticated(Map<String, String> headers) { String token = getAuthenticationToken("auth", headers); if (token != null) { Date expiration = parseToken(token); Date now = new Date(); if (now.before(expiration)) { return true; } } return false; } private Map<String, String> getHeaders(HttpServletRequest servletRequest) { return Collections.list(servletRequest.getHeaderNames()).stream() .collect(Collectors.toMap(h -> h, servletRequest::getHeader)); } public void init(FilterConfig filterConfig) throws ServletException { String encryptionKey = filterConfig.getInitParameter("encryptionKey"); username = filterConfig.getInitParameter("username"); password = filterConfig.getInitParameter("password"); expirationMinutes = Integer.parseInt(filterConfig.getInitParameter("expirationMinutes")); try { cipher = Cipher.getInstance(cipherTransformation); byte[] key = encryptionKey.getBytes(characterEncoding); secretKey = new SecretKeySpec(key, aesEncryptionAlgorithem); ivParameterSpec = new IvParameterSpec(key); } catch (Exception e) { throw new ServletException(e); } } public void destroy() { } public static Date getMinutesFromNow(int minutes) { Calendar now = Calendar.getInstance(); now.add(Calendar.MINUTE, minutes); return now.getTime(); } private static String toGMTString(Date date) { //return date.toGMTString(); DateFormat df = new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss zzz"); df.setTimeZone(TimeZone.getTimeZone("GMT")); return df.format(date); } private static String toUtcString(Date date) { return date.toInstant().toString(); } private Date parseToken(String encryptedToken) { String token = decrypt(encryptedToken); Instant time = Instant.parse(token); return Date.from(time); } private String createToken(Date date) { String payload = toUtcString(date); return encrypt(payload); } private String encrypt(String plainText) { String encryptedText = ""; try { cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); byte[] cipherText = cipher.doFinal(plainText.getBytes("UTF8")); Base64.Encoder encoder = Base64.getEncoder(); encryptedText = encoder.encodeToString(cipherText); } catch (Exception e) { System.err.println("Encrypt Exception : " + e.getMessage()); } return encryptedText; } private String decrypt(String encryptedText) { String decryptedText = ""; try { Base64.Decoder decoder = Base64.getDecoder(); byte[] cipherText = decoder.decode(encryptedText.getBytes("UTF8")); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); decryptedText = new String(cipher.doFinal(cipherText), "UTF-8"); } catch (Exception e) { System.err.println("decrypt Exception : " + e.getMessage()); } return decryptedText; } }

Basic authentication-based filter:

package com.cacoveanu.spark.authentication; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.bind.DatatypeConverter; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; public class SparkBasicAuthFilter implements Filter { private String username; private String password; private String realm; public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException { Map<String, String> headers = getHeaders((HttpServletRequest) servletRequest); if (isAuthenticated(headers)) { filterChain.doFilter(servletRequest, servletResponse); } else { ((HttpServletResponse) servletResponse).setHeader( "WWW-Authenticate", "Basic realm=\"" + realm + "\"" ); ((HttpServletResponse) servletResponse).setStatus(401); } } private static Map<String, String> parseMap( String data, String entrySeparator, String keyValueSeparator ) { return Arrays.stream(data.split(entrySeparator)) .map(p -> p.split(keyValueSeparator)) .collect(Collectors.toMap(a -> a[0], a-> a[1])); } private Boolean isAuthenticated(Map<String, String> headers) { if (headers.containsKey("Authorization")) { String token = headers.get("Authorization"); String userPassB64 = token.substring("Basic ".length()); String userPassText = new String( DatatypeConverter.parseBase64Binary(userPassB64) ); String username = userPassText.substring( 0, userPassText.indexOf(':') ); String password = userPassText.substring( userPassText.indexOf(':') + 1 ); return this.username.equals(username) && this.password.equals(password); } return false; } private Map<String, String> getHeaders(HttpServletRequest servletRequest) { return Collections.list(servletRequest.getHeaderNames()).stream() .collect(Collectors.toMap(h -> h, servletRequest::getHeader)); } public void init(FilterConfig filterConfig) throws ServletException { username = filterConfig.getInitParameter("username"); password = filterConfig.getInitParameter("password"); realm = filterConfig.getInitParameter("realm"); } public void destroy() { } }