+ * A User-Agent String is considered to be a browser if it does not contain
+ * any of the values from alt-kerberos.non-browser.user-agents; the default
+ * behavior is to consider everything a browser unless it contains one of:
+ * "java", "curl", "wget", or "perl". Subclasses can optionally override
+ * this method to use different behavior.
+ *
+ * @param userAgent The User-Agent String, or null if there isn't one
+ * @return true if the User-Agent String refers to a browser, false if not
+ */
+ protected boolean isBrowser(String userAgent) {
+ if (userAgent == null) {
+ return false;
+ }
+ userAgent = userAgent.toLowerCase(Locale.ENGLISH);
+ boolean isBrowser = true;
+ for (String nonBrowserUserAgent : nonBrowserUserAgents) {
+ if (userAgent.contains(nonBrowserUserAgent)) {
+ isBrowser = false;
+ break;
+ }
+ }
+ return isBrowser;
+ }
+
+ /**
+ * Subclasses should implement this method to provide the custom
+ * authentication to be used for browsers.
+ *
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ * @return an authentication token if the request is authorized, or null
+ * @throws IOException thrown if an IO error occurs
+ * @throws AuthenticationException thrown if an authentication error occurs
+ */
+ public abstract AuthenticationToken alternateAuthenticate(
+ HttpServletRequest request, HttpServletResponse response)
+ throws IOException, AuthenticationException;
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationFilter.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationFilter.java
new file mode 100644
index 000000000000..d95a6652decf
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationFilter.java
@@ -0,0 +1,712 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import java.lang.reflect.InvocationTargetException;
+import java.io.IOException;
+import java.security.Principal;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+import org.apache.hadoop.hbase.security.authentication.util.Signer;
+import org.apache.hadoop.hbase.security.authentication.util.SignerException;
+import org.apache.hadoop.hbase.security.authentication.util.SignerSecretProvider;
+import org.apache.hadoop.hbase.security.authentication.util.FileSignerSecretProvider;
+import org.apache.hadoop.hbase.security.authentication.util.RandomSignerSecretProvider;
+import org.apache.hadoop.hbase.security.authentication.util.ZKSignerSecretProvider;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * The {@link AuthenticationFilter} enables protecting web application
+ * resources with different (pluggable)
+ * authentication mechanisms and signer secret providers.
+ *
+ * Additional authentication mechanisms are supported via the {@link AuthenticationHandler} interface.
+ *
+ * This filter delegates to the configured authentication handler for authentication and once it obtains an
+ * {@link AuthenticationToken} from it, sets a signed HTTP cookie with the token. For client requests
+ * that provide the signed HTTP cookie, it verifies the validity of the cookie, extracts the user information
+ * and lets the request proceed to the target resource.
+ *
+ * The rest of the configuration properties are specific to the {@link AuthenticationHandler} implementation and the
+ * {@link AuthenticationFilter} will take all the properties that start with the prefix #PREFIX#, it will remove
+ * the prefix from it and it will pass them to the the authentication handler for initialization. Properties that do
+ * not start with the prefix will not be passed to the authentication handler initialization.
+ *
+ * The "zookeeper" implementation has additional configuration properties that
+ * must be specified; see {@link ZKSignerSecretProvider} for details.
+ */
+
+@InterfaceAudience.Private
+@InterfaceStability.Unstable
+public class AuthenticationFilter implements Filter {
+
+ private static Logger LOG = LoggerFactory.getLogger(AuthenticationFilter.class);
+
+ /**
+ * Constant for the property that specifies the configuration prefix.
+ */
+ public static final String CONFIG_PREFIX = "config.prefix";
+
+ /**
+ * Constant for the property that specifies the authentication handler to use.
+ */
+ public static final String AUTH_TYPE = "type";
+
+ /**
+ * Constant for the property that specifies the secret to use for signing the HTTP Cookies.
+ */
+ public static final String SIGNATURE_SECRET = "signature.secret";
+
+ public static final String SIGNATURE_SECRET_FILE = SIGNATURE_SECRET + ".file";
+
+ /**
+ * Constant for the configuration property
+ * that indicates the max inactive interval of the generated token.
+ */
+ public static final String
+ AUTH_TOKEN_MAX_INACTIVE_INTERVAL = "token.max-inactive-interval";
+
+ /**
+ * Constant for the configuration property that indicates the validity of the generated token.
+ */
+ public static final String AUTH_TOKEN_VALIDITY = "token.validity";
+
+ /**
+ * Constant for the configuration property that indicates the domain to use in the HTTP cookie.
+ */
+ public static final String COOKIE_DOMAIN = "cookie.domain";
+
+ /**
+ * Constant for the configuration property that indicates the path to use in the HTTP cookie.
+ */
+ public static final String COOKIE_PATH = "cookie.path";
+
+ /**
+ * Constant for the configuration property
+ * that indicates the persistence of the HTTP cookie.
+ */
+ public static final String COOKIE_PERSISTENT = "cookie.persistent";
+
+ /**
+ * Constant for the configuration property that indicates the name of the
+ * SignerSecretProvider class to use.
+ * Possible values are: "file", "random", "zookeeper", or a classname.
+ * If not specified, the "file" implementation will be used with
+ * SIGNATURE_SECRET_FILE; and if that's not specified, the "random"
+ * implementation will be used.
+ */
+ public static final String SIGNER_SECRET_PROVIDER =
+ "signer.secret.provider";
+
+ /**
+ * Constant for the ServletContext attribute that can be used for providing a
+ * custom implementation of the SignerSecretProvider. Note that the class
+ * should already be initialized. If not specified, SIGNER_SECRET_PROVIDER
+ * will be used.
+ */
+ public static final String SIGNER_SECRET_PROVIDER_ATTRIBUTE =
+ "signer.secret.provider.object";
+
+ private Properties config;
+ private Signer signer;
+ private SignerSecretProvider secretProvider;
+ private AuthenticationHandler authHandler;
+ private long maxInactiveInterval;
+ private long validity;
+ private String cookieDomain;
+ private String cookiePath;
+ private boolean isCookiePersistent;
+ private boolean destroySecretProvider;
+
+ /**
+ *
Initializes the authentication filter and signer secret provider.
+ * It instantiates and initializes the specified {@link
+ * AuthenticationHandler}.
+ *
+ * @param filterConfig filter configuration.
+ *
+ * @throws ServletException thrown if the filter or the authentication handler could not be initialized properly.
+ */
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ String configPrefix = filterConfig.getInitParameter(CONFIG_PREFIX);
+ configPrefix = (configPrefix != null) ? configPrefix + "." : "";
+ config = getConfiguration(configPrefix, filterConfig);
+ String authHandlerName = config.getProperty(AUTH_TYPE, null);
+ String authHandlerClassName;
+ if (authHandlerName == null) {
+ throw new ServletException("Authentication type must be specified: " +
+ PseudoAuthenticationHandler.TYPE + "|" +
+ KerberosAuthenticationHandler.TYPE + "|");
+ }
+ authHandlerClassName =
+ AuthenticationHandlerUtil
+ .getAuthenticationHandlerClassName(authHandlerName);
+ maxInactiveInterval = Long.parseLong(config.getProperty(
+ AUTH_TOKEN_MAX_INACTIVE_INTERVAL, "-1")); // By default, disable.
+ if (maxInactiveInterval > 0) {
+ maxInactiveInterval *= 1000;
+ }
+ validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, "36000"))
+ * 1000; //10 hours
+ initializeSecretProvider(filterConfig);
+
+ initializeAuthHandler(authHandlerClassName, filterConfig);
+
+ cookieDomain = config.getProperty(COOKIE_DOMAIN, null);
+ cookiePath = config.getProperty(COOKIE_PATH, null);
+ isCookiePersistent = Boolean.parseBoolean(
+ config.getProperty(COOKIE_PERSISTENT, "false"));
+
+ }
+
+ protected void initializeAuthHandler(String authHandlerClassName, FilterConfig filterConfig)
+ throws ServletException {
+ try {
+ Class> klass = Thread.currentThread().getContextClassLoader().loadClass(authHandlerClassName);
+ authHandler = (AuthenticationHandler) klass.getDeclaredConstructor().newInstance();
+ authHandler.init(config);
+ } catch (ClassNotFoundException | InstantiationException |
+ IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) {
+ throw new ServletException(ex);
+ }
+ }
+
+ protected void initializeSecretProvider(FilterConfig filterConfig)
+ throws ServletException {
+ secretProvider = (SignerSecretProvider) filterConfig.getServletContext().
+ getAttribute(SIGNER_SECRET_PROVIDER_ATTRIBUTE);
+ if (secretProvider == null) {
+ // As tomcat cannot specify the provider object in the configuration.
+ // It'll go into this path
+ try {
+ secretProvider = constructSecretProvider(
+ filterConfig.getServletContext(),
+ config, false);
+ destroySecretProvider = true;
+ } catch (Exception ex) {
+ throw new ServletException(ex);
+ }
+ }
+ signer = new Signer(secretProvider);
+ }
+
+ public static SignerSecretProvider constructSecretProvider(
+ ServletContext ctx, Properties config,
+ boolean disallowFallbackToRandomSecretProvider) throws Exception {
+ String name = config.getProperty(SIGNER_SECRET_PROVIDER, "file");
+ long validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY,
+ "36000")) * 1000;
+
+ if (!disallowFallbackToRandomSecretProvider
+ && "file".equals(name)
+ && config.getProperty(SIGNATURE_SECRET_FILE) == null) {
+ name = "random";
+ }
+
+ SignerSecretProvider provider;
+ if ("file".equals(name)) {
+ provider = new FileSignerSecretProvider();
+ try {
+ provider.init(config, ctx, validity);
+ } catch (Exception e) {
+ if (!disallowFallbackToRandomSecretProvider) {
+ LOG.warn("Unable to initialize FileSignerSecretProvider, " +
+ "falling back to use random secrets. Reason: " + e.getMessage());
+ provider = new RandomSignerSecretProvider();
+ provider.init(config, ctx, validity);
+ } else {
+ throw e;
+ }
+ }
+ } else if ("random".equals(name)) {
+ provider = new RandomSignerSecretProvider();
+ provider.init(config, ctx, validity);
+ } else if ("zookeeper".equals(name)) {
+ provider = new ZKSignerSecretProvider();
+ provider.init(config, ctx, validity);
+ } else {
+ provider = (SignerSecretProvider) Thread.currentThread().
+ getContextClassLoader().loadClass(name).getDeclaredConstructor().newInstance();
+ provider.init(config, ctx, validity);
+ }
+ return provider;
+ }
+
+ /**
+ * Returns the configuration properties of the {@link AuthenticationFilter}
+ * without the prefix. The returned properties are the same that the
+ * {@link #getConfiguration(String, FilterConfig)} method returned.
+ *
+ * @return the configuration properties.
+ */
+ protected Properties getConfiguration() {
+ return config;
+ }
+
+ /**
+ * Returns the authentication handler being used.
+ *
+ * @return the authentication handler being used.
+ */
+ protected AuthenticationHandler getAuthenticationHandler() {
+ return authHandler;
+ }
+
+ /**
+ * Returns if a random secret is being used.
+ *
+ * @return if a random secret is being used.
+ */
+ protected boolean isRandomSecret() {
+ return secretProvider.getClass() == RandomSignerSecretProvider.class;
+ }
+
+ /**
+ * Returns if a custom implementation of a SignerSecretProvider is being used.
+ *
+ * @return if a custom implementation of a SignerSecretProvider is being used.
+ */
+ protected boolean isCustomSignerSecretProvider() {
+ Class> clazz = secretProvider.getClass();
+ return clazz != FileSignerSecretProvider.class && clazz !=
+ RandomSignerSecretProvider.class && clazz != ZKSignerSecretProvider
+ .class;
+ }
+
+ /**
+ * Returns the max inactive interval time of the generated tokens.
+ *
+ * @return the max inactive interval time of the generated tokens in seconds.
+ */
+ protected long getMaxInactiveInterval() {
+ return maxInactiveInterval / 1000;
+ }
+
+ /**
+ * Returns the validity time of the generated tokens.
+ *
+ * @return the validity time of the generated tokens, in seconds.
+ */
+ protected long getValidity() {
+ return validity / 1000;
+ }
+
+ /**
+ * Returns the cookie domain to use for the HTTP cookie.
+ *
+ * @return the cookie domain to use for the HTTP cookie.
+ */
+ protected String getCookieDomain() {
+ return cookieDomain;
+ }
+
+ /**
+ * Returns the cookie path to use for the HTTP cookie.
+ *
+ * @return the cookie path to use for the HTTP cookie.
+ */
+ protected String getCookiePath() {
+ return cookiePath;
+ }
+
+ /**
+ * Returns the cookie persistence to use for the HTTP cookie.
+ *
+ * @return the cookie persistence to use for the HTTP cookie.
+ */
+ protected boolean isCookiePersistent() {
+ return isCookiePersistent;
+ }
+
+ /**
+ * Destroys the filter.
+ *
+ * It invokes the {@link AuthenticationHandler#destroy()} method to release any resources it may hold.
+ */
+ @Override
+ public void destroy() {
+ if (authHandler != null) {
+ authHandler.destroy();
+ authHandler = null;
+ }
+ if (secretProvider != null && destroySecretProvider) {
+ secretProvider.destroy();
+ secretProvider = null;
+ }
+ }
+
+ /**
+ * Returns the filtered configuration (only properties starting with the specified prefix). The property keys
+ * are also trimmed from the prefix. The returned {@link Properties} object is used to initialized the
+ * {@link AuthenticationHandler}.
+ *
+ * This method can be overriden by subclasses to obtain the configuration from other configuration source than
+ * the web.xml file.
+ *
+ * @param configPrefix configuration prefix to use for extracting configuration properties.
+ * @param filterConfig filter configuration object
+ *
+ * @return the configuration to be used with the {@link AuthenticationHandler} instance.
+ *
+ * @throws ServletException thrown if the configuration could not be created.
+ */
+ protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException {
+ Properties props = new Properties();
+ Enumeration> names = filterConfig.getInitParameterNames();
+ while (names.hasMoreElements()) {
+ String name = (String) names.nextElement();
+ if (name.startsWith(configPrefix)) {
+ String value = filterConfig.getInitParameter(name);
+ props.put(name.substring(configPrefix.length()), value);
+ }
+ }
+ return props;
+ }
+
+ /**
+ * Returns the full URL of the request including the query string.
+ *
+ * Used as a convenience method for logging purposes.
+ *
+ * @param request the request object.
+ *
+ * @return the full URL of the request including the query string.
+ */
+ protected String getRequestURL(HttpServletRequest request) {
+ StringBuffer sb = request.getRequestURL();
+ if (request.getQueryString() != null) {
+ sb.append("?").append(request.getQueryString());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns the {@link AuthenticationToken} for the request.
+ *
+ * It looks at the received HTTP cookies and extracts the value of the {@link AuthenticatedURL#AUTH_COOKIE}
+ * if present. It verifies the signature and if correct it creates the {@link AuthenticationToken} and returns
+ * it.
+ *
+ * If this method returns null the filter will invoke the configured {@link AuthenticationHandler}
+ * to perform user authentication.
+ *
+ * @param request request object.
+ *
+ * @return the Authentication token if the request is authenticated, null otherwise.
+ *
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if the token is invalid or if it has expired.
+ */
+ protected AuthenticationToken getToken(HttpServletRequest request) throws IOException, AuthenticationException {
+ AuthenticationToken token = null;
+ String tokenStr = null;
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(AuthenticatedURL.AUTH_COOKIE)) {
+ tokenStr = cookie.getValue();
+ if (tokenStr.isEmpty()) {
+ throw new AuthenticationException("Unauthorized access");
+ }
+ try {
+ tokenStr = signer.verifyAndExtract(tokenStr);
+ } catch (SignerException ex) {
+ throw new AuthenticationException(ex);
+ }
+ break;
+ }
+ }
+ }
+ if (tokenStr != null) {
+ token = AuthenticationToken.parse(tokenStr);
+ boolean match = verifyTokenType(getAuthenticationHandler(), token);
+ if (!match) {
+ throw new AuthenticationException("Invalid AuthenticationToken type");
+ }
+ if (token.isExpired()) {
+ throw new AuthenticationException("AuthenticationToken expired");
+ }
+ }
+ return token;
+ }
+
+ /**
+ * This method verifies if the specified token type matches one of the the
+ * token types supported by a specified {@link AuthenticationHandler}. This
+ * method is specifically designed to work with
+ * {@link CompositeAuthenticationHandler} implementation which supports
+ * multiple authentication schemes while the {@link AuthenticationHandler}
+ * interface supports a single type via
+ * {@linkplain AuthenticationHandler#getType()} method.
+ *
+ * @param handler The authentication handler whose supported token types
+ * should be used for verification.
+ * @param token The token whose type needs to be verified.
+ * @return true If the token type matches one of the supported token types
+ * false Otherwise
+ */
+ protected boolean verifyTokenType(AuthenticationHandler handler,
+ AuthenticationToken token) {
+ if(!(handler instanceof CompositeAuthenticationHandler)) {
+ return handler.getType().equals(token.getType());
+ }
+ boolean match = false;
+ Collection tokenTypes =
+ ((CompositeAuthenticationHandler) handler).getTokenTypes();
+ for (String tokenType : tokenTypes) {
+ if (tokenType.equals(token.getType())) {
+ match = true;
+ break;
+ }
+ }
+ return match;
+ }
+
+ /**
+ * If the request has a valid authentication token it allows the request to continue to the target resource,
+ * otherwise it triggers an authentication sequence using the configured {@link AuthenticationHandler}.
+ *
+ * @param request the request object.
+ * @param response the response object.
+ * @param filterChain the filter chain object.
+ *
+ * @throws IOException thrown if an IO error occurred.
+ * @throws ServletException thrown if a processing error occurred.
+ */
+ @Override
+ public void doFilter(ServletRequest request,
+ ServletResponse response,
+ FilterChain filterChain)
+ throws IOException, ServletException {
+ boolean unauthorizedResponse = true;
+ int errCode = HttpServletResponse.SC_UNAUTHORIZED;
+ AuthenticationException authenticationEx = null;
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ boolean isHttps = "https".equals(httpRequest.getScheme());
+ try {
+ boolean newToken = false;
+ AuthenticationToken token;
+ try {
+ token = getToken(httpRequest);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Got token {} from httpRequest {}", token,
+ getRequestURL(httpRequest));
+ }
+ }
+ catch (AuthenticationException ex) {
+ LOG.warn("AuthenticationToken ignored: " + ex.getMessage());
+ // will be sent back in a 401 unless filter authenticates
+ authenticationEx = ex;
+ token = null;
+ }
+ if (authHandler.managementOperation(token, httpRequest, httpResponse)) {
+ if (token == null) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Request [{}] triggering authentication. handler: {}",
+ getRequestURL(httpRequest), authHandler.getClass());
+ }
+ token = authHandler.authenticate(httpRequest, httpResponse);
+ if (token != null && token != AuthenticationToken.ANONYMOUS) {
+ if (token.getMaxInactives() > 0) {
+ token.setMaxInactives(System.currentTimeMillis()
+ + getMaxInactiveInterval() * 1000);
+ }
+ if (token.getExpires() != 0) {
+ token.setExpires(System.currentTimeMillis()
+ + getValidity() * 1000);
+ }
+ }
+ newToken = true;
+ }
+ if (token != null) {
+ unauthorizedResponse = false;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Request [{}] user [{}] authenticated",
+ getRequestURL(httpRequest), token.getUserName());
+ }
+ final AuthenticationToken authToken = token;
+ httpRequest = new HttpServletRequestWrapper(httpRequest) {
+
+ @Override
+ public String getAuthType() {
+ return authToken.getType();
+ }
+
+ @Override
+ public String getRemoteUser() {
+ return authToken.getUserName();
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return (authToken != AuthenticationToken.ANONYMOUS) ?
+ authToken : null;
+ }
+ };
+
+ // If cookie persistence is configured to false,
+ // it means the cookie will be a session cookie.
+ // If the token is an old one, renew the its maxInactiveInterval.
+ if (!newToken && !isCookiePersistent()
+ && getMaxInactiveInterval() > 0) {
+ token.setMaxInactives(System.currentTimeMillis()
+ + getMaxInactiveInterval() * 1000);
+ token.setExpires(token.getExpires());
+ newToken = true;
+ }
+ if (newToken && !token.isExpired()
+ && token != AuthenticationToken.ANONYMOUS) {
+ String signedToken = signer.sign(token.toString());
+ createAuthCookie(httpResponse, signedToken, getCookieDomain(),
+ getCookiePath(), token.getExpires(),
+ isCookiePersistent(), isHttps);
+ }
+ doFilter(filterChain, httpRequest, httpResponse);
+ }
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("managementOperation returned false for request {}."
+ + " token: {}", getRequestURL(httpRequest), token);
+ }
+ unauthorizedResponse = false;
+ }
+ } catch (AuthenticationException ex) {
+ // exception from the filter itself is fatal
+ errCode = HttpServletResponse.SC_FORBIDDEN;
+ authenticationEx = ex;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Authentication exception: " + ex.getMessage(), ex);
+ } else {
+ LOG.warn("Authentication exception: " + ex.getMessage());
+ }
+ }
+ if (unauthorizedResponse) {
+ if (!httpResponse.isCommitted()) {
+ createAuthCookie(httpResponse, "", getCookieDomain(),
+ getCookiePath(), 0, isCookiePersistent(), isHttps);
+ // If response code is 401. Then WWW-Authenticate Header should be
+ // present.. reset to 403 if not found..
+ if ((errCode == HttpServletResponse.SC_UNAUTHORIZED)
+ && (!httpResponse.containsHeader(
+ KerberosAuthenticator.WWW_AUTHENTICATE)
+ && !httpResponse.containsHeader(
+ KerberosAuthenticator.WWW_AUTHENTICATE.toLowerCase()))) {
+ errCode = HttpServletResponse.SC_FORBIDDEN;
+ }
+ // After Jetty 9.4.21, sendError() no longer allows a custom message.
+ // use setStatus() to set a custom message.
+ String reason;
+ if (authenticationEx == null) {
+ reason = "Authentication required";
+ } else {
+ reason = authenticationEx.getMessage();
+ }
+
+ httpResponse.setStatus(errCode, reason);
+ httpResponse.sendError(errCode, reason);
+ }
+ }
+ }
+
+ /**
+ * Delegates call to the servlet filter chain. Sub-classes my override this
+ * method to perform pre and post tasks.
+ *
+ * @param filterChain the filter chain object.
+ * @param request the request object.
+ * @param response the response object.
+ *
+ * @throws IOException thrown if an IO error occurred.
+ * @throws ServletException thrown if a processing error occurred.
+ */
+ protected void doFilter(FilterChain filterChain, HttpServletRequest request,
+ HttpServletResponse response) throws IOException, ServletException {
+ filterChain.doFilter(request, response);
+ }
+
+ /**
+ * Creates the Hadoop authentication HTTP cookie.
+ *
+ * @param resp the response object.
+ * @param token authentication token for the cookie.
+ * @param domain the cookie domain.
+ * @param path the cookie path.
+ * @param expires UNIX timestamp that indicates the expire date of the
+ * cookie. It has no effect if its value < 0.
+ * @param isSecure is the cookie secure?
+ * @param isCookiePersistent whether the cookie is persistent or not.
+ *
+ * XXX the following code duplicate some logic in Jetty / Servlet API,
+ * because of the fact that Hadoop is stuck at servlet 2.5 and jetty 6
+ * right now.
+ */
+ public static void createAuthCookie(HttpServletResponse resp, String token,
+ String domain, String path, long expires,
+ boolean isCookiePersistent,
+ boolean isSecure) {
+ StringBuilder sb = new StringBuilder(AuthenticatedURL.AUTH_COOKIE)
+ .append("=");
+ if (token != null && token.length() > 0) {
+ sb.append("\"").append(token).append("\"");
+ }
+
+ if (path != null) {
+ sb.append("; Path=").append(path);
+ }
+
+ if (domain != null) {
+ sb.append("; Domain=").append(domain);
+ }
+
+ if (expires >= 0 && isCookiePersistent) {
+ Date date = new Date(expires);
+ SimpleDateFormat df = new SimpleDateFormat("EEE, " +
+ "dd-MMM-yyyy HH:mm:ss zzz", Locale.US);
+ df.setTimeZone(TimeZone.getTimeZone("GMT"));
+ sb.append("; Expires=").append(df.format(date));
+ }
+
+ if (isSecure) {
+ sb.append("; Secure");
+ }
+
+ sb.append("; HttpOnly");
+ resp.addHeader("Set-Cookie", sb.toString());
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandler.java
new file mode 100644
index 000000000000..ca0edac772f0
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandler.java
@@ -0,0 +1,119 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.yetus.audience.InterfaceAudience;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Interface for server authentication mechanisms.
+ * The {@link AuthenticationFilter} manages the lifecycle of the authentication handler.
+ * Implementations must be thread-safe as one instance is initialized and used for all requests.
+ */
+@InterfaceAudience.Private
+public interface AuthenticationHandler {
+
+ String WWW_AUTHENTICATE = HttpConstants.WWW_AUTHENTICATE_HEADER;
+
+ /**
+ * Returns the authentication type of the authentication handler.
+ * This should be a name that uniquely identifies the authentication type.
+ * For example 'simple' or 'kerberos'.
+ *
+ * @return the authentication type of the authentication handler.
+ */
+ public String getType();
+
+ /**
+ * Initializes the authentication handler instance.
+ *
+ * This method is invoked by the {@link AuthenticationFilter#init} method.
+ *
+ * @param config configuration properties to initialize the handler.
+ *
+ * @throws ServletException thrown if the handler could not be initialized.
+ */
+ public void init(Properties config) throws ServletException;
+
+ /**
+ * Destroys the authentication handler instance.
+ *
+ * This method is invoked by the {@link AuthenticationFilter#destroy} method.
+ */
+ public void destroy();
+
+ /**
+ * Performs an authentication management operation.
+ *
+ * This is useful for handling operations like get/renew/cancel
+ * delegation tokens which are being handled as operations of the
+ * service end-point.
+ *
+ * If the method returns TRUE the request will continue normal
+ * processing, this means the method has not produced any HTTP response.
+ *
+ * If the method returns FALSE the request will end, this means
+ * the method has produced the corresponding HTTP response.
+ *
+ * @param token the authentication token if any, otherwise NULL.
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ * @return TRUE if the request should be processed as a regular
+ * request,
+ * FALSE otherwise.
+ *
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if an Authentication error occurred.
+ */
+ public boolean managementOperation(AuthenticationToken token,
+ HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, AuthenticationException;
+
+ /**
+ * Performs an authentication step for the given HTTP client request.
+ *
+ * This method is invoked by the {@link AuthenticationFilter} only if the HTTP client request is
+ * not yet authenticated.
+ *
+ * Depending upon the authentication mechanism being implemented, a particular HTTP client may
+ * end up making a sequence of invocations before authentication is successfully established (this is
+ * the case of Kerberos SPNEGO).
+ *
+ * This method must return an {@link AuthenticationToken} only if the the HTTP client request has
+ * been successfully and fully authenticated.
+ *
+ * If the HTTP client request has not been completely authenticated, this method must take over
+ * the corresponding HTTP response and it must return null.
+ *
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ *
+ * @return an {@link AuthenticationToken} if the HTTP client request has been authenticated,
+ * null otherwise (in this case it must take care of the response).
+ *
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if an Authentication error occurred.
+ */
+ public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, AuthenticationException;
+
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandlerUtil.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandlerUtil.java
new file mode 100644
index 000000000000..b234e2663972
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandlerUtil.java
@@ -0,0 +1,114 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import org.apache.yetus.audience.InterfaceAudience;
+
+import static org.apache.hadoop.hbase.security.authentication.server.HttpConstants.NEGOTIATE;
+import static org.apache.hadoop.hbase.security.authentication.server.HttpConstants.BASIC;
+import static org.apache.hadoop.hbase.security.authentication.server.HttpConstants.DIGEST;
+
+import java.util.Locale;
+
+/**
+ * This is a utility class designed to provide functionality related to
+ * {@link AuthenticationHandler}.
+ */
+@InterfaceAudience.Private
+public final class AuthenticationHandlerUtil {
+
+ /**
+ * This class should only contain the static utility methods. Hence it is not
+ * intended to be instantiated.
+ */
+ private AuthenticationHandlerUtil() {
+ }
+
+ /**
+ * This method provides an instance of {@link AuthenticationHandler} based on
+ * specified authHandlerName.
+ *
+ * @param authHandler The short-name (or fully qualified class name) of the
+ * authentication handler.
+ * @return an instance of AuthenticationHandler implementation.
+ */
+ public static String getAuthenticationHandlerClassName(String authHandler) {
+ if (authHandler == null) {
+ throw new NullPointerException();
+ }
+ String handlerName = authHandler.toLowerCase(Locale.ENGLISH);
+
+ String authHandlerClassName = null;
+
+ if (handlerName.equals(PseudoAuthenticationHandler.TYPE)) {
+ authHandlerClassName = PseudoAuthenticationHandler.class.getName();
+ } else if (handlerName.equals(KerberosAuthenticationHandler.TYPE)) {
+ authHandlerClassName = KerberosAuthenticationHandler.class.getName();
+ } else if (handlerName.equals(LdapAuthenticationHandler.TYPE)) {
+ authHandlerClassName = LdapAuthenticationHandler.class.getName();
+ } else if (handlerName.equals(MultiSchemeAuthenticationHandler.TYPE)) {
+ authHandlerClassName = MultiSchemeAuthenticationHandler.class.getName();
+ } else {
+ authHandlerClassName = authHandler;
+ }
+
+ return authHandlerClassName;
+ }
+
+ /**
+ * This method checks if the specified HTTP authentication scheme
+ * value is valid.
+ *
+ * @param scheme HTTP authentication scheme to be checked
+ * @return Canonical representation of HTTP authentication scheme
+ * @throws IllegalArgumentException In case the specified value is not a valid
+ * HTTP authentication scheme.
+ */
+ public static String checkAuthScheme(String scheme) {
+ if (BASIC.equalsIgnoreCase(scheme)) {
+ return BASIC;
+ } else if (NEGOTIATE.equalsIgnoreCase(scheme)) {
+ return NEGOTIATE;
+ } else if (DIGEST.equalsIgnoreCase(scheme)) {
+ return DIGEST;
+ }
+ throw new IllegalArgumentException(String.format(
+ "Unsupported HTTP authentication scheme %s ."
+ + " Supported schemes are [%s, %s, %s]", scheme, BASIC, NEGOTIATE,
+ DIGEST));
+ }
+
+ /**
+ * This method checks if the specified authToken belongs to the
+ * specified HTTP authentication scheme.
+ *
+ * @param scheme HTTP authentication scheme to be checked
+ * @param auth Authentication header value which is to be compared with the
+ * authentication scheme.
+ * @return true If the authentication header value corresponds to the
+ * specified authentication scheme false Otherwise.
+ */
+ public static boolean matchAuthScheme(String scheme, String auth) {
+ if (scheme == null) {
+ throw new NullPointerException();
+ }
+ scheme = scheme.trim();
+ if (auth == null) {
+ throw new NullPointerException();
+ }
+ auth = auth.trim();
+ return auth.regionMatches(true, 0, scheme, 0, scheme.length());
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationToken.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationToken.java
new file mode 100644
index 000000000000..176887da09cd
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationToken.java
@@ -0,0 +1,109 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.security.authentication.util.AuthToken;
+import org.apache.yetus.audience.InterfaceAudience;
+
+import java.security.Principal;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * The {@link AuthenticationToken} contains information about an authenticated
+ * HTTP client and doubles as the {@link Principal} to be returned by
+ * authenticated {@link HttpServletRequest}s
+ *
+ * The token can be serialized/deserialized to and from a string as it is sent
+ * and received in HTTP client responses and requests as a HTTP cookie (this is
+ * done by the {@link AuthenticationFilter}).
+ */
+@InterfaceAudience.Private
+public class AuthenticationToken extends AuthToken {
+
+ /**
+ * Constant that identifies an anonymous request.
+ */
+ public static final AuthenticationToken ANONYMOUS = new AuthenticationToken();
+
+ private AuthenticationToken() {
+ super();
+ }
+
+ private AuthenticationToken(AuthToken token) {
+ super(token.getUserName(), token.getName(), token.getType());
+ setMaxInactives(token.getMaxInactives());
+ setExpires(token.getExpires());
+ }
+
+ /**
+ * Creates an authentication token.
+ *
+ * @param userName user name.
+ * @param principal principal (commonly matches the user name, with Kerberos is the full/long principal
+ * name while the userName is the short name).
+ * @param type the authentication mechanism name.
+ * (System.currentTimeMillis() + validityPeriod).
+ */
+ public AuthenticationToken(String userName, String principal, String type) {
+ super(userName, principal, type);
+ }
+
+ /**
+ * Sets the max inactive time of the token.
+ *
+ * @param maxInactives inactive time of the token in milliseconds
+ * since the epoch.
+ */
+ public void setMaxInactives(long maxInactives) {
+ if (this != AuthenticationToken.ANONYMOUS) {
+ super.setMaxInactives(maxInactives);
+ }
+ }
+
+ /**
+ * Sets the expiration of the token.
+ *
+ * @param expires expiration time of the token in milliseconds since the epoch.
+ */
+ public void setExpires(long expires) {
+ if (this != AuthenticationToken.ANONYMOUS) {
+ super.setExpires(expires);
+ }
+ }
+
+ /**
+ * Returns true if the token has expired.
+ *
+ * @return true if the token has expired.
+ */
+ public boolean isExpired() {
+ return super.isExpired();
+ }
+
+ /**
+ * Parses a string into an authentication token.
+ *
+ * @param tokenStr string representation of a token.
+ *
+ * @return the parsed authentication token.
+ *
+ * @throws AuthenticationException thrown if the string representation could not be parsed into
+ * an authentication token.
+ */
+ public static AuthenticationToken parse(String tokenStr) throws AuthenticationException {
+ return new AuthenticationToken(AuthToken.parse(tokenStr));
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/CompositeAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/CompositeAuthenticationHandler.java
new file mode 100644
index 000000000000..00f7444f2914
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/CompositeAuthenticationHandler.java
@@ -0,0 +1,32 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import java.util.Collection;
+
+/**
+ * Interface to support multiple authentication mechanisms simultaneously.
+ *
+ */
+@InterfaceAudience.Private
+public interface CompositeAuthenticationHandler extends AuthenticationHandler {
+ /**
+ * This method returns the token types supported by this authentication
+ * handler.
+ *
+ * @return the token types supported by this authentication handler.
+ */
+ Collection getTokenTypes();
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/HttpConstants.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/HttpConstants.java
new file mode 100644
index 000000000000..516186cd7d58
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/HttpConstants.java
@@ -0,0 +1,58 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * This class defines constants used for HTTP protocol entities (such as
+ * headers, methods and their values).
+ */
+@InterfaceAudience.Private
+public final class HttpConstants {
+
+ /**
+ * This class defines the HTTP protocol constants. Hence it is not intended
+ * to be instantiated.
+ */
+ private HttpConstants() {
+ }
+
+ /**
+ * HTTP header used by the server endpoint during an authentication sequence.
+ */
+ public static final String WWW_AUTHENTICATE_HEADER = "WWW-Authenticate";
+
+ /**
+ * HTTP header used by the client endpoint during an authentication sequence.
+ */
+ public static final String AUTHORIZATION_HEADER = "Authorization";
+
+ /**
+ * HTTP header prefix used by the SPNEGO client/server endpoints during an
+ * authentication sequence.
+ */
+ public static final String NEGOTIATE = "Negotiate";
+
+ /**
+ * HTTP header prefix used during the Basic authentication sequence.
+ */
+ public static final String BASIC = "Basic";
+
+ /**
+ * HTTP header prefix used during the Basic authentication sequence.
+ */
+ public static final String DIGEST = "Digest";
+
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/JWTRedirectAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/JWTRedirectAuthenticationHandler.java
new file mode 100644
index 000000000000..550ecbc1d085
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/JWTRedirectAuthenticationHandler.java
@@ -0,0 +1,357 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import java.io.IOException;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Properties;
+import java.text.ParseException;
+
+import java.security.interfaces.RSAPublicKey;
+
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.hbase.security.authentication.util.CertificateUtil;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+
+/**
+ * The {@link JWTRedirectAuthenticationHandler} extends
+ * AltKerberosAuthenticationHandler to add WebSSO behavior for UIs. The expected
+ * SSO token is a JsonWebToken (JWT). The supported algorithm is RS256 which
+ * uses PKI between the token issuer and consumer. The flow requires a redirect
+ * to a configured authentication server URL and a subsequent request with the
+ * expected JWT token. This token is cryptographically verified and validated.
+ * The user identity is then extracted from the token and used to create an
+ * AuthenticationToken - as expected by the AuthenticationFilter.
+ *
+ *
+ * The supported configuration properties are:
+ *
+ *
+ *
authentication.provider.url: the full URL to the authentication server.
+ * This is the URL that the handler will redirect the browser to in order to
+ * authenticate the user. It does not have a default value.
+ *
public.key.pem: This is the PEM formatted public key of the issuer of the
+ * JWT token. It is required for verifying that the issuer is a trusted party.
+ * DO NOT include the PEM header and footer portions of the PEM encoded
+ * certificate. It does not have a default value.
+ *
expected.jwt.audiences: This is a list of strings that identify
+ * acceptable audiences for the JWT token. The audience is a way for the issuer
+ * to indicate what entity/s that the token is intended for. Default value is
+ * null which indicates that all audiences will be accepted.
+ *
jwt.cookie.name: the name of the cookie that contains the JWT token.
+ * Default value is "hadoop-jwt".
+ *
+ */
+@InterfaceAudience.Private
+public class JWTRedirectAuthenticationHandler extends
+ AltKerberosAuthenticationHandler {
+ private static Logger LOG = LoggerFactory
+ .getLogger(JWTRedirectAuthenticationHandler.class);
+
+ public static final String AUTHENTICATION_PROVIDER_URL =
+ "authentication.provider.url";
+ public static final String PUBLIC_KEY_PEM = "public.key.pem";
+ public static final String EXPECTED_JWT_AUDIENCES = "expected.jwt.audiences";
+ public static final String JWT_COOKIE_NAME = "jwt.cookie.name";
+ private static final String ORIGINAL_URL_QUERY_PARAM = "originalUrl=";
+ private String authenticationProviderUrl = null;
+ private RSAPublicKey publicKey = null;
+ private List audiences = null;
+ private String cookieName = "hadoop-jwt";
+
+ /**
+ * Primarily for testing, this provides a way to set the publicKey for
+ * signature verification without needing to get a PEM encoded value.
+ *
+ * @param pk publicKey for the token signtature verification
+ */
+ public void setPublicKey(RSAPublicKey pk) {
+ publicKey = pk;
+ }
+
+ /**
+ * Initializes the authentication handler instance.
+ *
+ * This method is invoked by the {@link AuthenticationFilter#init} method.
+ *
+ * @param config
+ * configuration properties to initialize the handler.
+ *
+ * @throws ServletException
+ * thrown if the handler could not be initialized.
+ */
+ @Override
+ public void init(Properties config) throws ServletException {
+ super.init(config);
+ // setup the URL to redirect to for authentication
+ authenticationProviderUrl = config
+ .getProperty(AUTHENTICATION_PROVIDER_URL);
+ if (authenticationProviderUrl == null) {
+ throw new ServletException(
+ "Authentication provider URL must not be null - configure: "
+ + AUTHENTICATION_PROVIDER_URL);
+ }
+
+ // setup the public key of the token issuer for verification
+ if (publicKey == null) {
+ String pemPublicKey = config.getProperty(PUBLIC_KEY_PEM);
+ if (pemPublicKey == null) {
+ throw new ServletException(
+ "Public key for signature validation must be provisioned.");
+ }
+ publicKey = CertificateUtil.parseRSAPublicKey(pemPublicKey);
+ }
+ // setup the list of valid audiences for token validation
+ String auds = config.getProperty(EXPECTED_JWT_AUDIENCES);
+ if (auds != null) {
+ // parse into the list
+ String[] audArray = auds.split(",");
+ audiences = new ArrayList();
+ for (String a : audArray) {
+ audiences.add(a);
+ }
+ }
+
+ // setup custom cookie name if configured
+ String customCookieName = config.getProperty(JWT_COOKIE_NAME);
+ if (customCookieName != null) {
+ cookieName = customCookieName;
+ }
+ }
+
+ @Override
+ public AuthenticationToken alternateAuthenticate(HttpServletRequest request,
+ HttpServletResponse response) throws IOException,
+ AuthenticationException {
+ AuthenticationToken token = null;
+
+ String serializedJWT = null;
+ HttpServletRequest req = (HttpServletRequest) request;
+ serializedJWT = getJWTFromCookie(req);
+ if (serializedJWT == null) {
+ String loginURL = constructLoginURL(request);
+ LOG.info("sending redirect to: " + loginURL);
+ ((HttpServletResponse) response).sendRedirect(loginURL);
+ } else {
+ String userName = null;
+ SignedJWT jwtToken = null;
+ boolean valid = false;
+ try {
+ jwtToken = SignedJWT.parse(serializedJWT);
+ valid = validateToken(jwtToken);
+ if (valid) {
+ userName = jwtToken.getJWTClaimsSet().getSubject();
+ LOG.info("USERNAME: " + userName);
+ } else {
+ LOG.warn("jwtToken failed validation: " + jwtToken.serialize());
+ }
+ } catch(ParseException pe) {
+ // unable to parse the token let's try and get another one
+ LOG.warn("Unable to parse the JWT token", pe);
+ }
+ if (valid) {
+ LOG.debug("Issuing AuthenticationToken for user.");
+ token = new AuthenticationToken(userName, userName, getType());
+ } else {
+ String loginURL = constructLoginURL(request);
+ LOG.info("token validation failed - sending redirect to: " + loginURL);
+ ((HttpServletResponse) response).sendRedirect(loginURL);
+ }
+ }
+ return token;
+ }
+
+ /**
+ * Encapsulate the acquisition of the JWT token from HTTP cookies within the
+ * request.
+ *
+ * @param req servlet request to get the JWT token from
+ * @return serialized JWT token
+ */
+ protected String getJWTFromCookie(HttpServletRequest req) {
+ String serializedJWT = null;
+ Cookie[] cookies = req.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookieName.equals(cookie.getName())) {
+ LOG.info(cookieName
+ + " cookie has been found and is being processed");
+ serializedJWT = cookie.getValue();
+ break;
+ }
+ }
+ }
+ return serializedJWT;
+ }
+
+ /**
+ * Create the URL to be used for authentication of the user in the absence of
+ * a JWT token within the incoming request.
+ *
+ * @param request for getting the original request URL
+ * @return url to use as login url for redirect
+ */
+ String constructLoginURL(HttpServletRequest request) {
+ String delimiter = "?";
+ if (authenticationProviderUrl.contains("?")) {
+ delimiter = "&";
+ }
+ String loginURL = authenticationProviderUrl + delimiter
+ + ORIGINAL_URL_QUERY_PARAM
+ + request.getRequestURL().toString() + getOriginalQueryString(request);
+ return loginURL;
+ }
+
+ private String getOriginalQueryString(HttpServletRequest request) {
+ String originalQueryString = request.getQueryString();
+ return (originalQueryString == null) ? "" : "?" + originalQueryString;
+ }
+
+ /**
+ * This method provides a single method for validating the JWT for use in
+ * request processing. It provides for the override of specific aspects of
+ * this implementation through submethods used within but also allows for the
+ * override of the entire token validation algorithm.
+ *
+ * @param jwtToken the token to validate
+ * @return true if valid
+ */
+ protected boolean validateToken(SignedJWT jwtToken) {
+ boolean sigValid = validateSignature(jwtToken);
+ if (!sigValid) {
+ LOG.warn("Signature could not be verified");
+ }
+ boolean audValid = validateAudiences(jwtToken);
+ if (!audValid) {
+ LOG.warn("Audience validation failed.");
+ }
+ boolean expValid = validateExpiration(jwtToken);
+ if (!expValid) {
+ LOG.info("Expiration validation failed.");
+ }
+
+ return sigValid && audValid && expValid;
+ }
+
+ /**
+ * Verify the signature of the JWT token in this method. This method depends
+ * on the public key that was established during init based upon the
+ * provisioned public key. Override this method in subclasses in order to
+ * customize the signature verification behavior.
+ *
+ * @param jwtToken the token that contains the signature to be validated
+ * @return valid true if signature verifies successfully; false otherwise
+ */
+ protected boolean validateSignature(SignedJWT jwtToken) {
+ boolean valid = false;
+ if (JWSObject.State.SIGNED == jwtToken.getState()) {
+ LOG.debug("JWT token is in a SIGNED state");
+ if (jwtToken.getSignature() != null) {
+ LOG.debug("JWT token signature is not null");
+ try {
+ JWSVerifier verifier = new RSASSAVerifier(publicKey);
+ if (jwtToken.verify(verifier)) {
+ valid = true;
+ LOG.debug("JWT token has been successfully verified");
+ } else {
+ LOG.warn("JWT signature verification failed.");
+ }
+ } catch (JOSEException je) {
+ LOG.warn("Error while validating signature", je);
+ }
+ }
+ }
+ return valid;
+ }
+
+ /**
+ * Validate whether any of the accepted audience claims is present in the
+ * issued token claims list for audience. Override this method in subclasses
+ * in order to customize the audience validation behavior.
+ *
+ * @param jwtToken
+ * the JWT token where the allowed audiences will be found
+ * @return true if an expected audience is present, otherwise false
+ */
+ protected boolean validateAudiences(SignedJWT jwtToken) {
+ boolean valid = false;
+ try {
+ List tokenAudienceList = jwtToken.getJWTClaimsSet()
+ .getAudience();
+ // if there were no expected audiences configured then just
+ // consider any audience acceptable
+ if (audiences == null) {
+ valid = true;
+ } else {
+ // if any of the configured audiences is found then consider it
+ // acceptable
+ boolean found = false;
+ for (String aud : tokenAudienceList) {
+ if (audiences.contains(aud)) {
+ LOG.debug("JWT token audience has been successfully validated");
+ valid = true;
+ break;
+ }
+ }
+ if (!valid) {
+ LOG.warn("JWT audience validation failed.");
+ }
+ }
+ } catch (ParseException pe) {
+ LOG.warn("Unable to parse the JWT token.", pe);
+ }
+ return valid;
+ }
+
+ /**
+ * Validate that the expiration time of the JWT token has not been violated.
+ * If it has then throw an AuthenticationException. Override this method in
+ * subclasses in order to customize the expiration validation behavior.
+ *
+ * @param jwtToken the token that contains the expiration date to validate
+ * @return valid true if the token has not expired; false otherwise
+ */
+ protected boolean validateExpiration(SignedJWT jwtToken) {
+ boolean valid = false;
+ try {
+ Date expires = jwtToken.getJWTClaimsSet().getExpirationTime();
+ if (expires == null || new Date().before(expires)) {
+ LOG.debug("JWT token expiration date has been "
+ + "successfully validated");
+ valid = true;
+ } else {
+ LOG.warn("JWT expiration date validation failed.");
+ }
+ } catch (ParseException pe) {
+ LOG.warn("JWT expiration date validation failed.", pe);
+ }
+ return valid;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/KerberosAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/KerberosAuthenticationHandler.java
new file mode 100644
index 000000000000..a9e8b168456d
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/KerberosAuthenticationHandler.java
@@ -0,0 +1,405 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.apache.hadoop.security.authentication.util.KerberosUtil;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.kerberos.KeyTab;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * The {@link KerberosAuthenticationHandler} implements the Kerberos SPNEGO
+ * authentication mechanism for HTTP.
+ *
+ * The supported configuration properties are:
+ *
+ *
kerberos.principal: the Kerberos principal to used by the server. As
+ * stated by the Kerberos SPNEGO specification, it should be
+ * HTTP/${HOSTNAME}@{REALM}. The realm can be omitted from the
+ * principal as the JDK GSS libraries will use the realm name of the configured
+ * default realm.
+ * It does not have a default value.
+ *
kerberos.keytab: the keytab file containing the credentials for the
+ * Kerberos principal.
+ * It does not have a default value.
+ *
kerberos.name.rules: kerberos names rules to resolve principal names, see
+ * {@link KerberosName#setRules(String)}
+ *
+ */
+@InterfaceAudience.Private
+public class KerberosAuthenticationHandler implements AuthenticationHandler {
+ public static final Logger LOG = LoggerFactory.getLogger(
+ KerberosAuthenticationHandler.class);
+
+ /**
+ * Constant that identifies the authentication mechanism.
+ */
+ public static final String TYPE = "kerberos";
+
+ /**
+ * Constant for the configuration property that indicates the kerberos
+ * principal.
+ */
+ public static final String PRINCIPAL = TYPE + ".principal";
+
+ /**
+ * Constant for the configuration property that indicates the keytab
+ * file path.
+ */
+ public static final String KEYTAB = TYPE + ".keytab";
+
+ /**
+ * Constant for the configuration property that indicates the Kerberos name
+ * rules for the Kerberos principals.
+ */
+ public static final String NAME_RULES = TYPE + ".name.rules";
+
+ /**
+ * Constant for the configuration property that indicates how auth_to_local
+ * rules are evaluated.
+ */
+ public static final String RULE_MECHANISM = TYPE + ".name.rules.mechanism";
+
+ /**
+ * Constant for the list of endpoints that skips Kerberos authentication.
+ */
+ static final String ENDPOINT_WHITELIST = TYPE + ".endpoint.whitelist";
+ private static final Pattern ENDPOINT_PATTERN = Pattern.compile("^/[\\w]+");
+
+ private String type;
+ private String keytab;
+ private GSSManager gssManager;
+ private Subject serverSubject = new Subject();
+ private final Collection whitelist = new HashSet<>();
+
+ /**
+ * Creates a Kerberos SPNEGO authentication handler with the default
+ * auth-token type, kerberos.
+ */
+ public KerberosAuthenticationHandler() {
+ this(TYPE);
+ }
+
+ /**
+ * Creates a Kerberos SPNEGO authentication handler with a custom auth-token
+ * type.
+ *
+ * @param type auth-token type.
+ */
+ public KerberosAuthenticationHandler(String type) {
+ this.type = type;
+ }
+
+ /**
+ * Initializes the authentication handler instance.
+ *
+ * It creates a Kerberos context using the principal and keytab specified in
+ * the configuration.
+ *
+ * This method is invoked by the {@link AuthenticationFilter#init} method.
+ *
+ * @param config configuration properties to initialize the handler.
+ *
+ * @throws ServletException thrown if the handler could not be initialized.
+ */
+ @Override
+ public void init(Properties config) throws ServletException {
+ try {
+ String principal = config.getProperty(PRINCIPAL);
+ if (principal == null || principal.trim().length() == 0) {
+ throw new ServletException("Principal not defined in configuration");
+ }
+ keytab = config.getProperty(KEYTAB, keytab);
+ if (keytab == null || keytab.trim().length() == 0) {
+ throw new ServletException("Keytab not defined in configuration");
+ }
+ File keytabFile = new File(keytab);
+ if (!keytabFile.exists()) {
+ throw new ServletException("Keytab does not exist: " + keytab);
+ }
+
+ // use all SPNEGO principals in the keytab if a principal isn't
+ // specifically configured
+ final String[] spnegoPrincipals;
+ if (principal.equals("*")) {
+ spnegoPrincipals = KerberosUtil.getPrincipalNames(
+ keytab, Pattern.compile("HTTP/.*"));
+ if (spnegoPrincipals.length == 0) {
+ throw new ServletException("Principals do not exist in the keytab");
+ }
+ } else {
+ spnegoPrincipals = new String[]{principal};
+ }
+ KeyTab keytabInstance = KeyTab.getInstance(keytabFile);
+ serverSubject.getPrivateCredentials().add(keytabInstance);
+ for (String spnegoPrincipal : spnegoPrincipals) {
+ Principal krbPrincipal = new KerberosPrincipal(spnegoPrincipal);
+ LOG.info("Using keytab {}, for principal {}",
+ keytab, krbPrincipal);
+ serverSubject.getPrincipals().add(krbPrincipal);
+ }
+ String nameRules = config.getProperty(NAME_RULES, null);
+ if (nameRules != null) {
+ KerberosName.setRules(nameRules);
+ }
+ String ruleMechanism = config.getProperty(RULE_MECHANISM, null);
+ if (ruleMechanism != null) {
+ KerberosName.setRuleMechanism(ruleMechanism);
+ }
+
+ final String whitelistStr = config.getProperty(ENDPOINT_WHITELIST, null);
+ if (whitelistStr != null) {
+ final String[] strs = whitelistStr.trim().split("\\s*[,\n]\\s*");
+ for (String s: strs) {
+ if (s.isEmpty()) continue;
+ if (ENDPOINT_PATTERN.matcher(s).matches()) {
+ whitelist.add(s);
+ } else {
+ throw new ServletException(
+ "The element of the whitelist: " + s + " must start with '/'"
+ + " and must not contain special characters afterwards");
+ }
+ }
+ }
+
+ try {
+ gssManager = Subject.doAs(serverSubject,
+ new PrivilegedExceptionAction() {
+ @Override
+ public GSSManager run() throws Exception {
+ return GSSManager.getInstance();
+ }
+ });
+ } catch (PrivilegedActionException ex) {
+ throw ex.getException();
+ }
+ } catch (Exception ex) {
+ throw new ServletException(ex);
+ }
+ }
+
+ /**
+ * Releases any resources initialized by the authentication handler.
+ *
+ * It destroys the Kerberos context.
+ */
+ @Override
+ public void destroy() {
+ keytab = null;
+ serverSubject = null;
+ }
+
+ /**
+ * Returns the authentication type of the authentication handler, 'kerberos'.
+ *
+ *
+ * @return the authentication type of the authentication handler, 'kerberos'.
+ */
+ @Override
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Returns the Kerberos principals used by the authentication handler.
+ *
+ * @return the Kerberos principals used by the authentication handler.
+ */
+ protected Set getPrincipals() {
+ return serverSubject.getPrincipals(KerberosPrincipal.class);
+ }
+
+ /**
+ * Returns the keytab used by the authentication handler.
+ *
+ * @return the keytab used by the authentication handler.
+ */
+ protected String getKeytab() {
+ return keytab;
+ }
+
+ /**
+ * This is an empty implementation, it always returns TRUE.
+ *
+ *
+ *
+ * @param token the authentication token if any, otherwise NULL.
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ *
+ * @return TRUE
+ * @throws IOException it is never thrown.
+ * @throws AuthenticationException it is never thrown.
+ */
+ @Override
+ public boolean managementOperation(AuthenticationToken token,
+ HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ return true;
+ }
+
+ /**
+ * It enforces the the Kerberos SPNEGO authentication sequence returning an
+ * {@link AuthenticationToken} only after the Kerberos SPNEGO sequence has
+ * completed successfully.
+ *
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ *
+ * @return an authentication token if the Kerberos SPNEGO sequence is complete
+ * and valid, null if it is in progress (in this case the handler
+ * handles the response to the client).
+ *
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if Kerberos SPNEGO sequence failed.
+ */
+ @Override
+ public AuthenticationToken authenticate(HttpServletRequest request,
+ final HttpServletResponse response)
+ throws IOException, AuthenticationException {
+
+ // If the request servlet path is in the whitelist,
+ // skip Kerberos authentication and return anonymous token.
+ final String path = request.getServletPath();
+ for(final String endpoint: whitelist) {
+ if (endpoint.equals(path)) {
+ return AuthenticationToken.ANONYMOUS;
+ }
+ }
+
+ AuthenticationToken token = null;
+ String authorization = request.getHeader(
+ KerberosAuthenticator.AUTHORIZATION);
+
+ if (authorization == null
+ || !authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) {
+ response.setHeader(WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE);
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ if (authorization == null) {
+ LOG.trace("SPNEGO starting for url: {}", request.getRequestURL());
+ } else {
+ LOG.warn("'" + KerberosAuthenticator.AUTHORIZATION +
+ "' does not start with '" +
+ KerberosAuthenticator.NEGOTIATE + "' : {}", authorization);
+ }
+ } else {
+ authorization = authorization.substring(
+ KerberosAuthenticator.NEGOTIATE.length()).trim();
+ final Base64 base64 = new Base64(0);
+ final byte[] clientToken = base64.decode(authorization);
+ try {
+ final String serverPrincipal =
+ KerberosUtil.getTokenServerName(clientToken);
+ if (!serverPrincipal.startsWith("HTTP/")) {
+ throw new IllegalArgumentException(
+ "Invalid server principal " + serverPrincipal +
+ "decoded from client request");
+ }
+ token = Subject.doAs(serverSubject,
+ new PrivilegedExceptionAction() {
+ @Override
+ public AuthenticationToken run() throws Exception {
+ return runWithPrincipal(serverPrincipal, clientToken,
+ base64, response);
+ }
+ });
+ } catch (PrivilegedActionException ex) {
+ if (ex.getException() instanceof IOException) {
+ throw (IOException) ex.getException();
+ } else {
+ throw new AuthenticationException(ex.getException());
+ }
+ } catch (Exception ex) {
+ throw new AuthenticationException(ex);
+ }
+ }
+ return token;
+ }
+
+ private AuthenticationToken runWithPrincipal(String serverPrincipal,
+ byte[] clientToken, Base64 base64, HttpServletResponse response) throws
+ IOException, GSSException {
+ GSSContext gssContext = null;
+ GSSCredential gssCreds = null;
+ AuthenticationToken token = null;
+ try {
+ LOG.trace("SPNEGO initiated with server principal [{}]", serverPrincipal);
+ gssCreds = this.gssManager.createCredential(
+ this.gssManager.createName(serverPrincipal,
+ KerberosUtil.NT_GSS_KRB5_PRINCIPAL_OID),
+ GSSCredential.INDEFINITE_LIFETIME,
+ new Oid[]{
+ KerberosUtil.GSS_SPNEGO_MECH_OID,
+ KerberosUtil.GSS_KRB5_MECH_OID },
+ GSSCredential.ACCEPT_ONLY);
+ gssContext = this.gssManager.createContext(gssCreds);
+ byte[] serverToken = gssContext.acceptSecContext(clientToken, 0,
+ clientToken.length);
+ if (serverToken != null && serverToken.length > 0) {
+ String authenticate = base64.encodeToString(serverToken);
+ response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE,
+ KerberosAuthenticator.NEGOTIATE + " " +
+ authenticate);
+ }
+ if (!gssContext.isEstablished()) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ LOG.trace("SPNEGO in progress");
+ } else {
+ String clientPrincipal = gssContext.getSrcName().toString();
+ KerberosName kerberosName = new KerberosName(clientPrincipal);
+ String userName = kerberosName.getShortName();
+ token = new AuthenticationToken(userName, clientPrincipal, getType());
+ response.setStatus(HttpServletResponse.SC_OK);
+ LOG.trace("SPNEGO completed for client principal [{}]",
+ clientPrincipal);
+ }
+ } finally {
+ if (gssContext != null) {
+ gssContext.dispose();
+ }
+ if (gssCreds != null) {
+ gssCreds.dispose();
+ }
+ }
+ return token;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/LdapAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/LdapAuthenticationHandler.java
new file mode 100644
index 000000000000..dff8b5a09a2a
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/LdapAuthenticationHandler.java
@@ -0,0 +1,338 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Hashtable;
+import java.util.Properties;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.StartTlsRequest;
+import javax.naming.ldap.StartTlsResponse;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link LdapAuthenticationHandler} implements the BASIC authentication
+ * mechanism for HTTP using LDAP back-end.
+ *
+ * The supported configuration properties are:
+ *
+ *
ldap.providerurl: The url of the LDAP server. It does not have a default
+ * value.
+ *
ldap.basedn: the base distinguished name (DN) to be used with the LDAP
+ * server. This value is appended to the provided user id for authentication
+ * purpose. It does not have a default value.
+ *
ldap.binddomain: the LDAP bind domain value to be used with the LDAP
+ * server. This property is optional and useful only in case of Active
+ * Directory server.
+ *
ldap.enablestarttls: A boolean value used to define if the LDAP server
+ * supports 'StartTLS' extension.
+ *
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public class LdapAuthenticationHandler implements AuthenticationHandler {
+ private static Logger logger = LoggerFactory
+ .getLogger(LdapAuthenticationHandler.class);
+
+ /**
+ * Constant that identifies the authentication mechanism.
+ */
+ public static final String TYPE = "ldap";
+
+ /**
+ * Constant that identifies the authentication mechanism to be used with the
+ * LDAP server.
+ */
+ public static final String SECURITY_AUTHENTICATION = "simple";
+
+ /**
+ * Constant for the configuration property that indicates the url of the LDAP
+ * server.
+ */
+ public static final String PROVIDER_URL = TYPE + ".providerurl";
+
+ /**
+ * Constant for the configuration property that indicates the base
+ * distinguished name (DN) to be used with the LDAP server. This value is
+ * appended to the provided user id for authentication purpose.
+ */
+ public static final String BASE_DN = TYPE + ".basedn";
+
+ /**
+ * Constant for the configuration property that indicates the LDAP bind
+ * domain value to be used with the LDAP server.
+ */
+ public static final String LDAP_BIND_DOMAIN = TYPE + ".binddomain";
+
+ /**
+ * Constant for the configuration property that indicates whether
+ * the LDAP server supports 'StartTLS' extension.
+ */
+ public static final String ENABLE_START_TLS = TYPE + ".enablestarttls";
+
+ private String ldapDomain;
+ private String baseDN;
+ private String providerUrl;
+ private Boolean enableStartTls;
+ private Boolean disableHostNameVerification;
+
+ /**
+ * Configure StartTLS LDAP extension for this handler.
+ *
+ * @param enableStartTls true If the StartTLS LDAP extension is to be enabled
+ * false otherwise
+ */
+ public void setEnableStartTls(Boolean enableStartTls) {
+ this.enableStartTls = enableStartTls;
+ }
+
+ /**
+ * Configure the Host name verification for this handler. This method is
+ * introduced only for unit testing and should never be used in production.
+ *
+ * @param disableHostNameVerification true to disable host-name verification
+ * false otherwise
+ */
+ public void setDisableHostNameVerification(
+ Boolean disableHostNameVerification) {
+ this.disableHostNameVerification = disableHostNameVerification;
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+
+ @Override
+ public void init(Properties config) throws ServletException {
+ this.baseDN = config.getProperty(BASE_DN);
+ this.providerUrl = config.getProperty(PROVIDER_URL);
+ this.ldapDomain = config.getProperty(LDAP_BIND_DOMAIN);
+ this.enableStartTls =
+ Boolean.valueOf(config.getProperty(ENABLE_START_TLS, "false"));
+
+ if (this.providerUrl == null) {
+ throw new NullPointerException("The LDAP URI can not be null");
+ }
+ if (!((this.baseDN == null)
+ ^ (this.ldapDomain == null))) {
+ throw new IllegalArgumentException(
+ "Either LDAP base DN or LDAP domain value needs to be specified");
+ }
+ if (this.enableStartTls) {
+ String tmp = this.providerUrl.toLowerCase();
+ if (tmp.startsWith("ldaps")) {
+ throw new IllegalArgumentException(
+ "Can not use ldaps and StartTLS option at the same time");
+ }
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public boolean managementOperation(AuthenticationToken token,
+ HttpServletRequest request, HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ return true;
+ }
+
+ @Override
+ public AuthenticationToken authenticate(HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ AuthenticationToken token = null;
+ String authorization =
+ request.getHeader(HttpConstants.AUTHORIZATION_HEADER);
+
+ if (authorization == null
+ || !AuthenticationHandlerUtil.matchAuthScheme(HttpConstants.BASIC,
+ authorization)) {
+ response.setHeader(WWW_AUTHENTICATE, HttpConstants.BASIC);
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ if (authorization == null) {
+ logger.trace("Basic auth starting");
+ } else {
+ logger.warn("'" + HttpConstants.AUTHORIZATION_HEADER
+ + "' does not start with '" + HttpConstants.BASIC + "' : {}",
+ authorization);
+ }
+ } else {
+ authorization =
+ authorization.substring(HttpConstants.BASIC.length()).trim();
+ final Base64 base64 = new Base64(0);
+ // As per RFC7617, UTF-8 charset should be used for decoding.
+ String[] credentials = new String(base64.decode(authorization),
+ StandardCharsets.UTF_8).split(":", 2);
+ if (credentials.length == 2) {
+ token = authenticateUser(credentials[0], credentials[1]);
+ response.setStatus(HttpServletResponse.SC_OK);
+ }
+ }
+ return token;
+ }
+
+ private AuthenticationToken authenticateUser(String userName,
+ String password) throws AuthenticationException {
+ if (userName == null || userName.isEmpty()) {
+ throw new AuthenticationException("Error validating LDAP user:"
+ + " a null or blank username has been provided");
+ }
+
+ // If the domain is available in the config, then append it unless domain
+ // is already part of the username. LDAP providers like Active Directory
+ // use a fully qualified user name like foo@bar.com.
+ if (!hasDomain(userName) && ldapDomain != null) {
+ userName = userName + "@" + ldapDomain;
+ }
+
+ if (password == null || password.isEmpty() ||
+ password.getBytes(StandardCharsets.UTF_8)[0] == 0) {
+ throw new AuthenticationException("Error validating LDAP user:"
+ + " a null or blank password has been provided");
+ }
+
+ // setup the security principal
+ String bindDN;
+ if (baseDN == null) {
+ bindDN = userName;
+ } else {
+ bindDN = "uid=" + userName + "," + baseDN;
+ }
+
+ if (this.enableStartTls) {
+ authenticateWithTlsExtension(bindDN, password);
+ } else {
+ authenticateWithoutTlsExtension(bindDN, password);
+ }
+
+ return new AuthenticationToken(userName, userName, TYPE);
+ }
+
+ private void authenticateWithTlsExtension(String userDN, String password)
+ throws AuthenticationException {
+ LdapContext ctx = null;
+ Hashtable env = new Hashtable();
+ env.put(Context.INITIAL_CONTEXT_FACTORY,
+ "com.sun.jndi.ldap.LdapCtxFactory");
+ env.put(Context.PROVIDER_URL, providerUrl);
+
+ try {
+ // Create initial context
+ ctx = new InitialLdapContext(env, null);
+ // Establish TLS session
+ StartTlsResponse tls =
+ (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
+
+ if (disableHostNameVerification) {
+ tls.setHostnameVerifier(new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ });
+ }
+
+ tls.negotiate();
+
+ // Initialize security credentials & perform read operation for
+ // verification.
+ ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION,
+ SECURITY_AUTHENTICATION);
+ ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN);
+ ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
+ ctx.lookup(userDN);
+ logger.debug("Authentication successful for {}", userDN);
+
+ } catch (NamingException | IOException ex) {
+ throw new AuthenticationException("Error validating LDAP user", ex);
+ } finally {
+ if (ctx != null) {
+ try {
+ ctx.close();
+ } catch (NamingException e) { /* Ignore. */
+ }
+ }
+ }
+ }
+
+ private void authenticateWithoutTlsExtension(String userDN, String password)
+ throws AuthenticationException {
+ Hashtable env = new Hashtable();
+ env.put(Context.INITIAL_CONTEXT_FACTORY,
+ "com.sun.jndi.ldap.LdapCtxFactory");
+ env.put(Context.PROVIDER_URL, providerUrl);
+ env.put(Context.SECURITY_AUTHENTICATION, SECURITY_AUTHENTICATION);
+ env.put(Context.SECURITY_PRINCIPAL, userDN);
+ env.put(Context.SECURITY_CREDENTIALS, password);
+
+ try {
+ // Create initial context
+ Context ctx = new InitialDirContext(env);
+ ctx.close();
+ logger.debug("Authentication successful for {}", userDN);
+
+ } catch (NamingException e) {
+ throw new AuthenticationException("Error validating LDAP user", e);
+ }
+ }
+
+ private static boolean hasDomain(String userName) {
+ return (indexOfDomainMatch(userName) > 0);
+ }
+
+ /*
+ * Get the index separating the user name from domain name (the user's name
+ * up to the first '/' or '@').
+ *
+ * @param userName full user name.
+ *
+ * @return index of domain match or -1 if not found
+ */
+ private static int indexOfDomainMatch(String userName) {
+ if (userName == null) {
+ return -1;
+ }
+
+ int idx = userName.indexOf('/');
+ int idx2 = userName.indexOf('@');
+ int endIdx = Math.min(idx, idx2); // Use the earlier match.
+ // Unless at least one of '/' or '@' was not found, in
+ // which case, user the latter match.
+ if (endIdx == -1) {
+ endIdx = Math.max(idx, idx2);
+ }
+ return endIdx;
+ }
+
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/MultiSchemeAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/MultiSchemeAuthenticationHandler.java
new file mode 100644
index 000000000000..1ef996d82fc2
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/MultiSchemeAuthenticationHandler.java
@@ -0,0 +1,214 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.hbase.thirdparty.com.google.common.base.Splitter;
+
+/**
+ * The {@link MultiSchemeAuthenticationHandler} supports configuring multiple
+ * authentication mechanisms simultaneously. e.g. server can support multiple
+ * authentication mechanisms such as Kerberos (SPENGO) and LDAP. During the
+ * authentication phase, server will specify all possible authentication schemes
+ * and let client choose the appropriate scheme. Please refer to RFC-2616 and
+ * HADOOP-12082 for more details.
+ *
+ * The supported configuration properties are:
+ *
+ *
multi-scheme-auth-handler.schemes: A comma separated list of HTTP
+ * authentication mechanisms supported by this handler. It does not have a
+ * default value. e.g. multi-scheme-auth-handler.schemes=basic,negotiate
+ *
multi-scheme-auth-handler.schemes.${scheme-name}.handler: The
+ * authentication handler implementation to be used for the specified
+ * authentication scheme. It does not have a default value. e.g.
+ * multi-scheme-auth-handler.schemes.negotiate.handler=kerberos
+ *
+ *
+ * It expected that for every authentication scheme specified in
+ * multi-scheme-auth-handler.schemes property, a handler needs to be configured.
+ * Note that while scheme values in 'multi-scheme-auth-handler.schemes' property
+ * are case-insensitive, the scheme value in the handler configuration property
+ * name must be lower case. i.e. property name such as
+ * multi-scheme-auth-handler.schemes.Negotiate.handler is invalid.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public class MultiSchemeAuthenticationHandler implements
+ CompositeAuthenticationHandler {
+ private static Logger logger = LoggerFactory
+ .getLogger(MultiSchemeAuthenticationHandler.class);
+ public static final String SCHEMES_PROPERTY =
+ "multi-scheme-auth-handler.schemes";
+ public static final String AUTH_HANDLER_PROPERTY =
+ "multi-scheme-auth-handler.schemes.%s.handler";
+ private static final Splitter STR_SPLITTER = Splitter.on(',').trimResults()
+ .omitEmptyStrings();
+
+ private final Map schemeToAuthHandlerMapping =
+ new HashMap<>();
+ private final Collection types = new HashSet<>();
+ private final String authType;
+
+ /**
+ * Constant that identifies the authentication mechanism.
+ */
+ public static final String TYPE = "multi-scheme";
+
+ public MultiSchemeAuthenticationHandler() {
+ this(TYPE);
+ }
+
+ public MultiSchemeAuthenticationHandler(String authType) {
+ this.authType = authType;
+ }
+
+ @Override
+ public String getType() {
+ return authType;
+ }
+
+ /**
+ * This method returns the token types supported by this authentication
+ * handler.
+ *
+ * @return the token types supported by this authentication handler.
+ */
+ @Override
+ public Collection getTokenTypes() {
+ return types;
+ }
+
+ @Override
+ public void init(Properties config) throws ServletException {
+ // Useful for debugging purpose.
+ for (Map.Entry prop : config.entrySet()) {
+ logger.info("{} : {}", prop.getKey(), prop.getValue());
+ }
+
+ this.types.clear();
+ if (config.getProperty(SCHEMES_PROPERTY) == null) {
+ throw new NullPointerException(SCHEMES_PROPERTY + " system property is not specified.");
+ }
+ String schemesProperty = config.getProperty(SCHEMES_PROPERTY);
+ for (String scheme : STR_SPLITTER.split(schemesProperty)) {
+ scheme = AuthenticationHandlerUtil.checkAuthScheme(scheme);
+ if (schemeToAuthHandlerMapping.containsKey(scheme)) {
+ throw new IllegalArgumentException("Handler is already specified for "
+ + scheme + " authentication scheme.");
+ }
+
+ String authHandlerPropName =
+ String.format(AUTH_HANDLER_PROPERTY, scheme).toLowerCase();
+ String authHandlerName = config.getProperty(authHandlerPropName);
+ if (authHandlerName == null) {
+ throw new NullPointerException(
+ "No auth handler configured for scheme " + scheme);
+ }
+
+ String authHandlerClassName =
+ AuthenticationHandlerUtil
+ .getAuthenticationHandlerClassName(authHandlerName);
+ AuthenticationHandler handler =
+ initializeAuthHandler(authHandlerClassName, config);
+ schemeToAuthHandlerMapping.put(scheme, handler);
+ types.add(handler.getType());
+ }
+ logger.info("Successfully initialized MultiSchemeAuthenticationHandler");
+ }
+
+ protected AuthenticationHandler initializeAuthHandler(
+ String authHandlerClassName, Properties config) throws ServletException {
+ try {
+ if (authHandlerClassName == null) {
+ throw new NullPointerException();
+ }
+ logger.debug("Initializing Authentication handler of type "
+ + authHandlerClassName);
+ Class> klass =
+ Thread.currentThread().getContextClassLoader()
+ .loadClass(authHandlerClassName);
+ AuthenticationHandler authHandler =
+ (AuthenticationHandler) klass.getDeclaredConstructor().newInstance();
+ authHandler.init(config);
+ logger.info("Successfully initialized Authentication handler of type "
+ + authHandlerClassName);
+ return authHandler;
+ } catch (ClassNotFoundException | InstantiationException
+ | IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) {
+ logger.error("Failed to initialize authentication handler "
+ + authHandlerClassName, ex);
+ throw new ServletException(ex);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ for (AuthenticationHandler handler : schemeToAuthHandlerMapping.values()) {
+ handler.destroy();
+ }
+ }
+
+ @Override
+ public boolean managementOperation(AuthenticationToken token,
+ HttpServletRequest request, HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ return true;
+ }
+
+ @Override
+ public AuthenticationToken authenticate(HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ String authorization =
+ request.getHeader(HttpConstants.AUTHORIZATION_HEADER);
+ if (authorization != null) {
+ for (Map.Entry entry :
+ schemeToAuthHandlerMapping.entrySet()) {
+ if (AuthenticationHandlerUtil.matchAuthScheme(
+ entry.getKey(), authorization)) {
+ AuthenticationToken token =
+ entry.getValue().authenticate(request, response);
+ logger.trace("Token generated with type {}", token.getType());
+ return token;
+ }
+ }
+ }
+
+ // Handle the case when (authorization == null) or an invalid authorization
+ // header (e.g. a header value without the scheme name).
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ for (String scheme : schemeToAuthHandlerMapping.keySet()) {
+ response.addHeader(HttpConstants.WWW_AUTHENTICATE_HEADER, scheme);
+ }
+
+ return null;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/PseudoAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/PseudoAuthenticationHandler.java
new file mode 100644
index 000000000000..c927aa122c4e
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/PseudoAuthenticationHandler.java
@@ -0,0 +1,200 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.security.authentication.client.PseudoAuthenticator;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.NameValuePair;
+import org.apache.yetus.audience.InterfaceAudience;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * The PseudoAuthenticationHandler provides a pseudo authentication mechanism that accepts
+ * the user name specified as a query string parameter.
+ *
+ * This mimics the model of Hadoop Simple authentication which trust the 'user.name' property provided in
+ * the configuration object.
+ *
+ * This handler can be configured to support anonymous users.
+ *
+ * The only supported configuration property is:
+ *
+ *
simple.anonymous.allowed: true|false, default value is false
+ *
+ */
+@InterfaceAudience.Private
+public class PseudoAuthenticationHandler implements AuthenticationHandler {
+
+ /**
+ * Constant that identifies the authentication mechanism.
+ */
+ public static final String TYPE = "simple";
+
+ /**
+ * Constant for the configuration property that indicates if anonymous users are allowed.
+ */
+ public static final String ANONYMOUS_ALLOWED = TYPE + ".anonymous.allowed";
+
+ private static final String PSEUDO_AUTH = "PseudoAuth";
+
+ private boolean acceptAnonymous;
+ private String type;
+
+ /**
+ * Creates a Hadoop pseudo authentication handler with the default auth-token
+ * type, simple.
+ */
+ public PseudoAuthenticationHandler() {
+ this(TYPE);
+ }
+
+ /**
+ * Creates a Hadoop pseudo authentication handler with a custom auth-token
+ * type.
+ *
+ * @param type auth-token type.
+ */
+ public PseudoAuthenticationHandler(String type) {
+ this.type = type;
+ }
+
+ /**
+ * Initializes the authentication handler instance.
+ *
+ * This method is invoked by the {@link AuthenticationFilter#init} method.
+ *
+ * @param config configuration properties to initialize the handler.
+ *
+ * @throws ServletException thrown if the handler could not be initialized.
+ */
+ @Override
+ public void init(Properties config) throws ServletException {
+ acceptAnonymous = Boolean.parseBoolean(config.getProperty(ANONYMOUS_ALLOWED, "false"));
+ }
+
+ /**
+ * Returns if the handler is configured to support anonymous users.
+ *
+ * @return if the handler is configured to support anonymous users.
+ */
+ protected boolean getAcceptAnonymous() {
+ return acceptAnonymous;
+ }
+
+ /**
+ * Releases any resources initialized by the authentication handler.
+ *
+ * This implementation does a NOP.
+ */
+ @Override
+ public void destroy() {
+ }
+
+ /**
+ * Returns the authentication type of the authentication handler, 'simple'.
+ *
+ * @return the authentication type of the authentication handler, 'simple'.
+ */
+ @Override
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * This is an empty implementation, it always returns TRUE.
+ *
+ *
+ *
+ * @param token the authentication token if any, otherwise NULL.
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ *
+ * @return TRUE
+ * @throws IOException it is never thrown.
+ * @throws AuthenticationException it is never thrown.
+ */
+ @Override
+ public boolean managementOperation(AuthenticationToken token,
+ HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ return true;
+ }
+
+ private String getUserName(HttpServletRequest request) {
+ String queryString = request.getQueryString();
+ if(queryString == null || queryString.length() == 0) {
+ return null;
+ }
+ List list = URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8);
+ if (list != null) {
+ for (NameValuePair nv : list) {
+ if (PseudoAuthenticator.USER_NAME.equals(nv.getName())) {
+ return nv.getValue();
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Authenticates an HTTP client request.
+ *
+ * It extracts the {@link PseudoAuthenticator#USER_NAME} parameter from the query string and creates
+ * an {@link AuthenticationToken} with it.
+ *
+ * If the HTTP client request does not contain the {@link PseudoAuthenticator#USER_NAME} parameter and
+ * the handler is configured to allow anonymous users it returns the {@link AuthenticationToken#ANONYMOUS}
+ * token.
+ *
+ * If the HTTP client request does not contain the {@link PseudoAuthenticator#USER_NAME} parameter and
+ * the handler is configured to disallow anonymous users it throws an {@link AuthenticationException}.
+ *
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ *
+ * @return an authentication token if the HTTP client request is accepted and credentials are valid.
+ *
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if HTTP client request was not accepted as an authentication request.
+ */
+ @Override
+ public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ AuthenticationToken token;
+ String userName = getUserName(request);
+ if (userName == null) {
+ if (getAcceptAnonymous()) {
+ token = AuthenticationToken.ANONYMOUS;
+ } else {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ response.setHeader(WWW_AUTHENTICATE, PSEUDO_AUTH);
+ token = null;
+ }
+ } else {
+ token = new AuthenticationToken(userName, userName, getType());
+ }
+ return token;
+ }
+
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/CertificateUtil.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/CertificateUtil.java
new file mode 100644
index 000000000000..c7337f69cb24
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/CertificateUtil.java
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.hadoop.hbase.security.authentication.util;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+
+import javax.servlet.ServletException;
+
+@InterfaceAudience.Private
+public class CertificateUtil {
+ private static final String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n";
+ private static final String PEM_FOOTER = "\n-----END CERTIFICATE-----";
+
+ /**
+ * Gets an RSAPublicKey from the provided PEM encoding.
+ *
+ * @param pem
+ * - the pem encoding from config without the header and footer
+ * @return RSAPublicKey the RSA public key
+ * @throws ServletException thrown if a processing error occurred
+ */
+ public static RSAPublicKey parseRSAPublicKey(String pem) throws ServletException {
+ String fullPem = PEM_HEADER + pem + PEM_FOOTER;
+ PublicKey key = null;
+ try {
+ CertificateFactory fact = CertificateFactory.getInstance("X.509");
+ ByteArrayInputStream is = new ByteArrayInputStream(
+ fullPem.getBytes(StandardCharsets.UTF_8));
+
+ X509Certificate cer = (X509Certificate) fact.generateCertificate(is);
+ key = cer.getPublicKey();
+ } catch (CertificateException ce) {
+ String message = null;
+ if (pem.startsWith(PEM_HEADER)) {
+ message = "CertificateException - be sure not to include PEM header "
+ + "and footer in the PEM configuration element.";
+ } else {
+ message = "CertificateException - PEM may be corrupt";
+ }
+ throw new ServletException(message, ce);
+ }
+ return (RSAPublicKey) key;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/FileSignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/FileSignerSecretProvider.java
new file mode 100644
index 000000000000..3c79ea10f514
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/FileSignerSecretProvider.java
@@ -0,0 +1,79 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter;
+
+import javax.servlet.ServletContext;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Properties;
+
+/**
+ * A SignerSecretProvider that simply loads a secret from a specified file.
+ */
+@InterfaceStability.Unstable
+@InterfaceAudience.Private
+public class FileSignerSecretProvider extends SignerSecretProvider {
+
+ private byte[] secret;
+ private byte[][] secrets;
+
+ public FileSignerSecretProvider() {}
+
+ @Override
+ public void init(Properties config, ServletContext servletContext,
+ long tokenValidity) throws Exception {
+
+ String signatureSecretFile = config.getProperty(
+ AuthenticationFilter.SIGNATURE_SECRET_FILE, null);
+
+ if (signatureSecretFile != null) {
+ try (Reader reader = new InputStreamReader(Files.newInputStream(
+ Paths.get(signatureSecretFile)), StandardCharsets.UTF_8)) {
+ StringBuilder sb = new StringBuilder();
+ int c = reader.read();
+ while (c > -1) {
+ sb.append((char) c);
+ c = reader.read();
+ }
+
+ secret = sb.toString().getBytes(StandardCharsets.UTF_8);
+ if (secret.length == 0) {
+ throw new RuntimeException("No secret in signature secret file: "
+ + signatureSecretFile);
+ }
+ } catch (IOException ex) {
+ throw new RuntimeException("Could not read signature secret file: " +
+ signatureSecretFile);
+ }
+ }
+
+ secrets = new byte[][]{secret};
+ }
+
+ @Override
+ public byte[] getCurrentSecret() {
+ return secret;
+ }
+
+ @Override
+ public byte[][] getAllSecrets() {
+ return secrets;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/JaasConfiguration.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/JaasConfiguration.java
new file mode 100644
index 000000000000..1505e56880f2
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/JaasConfiguration.java
@@ -0,0 +1,78 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import java.util.HashMap;
+import java.util.Map;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+
+/**
+ * Creates a programmatic version of a jaas.conf file. This can be used
+ * instead of writing a jaas.conf file and setting the system property,
+ * "java.security.auth.login.config", to point to that file. It is meant to be
+ * used for connecting to ZooKeeper.
+ */
+@InterfaceAudience.Private
+public class JaasConfiguration extends Configuration {
+
+ private final javax.security.auth.login.Configuration baseConfig =
+ javax.security.auth.login.Configuration.getConfiguration();
+ private final AppConfigurationEntry[] entry;
+ private final String entryName;
+
+ /**
+ * Add an entry to the jaas configuration with the passed in name,
+ * principal, and keytab. The other necessary options will be set for you.
+ *
+ * @param entryName The name of the entry (e.g. "Client")
+ * @param principal The principal of the user
+ * @param keytab The location of the keytab
+ */
+ public JaasConfiguration(String entryName, String principal, String keytab) {
+ this.entryName = entryName;
+ Map options = new HashMap<>();
+ options.put("keyTab", keytab);
+ options.put("principal", principal);
+ options.put("useKeyTab", "true");
+ options.put("storeKey", "true");
+ options.put("useTicketCache", "false");
+ options.put("refreshKrb5Config", "true");
+ String jaasEnvVar = System.getenv("HADOOP_JAAS_DEBUG");
+ if ("true".equalsIgnoreCase(jaasEnvVar)) {
+ options.put("debug", "true");
+ }
+ entry = new AppConfigurationEntry[]{
+ new AppConfigurationEntry(getKrb5LoginModuleName(),
+ AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+ options)};
+ }
+
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+ return (entryName.equals(name)) ? entry : ((baseConfig != null)
+ ? baseConfig.getAppConfigurationEntry(name) : null);
+ }
+
+ private String getKrb5LoginModuleName() {
+ String krb5LoginModuleName;
+ if (System.getProperty("java.vendor").contains("IBM")) {
+ krb5LoginModuleName = "com.ibm.security.auth.module.Krb5LoginModule";
+ } else {
+ krb5LoginModuleName = "com.sun.security.auth.module.Krb5LoginModule";
+ }
+ return krb5LoginModuleName;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RandomSignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RandomSignerSecretProvider.java
new file mode 100644
index 000000000000..39da6f333d4a
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RandomSignerSecretProvider.java
@@ -0,0 +1,53 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+
+/**
+ * A SignerSecretProvider that uses a random number as its secret. It rolls
+ * the secret at a regular interval.
+ */
+@InterfaceStability.Unstable
+@InterfaceAudience.Private
+public class RandomSignerSecretProvider extends RolloverSignerSecretProvider {
+
+ private final Random rand;
+
+ public RandomSignerSecretProvider() {
+ super();
+ rand = new SecureRandom();
+ }
+
+ /**
+ * This constructor lets you set the seed of the Random Number Generator and
+ * is meant for testing.
+ * @param seed the seed for the random number generator
+ */
+ public RandomSignerSecretProvider(long seed) {
+ super();
+ rand = new Random(seed);
+ }
+
+ @Override
+ protected byte[] generateNewSecret() {
+ byte[] secret = new byte[32]; // 32 bytes = 256 bits
+ rand.nextBytes(secret);
+ return secret;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RolloverSignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RolloverSignerSecretProvider.java
new file mode 100644
index 000000000000..468b0bc8e263
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RolloverSignerSecretProvider.java
@@ -0,0 +1,142 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import java.util.Properties;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletContext;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An abstract SignerSecretProvider that can be use used as the base for a
+ * rolling secret. The secret will roll over at the same interval as the token
+ * validity, so there are only ever a maximum of two valid secrets at any
+ * given time. This class handles storing and returning the secrets, as well
+ * as the rolling over. At a minimum, subclasses simply need to implement the
+ * generateNewSecret() method. More advanced implementations can override
+ * other methods to provide more advanced behavior, but should be careful when
+ * doing so.
+ */
+@InterfaceStability.Unstable
+@InterfaceAudience.Private
+public abstract class RolloverSignerSecretProvider
+ extends SignerSecretProvider {
+
+ static Logger LOG = LoggerFactory.getLogger(
+ RolloverSignerSecretProvider.class);
+ /**
+ * Stores the currently valid secrets. The current secret is the 0th element
+ * in the array.
+ */
+ private volatile byte[][] secrets;
+ private ScheduledExecutorService scheduler;
+ private boolean schedulerRunning;
+ private boolean isDestroyed;
+
+ public RolloverSignerSecretProvider() {
+ schedulerRunning = false;
+ isDestroyed = false;
+ }
+
+ /**
+ * Initialize the SignerSecretProvider. It initializes the current secret
+ * and starts the scheduler for the rollover to run at an interval of
+ * tokenValidity.
+ * @param config configuration properties
+ * @param servletContext servlet context
+ * @param tokenValidity The amount of time a token is valid for
+ * @throws Exception thrown if an error occurred
+ */
+ @Override
+ public void init(Properties config, ServletContext servletContext,
+ long tokenValidity) throws Exception {
+ initSecrets(generateNewSecret(), null);
+ startScheduler(tokenValidity, tokenValidity);
+ }
+
+ /**
+ * Initializes the secrets array. This should typically be called only once,
+ * during init but some implementations may wish to call it other times.
+ * previousSecret can be null if there isn't a previous secret, but
+ * currentSecret should never be null.
+ * @param currentSecret The current secret
+ * @param previousSecret The previous secret
+ */
+ protected void initSecrets(byte[] currentSecret, byte[] previousSecret) {
+ secrets = new byte[][]{currentSecret, previousSecret};
+ }
+
+ /**
+ * Starts the scheduler for the rollover to run at an interval.
+ * @param initialDelay The initial delay in the rollover in milliseconds
+ * @param period The interval for the rollover in milliseconds
+ */
+ protected synchronized void startScheduler(long initialDelay, long period) {
+ if (!schedulerRunning) {
+ schedulerRunning = true;
+ scheduler = Executors.newSingleThreadScheduledExecutor();
+ scheduler.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ rollSecret();
+ }
+ }, initialDelay, period, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ @Override
+ public synchronized void destroy() {
+ if (!isDestroyed) {
+ isDestroyed = true;
+ if (scheduler != null) {
+ scheduler.shutdown();
+ }
+ schedulerRunning = false;
+ super.destroy();
+ }
+ }
+
+ /**
+ * Rolls the secret. It is called automatically at the rollover interval.
+ */
+ protected synchronized void rollSecret() {
+ if (!isDestroyed) {
+ LOG.debug("rolling secret");
+ byte[] newSecret = generateNewSecret();
+ secrets = new byte[][]{newSecret, secrets[0]};
+ }
+ }
+
+ /**
+ * Subclasses should implement this to return a new secret. It will be called
+ * automatically at the secret rollover interval. It should never return null.
+ * @return a new secret
+ */
+ protected abstract byte[] generateNewSecret();
+
+ @Override
+ public byte[] getCurrentSecret() {
+ return secrets[0];
+ }
+
+ @Override
+ public byte[][] getAllSecrets() {
+ return secrets;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/Signer.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/Signer.java
new file mode 100644
index 000000000000..7b0cd122df3d
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/Signer.java
@@ -0,0 +1,125 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.StringUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Signs strings and verifies signed strings using a SHA digest.
+ */
+@InterfaceAudience.Private
+public class Signer {
+ private static final String SIGNATURE = "&s=";
+ private static final String SIGNING_ALGORITHM = "HmacSHA256";
+
+ private SignerSecretProvider secretProvider;
+
+ /**
+ * Creates a Signer instance using the specified SignerSecretProvider. The
+ * SignerSecretProvider should already be initialized.
+ *
+ * @param secretProvider The SignerSecretProvider to use
+ */
+ public Signer(SignerSecretProvider secretProvider) {
+ if (secretProvider == null) {
+ throw new IllegalArgumentException("secretProvider cannot be NULL");
+ }
+ this.secretProvider = secretProvider;
+ }
+
+ /**
+ * Returns a signed string.
+ *
+ * @param str string to sign.
+ *
+ * @return the signed string.
+ */
+ public synchronized String sign(String str) {
+ if (str == null || str.length() == 0) {
+ throw new IllegalArgumentException("NULL or empty string to sign");
+ }
+ byte[] secret = secretProvider.getCurrentSecret();
+ String signature = computeSignature(secret, str);
+ return str + SIGNATURE + signature;
+ }
+
+ /**
+ * Verifies a signed string and extracts the original string.
+ *
+ * @param signedStr the signed string to verify and extract.
+ *
+ * @return the extracted original string.
+ *
+ * @throws SignerException thrown if the given string is not a signed string or if the signature is invalid.
+ */
+ public String verifyAndExtract(String signedStr) throws SignerException {
+ int index = signedStr.lastIndexOf(SIGNATURE);
+ if (index == -1) {
+ throw new SignerException("Invalid signed text: " + signedStr);
+ }
+ String originalSignature = signedStr.substring(index + SIGNATURE.length());
+ String rawValue = signedStr.substring(0, index);
+ checkSignatures(rawValue, originalSignature);
+ return rawValue;
+ }
+
+ /**
+ * Returns then signature of a string.
+ *
+ * @param secret The secret to use
+ * @param str string to sign.
+ *
+ * @return the signature for the string.
+ */
+ protected String computeSignature(byte[] secret, String str) {
+ try {
+ SecretKeySpec key = new SecretKeySpec((secret), SIGNING_ALGORITHM);
+ Mac mac = Mac.getInstance(SIGNING_ALGORITHM);
+ mac.init(key);
+ byte[] sig = mac.doFinal(StringUtils.getBytesUtf8(str));
+ return new Base64(0).encodeToString(sig);
+ } catch (NoSuchAlgorithmException | InvalidKeyException ex) {
+ throw new RuntimeException("It should not happen, " + ex.getMessage(), ex);
+ }
+ }
+
+ protected void checkSignatures(String rawValue, String originalSignature)
+ throws SignerException {
+ byte[] orginalSignatureBytes = StringUtils.getBytesUtf8(originalSignature);
+ boolean isValid = false;
+ byte[][] secrets = secretProvider.getAllSecrets();
+ for (int i = 0; i < secrets.length; i++) {
+ byte[] secret = secrets[i];
+ if (secret != null) {
+ String currentSignature = computeSignature(secret, rawValue);
+ if (MessageDigest.isEqual(orginalSignatureBytes,
+ StringUtils.getBytesUtf8(currentSignature))) {
+ isValid = true;
+ break;
+ }
+ }
+ }
+ if (!isValid) {
+ throw new SignerException("Invalid signature");
+ }
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerException.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerException.java
new file mode 100644
index 000000000000..7865eda06ab1
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerException.java
@@ -0,0 +1,34 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Exception thrown by {@link Signer} when a string signature is invalid.
+ */
+@InterfaceAudience.Private
+public class SignerException extends Exception {
+
+ static final long serialVersionUID = 0;
+
+ /**
+ * Creates an exception instance.
+ *
+ * @param msg message for the exception.
+ */
+ public SignerException(String msg) {
+ super(msg);
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerSecretProvider.java
new file mode 100644
index 000000000000..0fcccb685231
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerSecretProvider.java
@@ -0,0 +1,63 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import java.util.Properties;
+import javax.servlet.ServletContext;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+
+/**
+ * The SignerSecretProvider is an abstract way to provide a secret to be used
+ * by the Signer so that we can have different implementations that potentially
+ * do more complicated things in the backend.
+ * See the RolloverSignerSecretProvider class for an implementation that
+ * supports rolling over the secret at a regular interval.
+ */
+@InterfaceStability.Unstable
+@InterfaceAudience.Private
+public abstract class SignerSecretProvider {
+
+ /**
+ * Initialize the SignerSecretProvider
+ * @param config configuration properties
+ * @param servletContext servlet context
+ * @param tokenValidity The amount of time a token is valid for
+ * @throws Exception thrown if an error occurred
+ */
+ public abstract void init(Properties config, ServletContext servletContext,
+ long tokenValidity) throws Exception;
+ /**
+ * Will be called on shutdown; subclasses should perform any cleanup here.
+ */
+ public void destroy() {}
+
+ /**
+ * Returns the current secret to be used by the Signer for signing new
+ * cookies. This should never return null.
+ *
+ * Callers should be careful not to modify the returned value.
+ * @return the current secret
+ */
+ public abstract byte[] getCurrentSecret();
+
+ /**
+ * Returns all secrets that a cookie could have been signed with and are still
+ * valid; this should include the secret returned by getCurrentSecret().
+ *
+ * Callers should be careful not to modify the returned value.
+ * @return the secrets
+ */
+ public abstract byte[][] getAllSecrets();
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZKSignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZKSignerSecretProvider.java
new file mode 100644
index 000000000000..591b41d89dde
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZKSignerSecretProvider.java
@@ -0,0 +1,379 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import java.nio.ByteBuffer;
+import java.security.SecureRandom;
+import java.util.Properties;
+import java.util.Random;
+import javax.servlet.ServletContext;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.hadoop.hbase.security.authentication.util.ZookeeperClient;
+
+/**
+ * A SignerSecretProvider that synchronizes a rolling random secret between
+ * multiple servers using ZooKeeper.
+ *
+ * It works by storing the secrets and next rollover time in a ZooKeeper znode.
+ * All ZKSignerSecretProviders looking at that znode will use those
+ * secrets and next rollover time to ensure they are synchronized. There is no
+ * "leader" -- any of the ZKSignerSecretProviders can choose the next secret;
+ * which one is indeterminate. Kerberos-based ACLs can also be enforced to
+ * prevent a malicious third-party from getting or setting the secrets. It uses
+ * its own CuratorFramework client for talking to ZooKeeper. If you want to use
+ * your own Curator client, you can pass it to ZKSignerSecretProvider; see
+ * {@link org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter}
+ * for more details.
+ *
+ * Details of the configurations are listed on Configuration Page
+ */
+@InterfaceStability.Unstable
+@InterfaceAudience.Private
+public class ZKSignerSecretProvider extends RolloverSignerSecretProvider {
+
+ private static final String CONFIG_PREFIX =
+ "signer.secret.provider.zookeeper.";
+
+ /**
+ * Constant for the property that specifies the ZooKeeper connection string.
+ */
+ public static final String ZOOKEEPER_CONNECTION_STRING =
+ CONFIG_PREFIX + "connection.string";
+
+ /**
+ * Constant for the property that specifies the ZooKeeper path.
+ */
+ public static final String ZOOKEEPER_PATH = CONFIG_PREFIX + "path";
+
+ /**
+ * Constant for the property that specifies the auth type to use. Supported
+ * values are "none" and "sasl". The default value is "none".
+ */
+ public static final String ZOOKEEPER_AUTH_TYPE = CONFIG_PREFIX + "auth.type";
+
+ /**
+ * Constant for the property that specifies the Kerberos keytab file.
+ */
+ public static final String ZOOKEEPER_KERBEROS_KEYTAB =
+ CONFIG_PREFIX + "kerberos.keytab";
+
+ /**
+ * Constant for the property that specifies the Kerberos principal.
+ */
+ public static final String ZOOKEEPER_KERBEROS_PRINCIPAL =
+ CONFIG_PREFIX + "kerberos.principal";
+
+ public static final String ZOOKEEPER_SSL_ENABLED = CONFIG_PREFIX + "ssl.enabled";
+ public static final String ZOOKEEPER_SSL_KEYSTORE_LOCATION =
+ CONFIG_PREFIX + "ssl.keystore.location";
+ public static final String ZOOKEEPER_SSL_KEYSTORE_PASSWORD =
+ CONFIG_PREFIX + "ssl.keystore.password";
+ public static final String ZOOKEEPER_SSL_TRUSTSTORE_LOCATION =
+ CONFIG_PREFIX + "ssl.truststore.location";
+ public static final String ZOOKEEPER_SSL_TRUSTSTORE_PASSWORD =
+ CONFIG_PREFIX + "ssl.truststore.password";
+
+ /**
+ * Constant for the property that specifies whether or not the Curator client
+ * should disconnect from ZooKeeper on shutdown. The default is "true". Only
+ * set this to "false" if a custom Curator client is being provided and the
+ * disconnection is being handled elsewhere.
+ */
+ public static final String DISCONNECT_FROM_ZOOKEEPER_ON_SHUTDOWN =
+ CONFIG_PREFIX + "disconnect.on.shutdown";
+
+ /**
+ * Constant for the ServletContext attribute that can be used for providing a
+ * custom CuratorFramework client. If set ZKSignerSecretProvider will use this
+ * Curator client instead of creating a new one. The providing class is
+ * responsible for creating and configuring the Curator client (including
+ * security and ACLs) in this case.
+ */
+ public static final String
+ ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE =
+ CONFIG_PREFIX + "curator.client";
+
+ private static final String JAAS_LOGIN_ENTRY_NAME =
+ "ZKSignerSecretProviderClient";
+
+ private static Logger LOG = LoggerFactory.getLogger(
+ ZKSignerSecretProvider.class);
+ private String path;
+ /**
+ * Stores the next secret that will be used after the current one rolls over.
+ * We do this to help with rollover performance by actually deciding the next
+ * secret at the previous rollover. This allows us to switch to the next
+ * secret very quickly. Afterwards, we have plenty of time to decide on the
+ * next secret.
+ */
+ private volatile byte[] nextSecret;
+ private final Random rand;
+ /**
+ * Stores the current version of the znode.
+ */
+ private int zkVersion;
+ /**
+ * Stores the next date that the rollover will occur. This is only used
+ * for allowing new servers joining later to synchronize their rollover
+ * with everyone else.
+ */
+ private long nextRolloverDate;
+ private long tokenValidity;
+ private CuratorFramework client;
+ private boolean shouldDisconnect;
+ private static int INT_BYTES = Integer.SIZE / Byte.SIZE;
+ private static int LONG_BYTES = Long.SIZE / Byte.SIZE;
+ private static int DATA_VERSION = 0;
+
+ public ZKSignerSecretProvider() {
+ super();
+ rand = new SecureRandom();
+ }
+
+ /**
+ * This constructor lets you set the seed of the Random Number Generator and
+ * is meant for testing.
+ * @param seed the seed for the random number generator
+ */
+ public ZKSignerSecretProvider(long seed) {
+ super();
+ rand = new Random(seed);
+ }
+
+ @Override
+ public void init(Properties config, ServletContext servletContext,
+ long tokenValidity) throws Exception {
+ Object curatorClientObj = servletContext.getAttribute(
+ ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE);
+ if (curatorClientObj != null
+ && curatorClientObj instanceof CuratorFramework) {
+ client = (CuratorFramework) curatorClientObj;
+ } else {
+ client = createCuratorClient(config);
+ servletContext.setAttribute(
+ ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE, client);
+ }
+ this.tokenValidity = tokenValidity;
+ shouldDisconnect = Boolean.parseBoolean(
+ config.getProperty(DISCONNECT_FROM_ZOOKEEPER_ON_SHUTDOWN, "true"));
+ path = config.getProperty(ZOOKEEPER_PATH);
+ if (path == null) {
+ throw new IllegalArgumentException(ZOOKEEPER_PATH
+ + " must be specified");
+ }
+ try {
+ nextRolloverDate = System.currentTimeMillis() + tokenValidity;
+ // everyone tries to do this, only one will succeed and only when the
+ // znode doesn't already exist. Everyone else will synchronize on the
+ // data from the znode
+ client.create().creatingParentsIfNeeded()
+ .forPath(path, generateZKData(generateRandomSecret(),
+ generateRandomSecret(), null));
+ zkVersion = 0;
+ LOG.info("Creating secret znode");
+ } catch (KeeperException.NodeExistsException nee) {
+ LOG.info("The secret znode already exists, retrieving data");
+ }
+ // Synchronize on the data from the znode
+ // passing true tells it to parse out all the data for initing
+ pullFromZK(true);
+ long initialDelay = nextRolloverDate - System.currentTimeMillis();
+ // If it's in the past, try to find the next interval that we should
+ // be using
+ if (initialDelay < 1l) {
+ int i = 1;
+ while (initialDelay < 1l) {
+ initialDelay = nextRolloverDate + tokenValidity * i
+ - System.currentTimeMillis();
+ i++;
+ }
+ }
+ super.startScheduler(initialDelay, tokenValidity);
+ }
+
+ /**
+ * Disconnects from ZooKeeper unless told not to.
+ */
+ @Override
+ public void destroy() {
+ if (shouldDisconnect && client != null) {
+ client.close();
+ }
+ super.destroy();
+ }
+
+ @Override
+ protected synchronized void rollSecret() {
+ super.rollSecret();
+ // Try to push the information to ZooKeeper with a potential next secret.
+ nextRolloverDate += tokenValidity;
+ byte[][] secrets = super.getAllSecrets();
+ pushToZK(generateRandomSecret(), secrets[0], secrets[1]);
+ // Pull info from ZooKeeper to get the decided next secret
+ // passing false tells it that we don't care about most of the data
+ pullFromZK(false);
+ }
+
+ @Override
+ protected byte[] generateNewSecret() {
+ // We simply return nextSecret because it's already been decided on
+ return nextSecret;
+ }
+
+ /**
+ * Pushes proposed data to ZooKeeper. If a different server pushes its data
+ * first, it gives up.
+ * @param newSecret The new secret to use
+ * @param currentSecret The current secret
+ * @param previousSecret The previous secret
+ */
+ private synchronized void pushToZK(byte[] newSecret, byte[] currentSecret,
+ byte[] previousSecret) {
+ byte[] bytes = generateZKData(newSecret, currentSecret, previousSecret);
+ try {
+ client.setData().withVersion(zkVersion).forPath(path, bytes);
+ } catch (KeeperException.BadVersionException bve) {
+ LOG.debug("Unable to push to znode; another server already did it");
+ } catch (Exception ex) {
+ LOG.error("An unexpected exception occurred pushing data to ZooKeeper",
+ ex);
+ }
+ }
+
+ /**
+ * Serialize the data to attempt to push into ZooKeeper. The format is this:
+ *
+ * @param newSecret The new secret to use
+ * @param currentSecret The current secret
+ * @param previousSecret The previous secret
+ * @return The serialized data for ZooKeeper
+ */
+ private synchronized byte[] generateZKData(byte[] newSecret,
+ byte[] currentSecret, byte[] previousSecret) {
+ int newSecretLength = newSecret.length;
+ int currentSecretLength = currentSecret.length;
+ int previousSecretLength = 0;
+ if (previousSecret != null) {
+ previousSecretLength = previousSecret.length;
+ }
+ ByteBuffer bb = ByteBuffer.allocate(INT_BYTES + INT_BYTES + newSecretLength
+ + INT_BYTES + currentSecretLength + INT_BYTES + previousSecretLength
+ + LONG_BYTES);
+ bb.putInt(DATA_VERSION);
+ bb.putInt(newSecretLength);
+ bb.put(newSecret);
+ bb.putInt(currentSecretLength);
+ bb.put(currentSecret);
+ bb.putInt(previousSecretLength);
+ if (previousSecretLength > 0) {
+ bb.put(previousSecret);
+ }
+ bb.putLong(nextRolloverDate);
+ return bb.array();
+ }
+
+ /**
+ * Pulls data from ZooKeeper. If isInit is false, it will only parse the
+ * next secret and version. If isInit is true, it will also parse the current
+ * and previous secrets, and the next rollover date; it will also init the
+ * secrets. Hence, isInit should only be true on startup.
+ * @param isInit see description above
+ */
+ private synchronized void pullFromZK(boolean isInit) {
+ try {
+ Stat stat = new Stat();
+ byte[] bytes = client.getData().storingStatIn(stat).forPath(path);
+ ByteBuffer bb = ByteBuffer.wrap(bytes);
+ int dataVersion = bb.getInt();
+ if (dataVersion > DATA_VERSION) {
+ throw new IllegalStateException("Cannot load data from ZooKeeper; it"
+ + "was written with a newer version");
+ }
+ int nextSecretLength = bb.getInt();
+ byte[] nextSecret = new byte[nextSecretLength];
+ bb.get(nextSecret);
+ this.nextSecret = nextSecret;
+ zkVersion = stat.getVersion();
+ if (isInit) {
+ int currentSecretLength = bb.getInt();
+ byte[] currentSecret = new byte[currentSecretLength];
+ bb.get(currentSecret);
+ int previousSecretLength = bb.getInt();
+ byte[] previousSecret = null;
+ if (previousSecretLength > 0) {
+ previousSecret = new byte[previousSecretLength];
+ bb.get(previousSecret);
+ }
+ super.initSecrets(currentSecret, previousSecret);
+ nextRolloverDate = bb.getLong();
+ }
+ } catch (Exception ex) {
+ LOG.error("An unexpected exception occurred while pulling data from"
+ + "ZooKeeper", ex);
+ }
+ }
+
+ protected byte[] generateRandomSecret() {
+ byte[] secret = new byte[32]; // 32 bytes = 256 bits
+ rand.nextBytes(secret);
+ return secret;
+ }
+
+ /**
+ * This method creates the Curator client and connects to ZooKeeper.
+ * @param config configuration properties
+ * @return A Curator client
+ */
+ protected CuratorFramework createCuratorClient(Properties config) {
+ String connectionString = config.getProperty(ZOOKEEPER_CONNECTION_STRING, "localhost:2181");
+ String authType = config.getProperty(ZOOKEEPER_AUTH_TYPE, "none");
+ String keytab = config.getProperty(ZOOKEEPER_KERBEROS_KEYTAB, "").trim();
+ String principal = config.getProperty(ZOOKEEPER_KERBEROS_PRINCIPAL, "").trim();
+
+ boolean sslEnabled = Boolean.parseBoolean(config.getProperty(ZOOKEEPER_SSL_ENABLED, "false"));
+ String keystoreLocation = config.getProperty(ZOOKEEPER_SSL_KEYSTORE_LOCATION, "");
+ String keystorePassword = config.getProperty(ZOOKEEPER_SSL_KEYSTORE_PASSWORD, "");
+ String truststoreLocation = config.getProperty(ZOOKEEPER_SSL_TRUSTSTORE_LOCATION, "");
+ String truststorePassword = config.getProperty(ZOOKEEPER_SSL_TRUSTSTORE_PASSWORD, "");
+
+ CuratorFramework zkClient =
+ ZookeeperClient.configure()
+ .withConnectionString(connectionString)
+ .withAuthType(authType)
+ .withKeytab(keytab)
+ .withPrincipal(principal)
+ .withJaasLoginEntryName(JAAS_LOGIN_ENTRY_NAME)
+ .enableSSL(sslEnabled)
+ .withKeystore(keystoreLocation)
+ .withKeystorePassword(keystorePassword)
+ .withTruststore(truststoreLocation)
+ .withTruststorePassword(truststorePassword)
+ .create();
+ zkClient.start();
+ return zkClient;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZookeeperClient.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZookeeperClient.java
new file mode 100644
index 000000000000..5343900d6293
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZookeeperClient.java
@@ -0,0 +1,320 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+
+package org.apache.hadoop.hbase.security.authentication.util;
+
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.api.ACLProvider;
+import org.apache.curator.framework.imps.DefaultACLProvider;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.curator.utils.ConfigurableZookeeperFactory;
+import org.apache.curator.utils.ZookeeperFactory;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.client.ZKClientConfig;
+import org.apache.zookeeper.common.ClientX509Util;
+import org.apache.zookeeper.data.ACL;
+import org.apache.zookeeper.data.Id;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.login.Configuration;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hadoop.hbase.security.authentication.util.JaasConfiguration;
+
+
+/**
+ * Utility class to create a CuratorFramework object that can be used to connect to Zookeeper
+ * based on configuration values that can be supplied from different configuration properties.
+ * It is used from ZKDelegationTokenSecretManager in hadoop-common, and from
+ * {@link ZKSignerSecretProvider}.
+ *
+ * The class implements a fluid API to set up all the different properties. A very basic setup
+ * would seem like:
+ *
keytab: the location of the keytab to be used for Kerberos authentication
+ *
principal: the Kerberos principal to be used from the supplied Kerberos keytab file.
+ *
jaasLoginEntryName: the login entry name in the JAAS configuration that is created for
+ * the KerberosLoginModule to be used by the Zookeeper client code.
+ *
+ *
+ *
if SSL is enabled:
+ *
+ *
the location of the Truststore file to be used
+ *
the location of the Keystore file to be used
+ *
if the Truststore is protected by a password, then the password of the Truststore
+ *
if the Keystore is protected by a password, then the password if the Keystore
+ *
+ *
+ *
+ *
+ * When using 'sasl' authentication type, the JAAS configuration to be used by the Zookeeper client
+ * withing CuratorFramework is set to use the supplied keytab and principal for Kerberos login,
+ * moreover an ACL provider is set to provide a default ACL that requires SASL auth and the same
+ * principal to have access to the used paths.
+ *
+ * When using SSL/TLS, the Zookeeper client will set to use the secure channel towards Zookeeper,
+ * with the specified Keystore and Truststore.
+ *
+ * Default values:
+ *
+ *
authentication type: 'none'
+ *
sessionTimeout: either the system property curator-default-session-timeout, or 60
+ * seconds
+ *
connectionTimeout: either the system property curator-default-connection-timeout, or 15
+ * seconds
+ *
retryPolicy: an ExponentialBackoffRetry, with a starting interval of 1 seconds and 3
+ * retries
+ *
zkFactory: a ConfigurableZookeeperFactory instance, to allow SSL setup via
+ * ZKClientConfig
+ *
+ *
+ * @see ZKSignerSecretProvider
+ */
+@InterfaceAudience.Private
+public class ZookeeperClient {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ZookeeperClient.class);
+
+ private String connectionString;
+ private String namespace;
+
+ private String authenticationType = "none";
+ private String keytab;
+ private String principal;
+ private String jaasLoginEntryName;
+
+ private int sessionTimeout =
+ Integer.getInteger("curator-default-session-timeout", 60 * 1000);
+ private int connectionTimeout =
+ Integer.getInteger("curator-default-connection-timeout", 15 * 1000);
+
+ private RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
+
+ private ZookeeperFactory zkFactory = new ConfigurableZookeeperFactory();
+
+ private boolean isSSLEnabled;
+ private String keystoreLocation;
+ private String keystorePassword;
+ private String truststoreLocation;
+ private String truststorePassword;
+
+ public static ZookeeperClient configure() {
+ return new ZookeeperClient();
+ }
+
+ public ZookeeperClient withConnectionString(String conn) {
+ connectionString = conn;
+ return this;
+ }
+
+ public ZookeeperClient withNamespace(String ns) {
+ this.namespace = ns;
+ return this;
+ }
+
+ public ZookeeperClient withAuthType(String authType) {
+ this.authenticationType = authType;
+ return this;
+ }
+
+ public ZookeeperClient withKeytab(String keytabPath) {
+ this.keytab = keytabPath;
+ return this;
+ }
+
+ public ZookeeperClient withPrincipal(String princ) {
+ this.principal = princ;
+ return this;
+ }
+
+ public ZookeeperClient withJaasLoginEntryName(String entryName) {
+ this.jaasLoginEntryName = entryName;
+ return this;
+ }
+
+ public ZookeeperClient withSessionTimeout(int timeoutMS) {
+ this.sessionTimeout = timeoutMS;
+ return this;
+ }
+
+ public ZookeeperClient withConnectionTimeout(int timeoutMS) {
+ this.connectionTimeout = timeoutMS;
+ return this;
+ }
+
+ public ZookeeperClient withRetryPolicy(RetryPolicy policy) {
+ this.retryPolicy = policy;
+ return this;
+ }
+
+ public ZookeeperClient withZookeeperFactory(ZookeeperFactory factory) {
+ this.zkFactory = factory;
+ return this;
+ }
+
+ public ZookeeperClient enableSSL(boolean enable) {
+ this.isSSLEnabled = enable;
+ return this;
+ }
+
+ public ZookeeperClient withKeystore(String keystorePath) {
+ this.keystoreLocation = keystorePath;
+ return this;
+ }
+
+ public ZookeeperClient withKeystorePassword(String keystorePass) {
+ this.keystorePassword = keystorePass;
+ return this;
+ }
+
+ public ZookeeperClient withTruststore(String truststorePath) {
+ this.truststoreLocation = truststorePath;
+ return this;
+ }
+
+ public ZookeeperClient withTruststorePassword(String truststorePass) {
+ this.truststorePassword = truststorePass;
+ return this;
+ }
+
+ public CuratorFramework create() {
+ checkNotNull(connectionString, "Zookeeper connection string cannot be null!");
+ checkNotNull(retryPolicy, "Zookeeper connection retry policy cannot be null!");
+
+ return createFrameworkFactoryBuilder()
+ .connectString(connectionString)
+ .zookeeperFactory(zkFactory)
+ .namespace(namespace)
+ .sessionTimeoutMs(sessionTimeout)
+ .connectionTimeoutMs(connectionTimeout)
+ .retryPolicy(retryPolicy)
+ .aclProvider(aclProvider())
+ .zkClientConfig(zkClientConfig())
+ .build();
+ }
+
+ CuratorFrameworkFactory.Builder createFrameworkFactoryBuilder() {
+ return CuratorFrameworkFactory.builder();
+ }
+
+ private ACLProvider aclProvider() {
+ // AuthType has to be explicitly set to 'none' or 'sasl'
+ checkNotNull(authenticationType, "Zookeeper authType cannot be null!");
+ checkArgument(authenticationType.equals("sasl") || authenticationType.equals("none"),
+ "Zookeeper authType must be one of [none, sasl]!");
+
+ ACLProvider aclProvider;
+ if (authenticationType.equals("sasl")) {
+ LOG.info("Connecting to ZooKeeper with SASL/Kerberos and using 'sasl' ACLs.");
+
+ checkArgument(!isEmpty(keytab), "Zookeeper client's Kerberos Keytab must be specified!");
+ checkArgument(!isEmpty(principal),
+ "Zookeeper client's Kerberos Principal must be specified!");
+ checkArgument(!isEmpty(jaasLoginEntryName), "JAAS Login Entry name must be specified!");
+
+ JaasConfiguration jConf = new JaasConfiguration(jaasLoginEntryName, principal, keytab);
+ Configuration.setConfiguration(jConf);
+ System.setProperty(ZKClientConfig.LOGIN_CONTEXT_NAME_KEY, jaasLoginEntryName);
+ System.setProperty("zookeeper.authProvider.1",
+ "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
+ aclProvider = new SASLOwnerACLProvider(principal.split("[/@]")[0]);
+ } else { // "none"
+ LOG.info("Connecting to ZooKeeper without authentication.");
+ aclProvider = new DefaultACLProvider(); // open to everyone
+ }
+ return aclProvider;
+ }
+
+ private ZKClientConfig zkClientConfig() {
+ ZKClientConfig zkClientConfig = new ZKClientConfig();
+ if (isSSLEnabled){
+ LOG.info("Zookeeper client will use SSL connection. (keystore = {}; truststore = {};)",
+ keystoreLocation, truststoreLocation);
+ checkArgument(!isEmpty(keystoreLocation),
+ "The keystore location parameter is empty for the ZooKeeper client connection.");
+ checkArgument(!isEmpty(truststoreLocation),
+ "The truststore location parameter is empty for the ZooKeeper client connection.");
+
+ try (ClientX509Util sslOpts = new ClientX509Util()) {
+ zkClientConfig.setProperty(ZKClientConfig.SECURE_CLIENT, "true");
+ zkClientConfig.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET,
+ "org.apache.zookeeper.ClientCnxnSocketNetty");
+ zkClientConfig.setProperty(sslOpts.getSslKeystoreLocationProperty(), keystoreLocation);
+ zkClientConfig.setProperty(sslOpts.getSslKeystorePasswdProperty(), keystorePassword);
+ zkClientConfig.setProperty(sslOpts.getSslTruststoreLocationProperty(), truststoreLocation);
+ zkClientConfig.setProperty(sslOpts.getSslTruststorePasswdProperty(), truststorePassword);
+ }
+ } else {
+ LOG.info("Zookeeper client will use Plain connection.");
+ }
+ return zkClientConfig;
+ }
+
+ /**
+ * Simple implementation of an {@link ACLProvider} that simply returns an ACL
+ * that gives all permissions only to a single principal.
+ */
+ static final class SASLOwnerACLProvider implements ACLProvider {
+
+ private final List saslACL;
+
+ private SASLOwnerACLProvider(String principal) {
+ this.saslACL = Collections.singletonList(
+ new ACL(ZooDefs.Perms.ALL, new Id("sasl", principal)));
+ }
+
+ @Override
+ public List getDefaultAcl() {
+ return saslACL;
+ }
+
+ @Override
+ public List getAclForPath(String path) {
+ return saslACL;
+ }
+ }
+
+ private boolean isEmpty(String str) {
+ return str == null || str.length() == 0;
+ }
+
+ //Preconditions allowed to be imported from hadoop-common, but that results
+ // in a circular dependency
+ private void checkNotNull(Object reference, String errorMessage) {
+ if (reference == null) {
+ throw new NullPointerException(errorMessage);
+ }
+ }
+
+ private void checkArgument(boolean expression, String errorMessage) {
+ if (!expression) {
+ throw new IllegalArgumentException(errorMessage);
+ }
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/HttpExceptionUtils.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/HttpExceptionUtils.java
new file mode 100644
index 000000000000..d0725106ac9e
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/HttpExceptionUtils.java
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.hadoop.hbase.util;
+
+import org.apache.hadoop.util.JsonSerialization;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * HTTP utility class to help propagate server side exception to the client
+ * over HTTP as a JSON payload.
+ *
+ * It creates HTTP Servlet and JAX-RPC error responses including details of the
+ * exception that allows a client to recreate the remote exception.
+ *
+ * It parses HTTP client connections and recreates the exception.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Unstable
+public class HttpExceptionUtils {
+
+ public static final String ERROR_JSON = "RemoteException";
+ public static final String ERROR_EXCEPTION_JSON = "exception";
+ public static final String ERROR_CLASSNAME_JSON = "javaClassName";
+ public static final String ERROR_MESSAGE_JSON = "message";
+
+ private static final String APPLICATION_JSON_MIME = "application/json";
+
+ private static final String ENTER = System.getProperty("line.separator");
+
+ /**
+ * Creates a HTTP servlet response serializing the exception in it as JSON.
+ *
+ * @param response the servlet response
+ * @param status the error code to set in the response
+ * @param ex the exception to serialize in the response
+ * @throws IOException thrown if there was an error while creating the
+ * response
+ */
+ public static void createServletExceptionResponse(
+ HttpServletResponse response, int status, Throwable ex)
+ throws IOException {
+ response.setStatus(status);
+ response.setContentType(APPLICATION_JSON_MIME);
+ Map json = new LinkedHashMap();
+ json.put(ERROR_MESSAGE_JSON, getOneLineMessage(ex));
+ json.put(ERROR_EXCEPTION_JSON, ex.getClass().getSimpleName());
+ json.put(ERROR_CLASSNAME_JSON, ex.getClass().getName());
+ Map jsonResponse =
+ Collections.singletonMap(ERROR_JSON, json);
+ Writer writer = response.getWriter();
+ JsonSerialization.writer().writeValue(writer, jsonResponse);
+ writer.flush();
+ }
+
+ private static String getOneLineMessage(Throwable exception) {
+ String message = exception.getMessage();
+ if (message != null) {
+ int i = message.indexOf(ENTER);
+ if (i > -1) {
+ message = message.substring(0, i);
+ }
+ }
+ return message;
+ }
+}
diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/ServletUtil.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/ServletUtil.java
new file mode 100644
index 000000000000..73e0abf4b736
--- /dev/null
+++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/ServletUtil.java
@@ -0,0 +1,112 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.hadoop.hbase.util;
+
+import java.io.*;
+import java.util.Calendar;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+
+@InterfaceAudience.Private
+@InterfaceStability.Unstable
+public class ServletUtil {
+ /**
+ * Initial HTML header.
+ *
+ * @param response response.
+ * @param title title.
+ * @throws IOException raised on errors performing I/O.
+ * @return PrintWriter.
+ */
+ public static PrintWriter initHTML(ServletResponse response, String title
+ ) throws IOException {
+ response.setContentType("text/html");
+ PrintWriter out = response.getWriter();
+ out.println("\n"
+ + "\n"
+ + "" + title + "\n"
+ + "\n"
+ + "
" + title + "
\n");
+ return out;
+ }
+
+ /**
+ * Get a parameter from a ServletRequest.
+ * Return null if the parameter contains only white spaces.
+ *
+ * @param request request.
+ * @param name name.
+ * @return get a parameter from a ServletRequest.
+ */
+ public static String getParameter(ServletRequest request, String name) {
+ String s = request.getParameter(name);
+ if (s == null) {
+ return null;
+ }
+ s = s.trim();
+ return s.length() == 0? null: s;
+ }
+
+ /**
+ * parseLongParam.
+ *
+ * @param request request.
+ * @param param param.
+ * @return a long value as passed in the given parameter, throwing
+ * an exception if it is not present or if it is not a valid number.
+ * @throws IOException raised on errors performing I/O.
+ */
+ public static long parseLongParam(ServletRequest request, String param)
+ throws IOException {
+ String paramStr = request.getParameter(param);
+ if (paramStr == null) {
+ throw new IOException("Invalid request has no " + param + " parameter");
+ }
+
+ return Long.parseLong(paramStr);
+ }
+
+ public static final String HTML_TAIL = "\n"
+ + "Hadoop, "
+ + Calendar.getInstance().get(Calendar.YEAR) + ".\n"
+ + "";
+
+ /**
+ * HTML footer to be added in the jsps.
+ * @return the HTML footer.
+ */
+ public static String htmlFooter() {
+ return HTML_TAIL;
+ }
+
+ /**
+ * Parse the path component from the given request and return w/o decoding.
+ * @param request Http request to parse
+ * @param servletName the name of servlet that precedes the path
+ * @return path component, null if the default charset is not supported
+ */
+ public static String getRawPath(final HttpServletRequest request, String servletName) {
+ Preconditions.checkArgument(request.getRequestURI().startsWith(servletName+"/"));
+ return request.getRequestURI().substring(servletName.length());
+ }
+}
diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/KerberosTestUtils.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/KerberosTestUtils.java
new file mode 100644
index 000000000000..2ff1feb84a73
--- /dev/null
+++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/KerberosTestUtils.java
@@ -0,0 +1,140 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+
+import org.apache.hadoop.security.authentication.util.KerberosUtil;
+
+import java.io.File;
+import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.UUID;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import static org.apache.hadoop.util.PlatformName.IBM_JAVA;
+
+/**
+ * Test helper class for Java Kerberos setup.
+ */
+public class KerberosTestUtils {
+ private static String keytabFile = new File(System.getProperty("test.dir", "target"),
+ UUID.randomUUID().toString()).getAbsolutePath();
+
+ public static String getRealm() {
+ return "EXAMPLE.COM";
+ }
+
+ public static String getClientPrincipal() {
+ return "client@EXAMPLE.COM";
+ }
+
+ public static String getServerPrincipal() {
+ return "HTTP/localhost@EXAMPLE.COM";
+ }
+
+ public static String getKeytabFile() {
+ return keytabFile;
+ }
+
+ private static class KerberosConfiguration extends Configuration {
+ private String principal;
+
+ public KerberosConfiguration(String principal) {
+ this.principal = principal;
+ }
+
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+ Map options = new HashMap();
+ if (IBM_JAVA) {
+ options.put("useKeytab", KerberosTestUtils.getKeytabFile().startsWith("file://") ?
+ KerberosTestUtils.getKeytabFile() : "file://" + KerberosTestUtils.getKeytabFile());
+ options.put("principal", principal);
+ options.put("refreshKrb5Config", "true");
+ options.put("credsType", "both");
+ } else {
+ options.put("keyTab", KerberosTestUtils.getKeytabFile());
+ options.put("principal", principal);
+ options.put("useKeyTab", "true");
+ options.put("storeKey", "true");
+ options.put("doNotPrompt", "true");
+ options.put("useTicketCache", "true");
+ options.put("renewTGT", "true");
+ options.put("refreshKrb5Config", "true");
+ options.put("isInitiator", "true");
+ }
+ String ticketCache = System.getenv("KRB5CCNAME");
+ if (ticketCache != null) {
+ if (IBM_JAVA) {
+ // IBM JAVA only respect system property and not env variable
+ // The first value searched when "useDefaultCcache" is used.
+ System.setProperty("KRB5CCNAME", ticketCache);
+ options.put("useDefaultCcache", "true");
+ options.put("renewTGT", "true");
+ } else {
+ options.put("ticketCache", ticketCache);
+ }
+ }
+ options.put("debug", "true");
+
+ return new AppConfigurationEntry[]{
+ new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(),
+ AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+ options),};
+ }
+ }
+
+ public static T doAs(String principal, final Callable callable) throws Exception {
+ LoginContext loginContext = null;
+ try {
+ Set principals = new HashSet<>();
+ principals.add(new KerberosPrincipal(KerberosTestUtils.getClientPrincipal()));
+ Subject subject = new Subject(false, principals, new HashSet<>(), new HashSet<>());
+ loginContext = new LoginContext("", subject, null, new KerberosConfiguration(principal));
+ loginContext.login();
+ subject = loginContext.getSubject();
+ return Subject.doAs(subject, new PrivilegedExceptionAction() {
+ @Override
+ public T run() throws Exception {
+ return callable.call();
+ }
+ });
+ } catch (PrivilegedActionException ex) {
+ throw ex.getException();
+ } finally {
+ if (loginContext != null) {
+ loginContext.logout();
+ }
+ }
+ }
+
+ public static T doAsClient(Callable callable) throws Exception {
+ return doAs(getClientPrincipal(), callable);
+ }
+
+ public static T doAsServer(Callable callable) throws Exception {
+ return doAs(getServerPrincipal(), callable);
+ }
+
+}
diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/AuthenticatorTestCase.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/AuthenticatorTestCase.java
new file mode 100644
index 000000000000..1f69af369fbb
--- /dev/null
+++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/AuthenticatorTestCase.java
@@ -0,0 +1,275 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.client;
+
+import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.auth.SPNegoScheme;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Connector;
+import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Server;
+import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ServerConnector;
+import org.apache.hbase.thirdparty.org.eclipse.jetty.ee8.servlet.FilterHolder;
+import org.apache.hbase.thirdparty.org.eclipse.jetty.ee8.servlet.ServletContextHandler;
+import org.apache.hbase.thirdparty.org.eclipse.jetty.ee8.servlet.ServletHolder;
+
+import javax.servlet.DispatcherType;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.InputStreamReader;
+import java.io.Writer;
+import java.net.HttpURLConnection;
+import java.net.ServerSocket;
+import java.net.URL;
+import java.security.Principal;
+import java.util.EnumSet;
+import java.util.Properties;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.ClassRule;
+import org.junit.experimental.categories.Category;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
+import org.apache.hadoop.security.authentication.client.Authenticator;
+import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
+
+public class AuthenticatorTestCase {
+ private Server server;
+ private String host = null;
+ private int port = -1;
+ ServletContextHandler context;
+
+ private static Properties authenticatorConfig;
+
+ public AuthenticatorTestCase() {}
+
+ protected static void setAuthenticationHandlerConfig(Properties config) {
+ authenticatorConfig = config;
+ }
+
+ public static class TestFilter extends AuthenticationFilter {
+
+ @Override
+ protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException {
+ return authenticatorConfig;
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class TestServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ InputStream is = req.getInputStream();
+ OutputStream os = resp.getOutputStream();
+ int c = is.read();
+ while (c > -1) {
+ os.write(c);
+ c = is.read();
+ }
+ is.close();
+ os.close();
+ resp.setStatus(HttpServletResponse.SC_OK);
+ }
+ }
+
+ protected int getLocalPort() throws Exception {
+ ServerSocket ss = new ServerSocket(0);
+ int ret = ss.getLocalPort();
+ ss.close();
+ return ret;
+ }
+
+ protected void start() throws Exception {
+ startJetty();
+ }
+
+ protected void startJetty() throws Exception {
+ server = new Server();
+ context = new ServletContextHandler();
+ context.setContextPath("/foo");
+ server.setHandler(context);
+ context.addFilter(new FilterHolder(TestFilter.class), "/*",
+ EnumSet.of(DispatcherType.REQUEST));
+ context.addServlet(new ServletHolder(TestServlet.class), "/bar");
+ host = "localhost";
+ port = getLocalPort();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setHost(host);
+ connector.setPort(port);
+ server.setConnectors(new Connector[] {connector});
+ server.start();
+ System.out.println("Running embedded servlet container at: http://" + host + ":" + port);
+ }
+
+ protected void stop() throws Exception {
+ stopJetty();
+ }
+
+ protected void stopJetty() throws Exception {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ }
+
+ try {
+ server.destroy();
+ } catch (Exception e) {
+ }
+ }
+
+ protected String getBaseURL() {
+ return "http://" + host + ":" + port + "/foo/bar";
+ }
+
+ private static class TestConnectionConfigurator
+ implements ConnectionConfigurator {
+ boolean invoked;
+
+ @Override
+ public HttpURLConnection configure(HttpURLConnection conn)
+ throws IOException {
+ invoked = true;
+ return conn;
+ }
+ }
+
+ private String POST = "test";
+
+ protected void _testAuthentication(Authenticator authenticator, boolean doPost) throws Exception {
+ start();
+ try {
+ URL url = new URL(getBaseURL());
+ AuthenticatedURL.Token token = new AuthenticatedURL.Token();
+ assertFalse(token.isSet());
+ TestConnectionConfigurator connConf = new TestConnectionConfigurator();
+ AuthenticatedURL aUrl = new AuthenticatedURL(authenticator, connConf);
+ HttpURLConnection conn = aUrl.openConnection(url, token);
+ assertTrue(connConf.invoked);
+ String tokenStr = token.toString();
+ if (doPost) {
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ }
+ conn.connect();
+ if (doPost) {
+ Writer writer = new OutputStreamWriter(conn.getOutputStream());
+ writer.write(POST);
+ writer.close();
+ }
+ assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
+ if (doPost) {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ String echo = reader.readLine();
+ assertEquals(POST, echo);
+ assertNull(reader.readLine());
+ }
+ aUrl = new AuthenticatedURL();
+ conn = aUrl.openConnection(url, token);
+ conn.connect();
+ assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
+ assertEquals(tokenStr, token.toString());
+ } finally {
+ stop();
+ }
+ }
+
+ private HttpClient getHttpClient() {
+ HttpClientBuilder builder = HttpClientBuilder.create();
+ // Register auth schema
+ builder.setDefaultAuthSchemeRegistry(
+ s-> httpContext -> new SPNegoScheme(true, true)
+ );
+
+ Credentials useJaasCreds = new Credentials() {
+ public String getPassword() {
+ return null;
+ }
+ public Principal getUserPrincipal() {
+ return null;
+ }
+ };
+
+ CredentialsProvider jaasCredentialProvider
+ = new BasicCredentialsProvider();
+ jaasCredentialProvider.setCredentials(AuthScope.ANY, useJaasCreds);
+ // Set credential provider
+ builder.setDefaultCredentialsProvider(jaasCredentialProvider);
+
+ return builder.build();
+ }
+
+ private void doHttpClientRequest(HttpClient httpClient, HttpUriRequest request) throws Exception {
+ HttpResponse response = null;
+ try {
+ response = httpClient.execute(request);
+ final int httpStatus = response.getStatusLine().getStatusCode();
+ assertEquals(HttpURLConnection.HTTP_OK, httpStatus);
+ } finally {
+ if (response != null) EntityUtils.consumeQuietly(response.getEntity());
+ }
+ }
+
+ protected void _testAuthenticationHttpClient(Authenticator authenticator, boolean doPost) throws Exception {
+ start();
+ try {
+ HttpClient httpClient = getHttpClient();
+ doHttpClientRequest(httpClient, new HttpGet(getBaseURL()));
+
+ // Always do a GET before POST to trigger the SPNego negotiation
+ if (doPost) {
+ HttpPost post = new HttpPost(getBaseURL());
+ byte [] postBytes = POST.getBytes();
+ ByteArrayInputStream bis = new ByteArrayInputStream(postBytes);
+ InputStreamEntity entity = new InputStreamEntity(bis, postBytes.length);
+
+ // Important that the entity is not repeatable -- this means if
+ // we have to renegotiate (e.g. b/c the cookie wasn't handled properly)
+ // the test will fail.
+ assertFalse(entity.isRepeatable());
+ post.setEntity(entity);
+ doHttpClientRequest(httpClient, post);
+ }
+ } finally {
+ stop();
+ }
+ }
+}
diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestKerberosAuthenticator.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestKerberosAuthenticator.java
new file mode 100644
index 000000000000..8dd607318a3a
--- /dev/null
+++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestKerberosAuthenticator.java
@@ -0,0 +1,320 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.client;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.apache.hadoop.hbase.security.authentication.server.MultiSchemeAuthenticationHandler.SCHEMES_PROPERTY;
+import static org.apache.hadoop.hbase.security.authentication.server.MultiSchemeAuthenticationHandler.AUTH_HANDLER_PROPERTY;
+import static org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter.AUTH_TYPE;
+import static org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler.PRINCIPAL;
+import static org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler.KEYTAB;
+import static org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler.NAME_RULES;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.CharacterCodingException;
+import javax.security.sasl.AuthenticationException;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.hadoop.minikdc.KerberosSecurityTestcase;
+import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
+import org.apache.hadoop.hbase.security.authentication.KerberosTestUtils;
+import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter;
+import org.apache.hadoop.hbase.security.authentication.server.MultiSchemeAuthenticationHandler;
+import org.apache.hadoop.hbase.security.authentication.server.PseudoAuthenticationHandler;
+import org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler;
+
+import java.io.File;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Properties;
+import java.util.concurrent.Callable;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.ClassRule;
+import org.junit.experimental.categories.Category;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.junit.Before;
+
+/**
+ * Test class for {@link KerberosAuthenticator}.
+ */
+@Category({ MiscTests.class, SmallTests.class })
+public class TestKerberosAuthenticator extends KerberosSecurityTestcase {
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestKerberosAuthenticator.class);
+
+
+ public TestKerberosAuthenticator() {
+ }
+
+ @Before
+ public void setup() throws Exception {
+ // create keytab
+ File keytabFile = new File(KerberosTestUtils.getKeytabFile());
+ String clientPrincipal = KerberosTestUtils.getClientPrincipal();
+ String serverPrincipal = KerberosTestUtils.getServerPrincipal();
+ clientPrincipal = clientPrincipal.substring(0, clientPrincipal.lastIndexOf("@"));
+ serverPrincipal = serverPrincipal.substring(0, serverPrincipal.lastIndexOf("@"));
+ getKdc().createPrincipal(keytabFile, clientPrincipal, serverPrincipal);
+ }
+
+ private Properties getAuthenticationHandlerConfiguration() {
+ Properties props = new Properties();
+ props.setProperty(AuthenticationFilter.AUTH_TYPE, "kerberos");
+ props.setProperty(KerberosAuthenticationHandler.PRINCIPAL, KerberosTestUtils.getServerPrincipal());
+ props.setProperty(KerberosAuthenticationHandler.KEYTAB, KerberosTestUtils.getKeytabFile());
+ props.setProperty(KerberosAuthenticationHandler.NAME_RULES,
+ "RULE:[1:$1@$0](.*@" + KerberosTestUtils.getRealm()+")s/@.*//\n");
+ props.setProperty(KerberosAuthenticationHandler.RULE_MECHANISM, "hadoop");
+ return props;
+ }
+
+ private Properties getMultiAuthHandlerConfiguration() {
+ Properties props = new Properties();
+ props.setProperty(AUTH_TYPE, MultiSchemeAuthenticationHandler.TYPE);
+ props.setProperty(SCHEMES_PROPERTY, "negotiate");
+ props.setProperty(String.format(AUTH_HANDLER_PROPERTY, "negotiate"),
+ "kerberos");
+ props.setProperty(PRINCIPAL, KerberosTestUtils.getServerPrincipal());
+ props.setProperty(KEYTAB, KerberosTestUtils.getKeytabFile());
+ props.setProperty(NAME_RULES,
+ "RULE:[1:$1@$0](.*@" + KerberosTestUtils.getRealm() + ")s/@.*//\n");
+ return props;
+ }
+
+ @Test(timeout = 60000)
+ public void testFallbacktoPseudoAuthenticator() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ Properties props = new Properties();
+ props.setProperty(AuthenticationFilter.AUTH_TYPE, "simple");
+ props.setProperty(PseudoAuthenticationHandler.ANONYMOUS_ALLOWED, "false");
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(props);
+ auth._testAuthentication(new KerberosAuthenticator(), false);
+ }
+
+ @Test(timeout = 60000)
+ public void testFallbacktoPseudoAuthenticatorAnonymous() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ Properties props = new Properties();
+ props.setProperty(AuthenticationFilter.AUTH_TYPE, "simple");
+ props.setProperty(PseudoAuthenticationHandler.ANONYMOUS_ALLOWED, "true");
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(props);
+ auth._testAuthentication(new KerberosAuthenticator(), false);
+ }
+
+ @Test(timeout = 60000)
+ public void testNotAuthenticated() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(getAuthenticationHandlerConfiguration());
+ auth.start();
+ try {
+ URL url = new URL(auth.getBaseURL());
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.connect();
+ assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
+ assertTrue(conn.getHeaderField(KerberosAuthenticator.WWW_AUTHENTICATE) != null);
+ } finally {
+ auth.stop();
+ }
+ }
+
+ @Test(timeout = 60000)
+ public void testAuthentication() throws Exception {
+ final AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration());
+ KerberosTestUtils.doAsClient(new Callable() {
+ @Override
+ public Void call() throws Exception {
+ auth._testAuthentication(new KerberosAuthenticator(), false);
+ return null;
+ }
+ });
+ }
+
+ @Test(timeout = 60000)
+ public void testAuthenticationPost() throws Exception {
+ final AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration());
+ KerberosTestUtils.doAsClient(new Callable() {
+ @Override
+ public Void call() throws Exception {
+ auth._testAuthentication(new KerberosAuthenticator(), true);
+ return null;
+ }
+ });
+ }
+
+ @Test(timeout = 60000)
+ public void testAuthenticationHttpClient() throws Exception {
+ final AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration());
+ KerberosTestUtils.doAsClient(new Callable() {
+ @Override
+ public Void call() throws Exception {
+ auth._testAuthenticationHttpClient(new KerberosAuthenticator(), false);
+ return null;
+ }
+ });
+ }
+
+ @Test(timeout = 60000)
+ public void testAuthenticationHttpClientPost() throws Exception {
+ final AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration());
+ KerberosTestUtils.doAsClient(new Callable() {
+ @Override
+ public Void call() throws Exception {
+ auth._testAuthenticationHttpClient(new KerberosAuthenticator(), true);
+ return null;
+ }
+ });
+ }
+
+ @Test(timeout = 60000)
+ public void testNotAuthenticatedWithMultiAuthHandler() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase
+ .setAuthenticationHandlerConfig(getMultiAuthHandlerConfiguration());
+ auth.start();
+ try {
+ URL url = new URL(auth.getBaseURL());
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.connect();
+ assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED,
+ conn.getResponseCode());
+ assertTrue(conn
+ .getHeaderField(KerberosAuthenticator.WWW_AUTHENTICATE) != null);
+ } finally {
+ auth.stop();
+ }
+ }
+
+ @Test(timeout = 60000)
+ public void testAuthenticationWithMultiAuthHandler() throws Exception {
+ final AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase
+ .setAuthenticationHandlerConfig(getMultiAuthHandlerConfiguration());
+ KerberosTestUtils.doAsClient(new Callable() {
+ @Override
+ public Void call() throws Exception {
+ auth._testAuthentication(new KerberosAuthenticator(), false);
+ return null;
+ }
+ });
+ }
+
+ @Test(timeout = 60000)
+ public void testAuthenticationHttpClientPostWithMultiAuthHandler()
+ throws Exception {
+ final AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase
+ .setAuthenticationHandlerConfig(getMultiAuthHandlerConfiguration());
+ KerberosTestUtils.doAsClient(new Callable() {
+ @Override
+ public Void call() throws Exception {
+ auth._testAuthenticationHttpClient(new KerberosAuthenticator(), true);
+ return null;
+ }
+ });
+ }
+
+ @Test(timeout = 60000)
+ public void testNegotiate() throws NoSuchMethodException, InvocationTargetException,
+ IllegalAccessException, IOException {
+ KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator();
+
+ HttpURLConnection conn = mock(HttpURLConnection.class);
+ when(conn.getHeaderField(KerberosAuthenticator.WWW_AUTHENTICATE)).
+ thenReturn(KerberosAuthenticator.NEGOTIATE);
+ when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED);
+
+ Method method = KerberosAuthenticator.class.getDeclaredMethod("isNegotiate",
+ HttpURLConnection.class);
+ method.setAccessible(true);
+
+ assertTrue((boolean)method.invoke(kerberosAuthenticator, conn));
+ }
+
+ @Test(timeout = 60000)
+ public void testNegotiateLowerCase() throws NoSuchMethodException, InvocationTargetException,
+ IllegalAccessException, IOException {
+ KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator();
+
+ HttpURLConnection conn = mock(HttpURLConnection.class);
+ when(conn.getHeaderField("www-authenticate"))
+ .thenReturn(KerberosAuthenticator.NEGOTIATE);
+ when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED);
+
+ Method method = KerberosAuthenticator.class.getDeclaredMethod("isNegotiate",
+ HttpURLConnection.class);
+ method.setAccessible(true);
+
+ assertTrue((boolean)method.invoke(kerberosAuthenticator, conn));
+ }
+
+ @Test(timeout = 60000)
+ public void testReadToken() throws NoSuchMethodException, IOException, IllegalAccessException,
+ InvocationTargetException {
+ KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator();
+ FieldUtils.writeField(kerberosAuthenticator, "base64", new Base64(), true);
+
+ Base64 base64 = new Base64();
+
+ HttpURLConnection conn = mock(HttpURLConnection.class);
+ when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED);
+ when(conn.getHeaderField(KerberosAuthenticator.WWW_AUTHENTICATE))
+ .thenReturn(KerberosAuthenticator.NEGOTIATE + " " +
+ Arrays.toString(base64.encode("foobar".getBytes())));
+
+ Method method = KerberosAuthenticator.class.getDeclaredMethod("readToken",
+ HttpURLConnection.class);
+ method.setAccessible(true);
+
+ method.invoke(kerberosAuthenticator, conn); // expecting this not to throw an exception
+ }
+
+ @Test(timeout = 60000)
+ public void testReadTokenLowerCase() throws NoSuchMethodException, IOException,
+ IllegalAccessException, InvocationTargetException {
+ KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator();
+ FieldUtils.writeField(kerberosAuthenticator, "base64", new Base64(), true);
+
+ Base64 base64 = new Base64();
+
+ HttpURLConnection conn = mock(HttpURLConnection.class);
+ when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED);
+ when(conn.getHeaderField("www-authenticate"))
+ .thenReturn(KerberosAuthenticator.NEGOTIATE +
+ Arrays.toString(base64.encode("foobar".getBytes())));
+
+ Method method = KerberosAuthenticator.class.getDeclaredMethod("readToken",
+ HttpURLConnection.class);
+ method.setAccessible(true);
+
+ method.invoke(kerberosAuthenticator, conn); // expecting this not to throw an exception
+ }
+}
diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestPseudoAuthenticator.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestPseudoAuthenticator.java
new file mode 100644
index 000000000000..1ab04e65b707
--- /dev/null
+++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestPseudoAuthenticator.java
@@ -0,0 +1,120 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.client;
+
+import org.apache.hadoop.security.authentication.client.PseudoAuthenticator;
+import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter;
+import org.apache.hadoop.hbase.security.authentication.server.PseudoAuthenticationHandler;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Properties;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.ClassRule;
+import org.junit.experimental.categories.Category;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+
+@Category({ MiscTests.class, SmallTests.class })
+public class TestPseudoAuthenticator {
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestPseudoAuthenticator.class);
+
+ private Properties getAuthenticationHandlerConfiguration(boolean anonymousAllowed) {
+ Properties props = new Properties();
+ props.setProperty(AuthenticationFilter.AUTH_TYPE, "simple");
+ props.setProperty(PseudoAuthenticationHandler.ANONYMOUS_ALLOWED, Boolean.toString(anonymousAllowed));
+ return props;
+ }
+
+ @Test
+ public void testGetUserName() throws Exception {
+ PseudoAuthenticator authenticator = new PseudoAuthenticator();
+ // TODO getUserName() has protected access
+ // assertEquals(System.getProperty("user.name"), authenticator.getUserName());
+ }
+
+ @Test
+ public void testAnonymousAllowed() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration(true));
+ auth.start();
+ try {
+ URL url = new URL(auth.getBaseURL());
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.connect();
+ assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
+ } finally {
+ auth.stop();
+ }
+ }
+
+ @Test
+ public void testAnonymousDisallowed() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration(false));
+ auth.start();
+ try {
+ URL url = new URL(auth.getBaseURL());
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.connect();
+ assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
+ assertTrue(conn.getHeaderFields().containsKey("WWW-Authenticate"));
+ //assertEquals("Authentication required", conn.getResponseMessage());
+ // TODO: Was support to be "Authentication required"
+ assertEquals("Unauthorized", conn.getResponseMessage());
+ } finally {
+ auth.stop();
+ }
+ }
+
+ @Test
+ public void testAuthenticationAnonymousAllowed() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration(true));
+ auth._testAuthentication(new PseudoAuthenticator(), false);
+ }
+
+ @Test
+ public void testAuthenticationAnonymousDisallowed() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration(false));
+ auth._testAuthentication(new PseudoAuthenticator(), false);
+ }
+
+ @Test
+ public void testAuthenticationAnonymousAllowedWithPost() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration(true));
+ auth._testAuthentication(new PseudoAuthenticator(), true);
+ }
+
+ @Test
+ public void testAuthenticationAnonymousDisallowedWithPost() throws Exception {
+ AuthenticatorTestCase auth = new AuthenticatorTestCase();
+ AuthenticatorTestCase.setAuthenticationHandlerConfig(
+ getAuthenticationHandlerConfiguration(false));
+ auth._testAuthentication(new PseudoAuthenticator(), true);
+ }
+
+}
diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/server/TestAuthenticationFilter.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/server/TestAuthenticationFilter.java
new file mode 100644
index 000000000000..7ba0ea1c4672
--- /dev/null
+++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/server/TestAuthenticationFilter.java
@@ -0,0 +1,1332 @@
+/**
+ * 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. See accompanying LICENSE file.
+ */
+package org.apache.hadoop.hbase.security.authentication.server;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.net.HttpCookie;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Vector;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.hbase.security.authentication.server.AuthenticationToken;
+import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter;
+import org.apache.hadoop.hbase.security.authentication.util.Signer;
+import org.apache.hadoop.hbase.security.authentication.util.SignerSecretProvider;
+import org.apache.hadoop.hbase.security.authentication.util.StringSignerSecretProviderCreator;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.reset;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.ClassRule;
+import org.junit.experimental.categories.Category;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+
+@Category({ MiscTests.class, SmallTests.class })
+public class TestAuthenticationFilter {
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestAuthenticationFilter.class);
+
+ private static final long TOKEN_VALIDITY_SEC = 1000;
+ private static final long TOKEN_MAX_INACTIVE_INTERVAL = 1000;
+
+ @Test
+ public void testGetConfiguration() throws Exception {
+ AuthenticationFilter filter = new AuthenticationFilter();
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter(AuthenticationFilter.CONFIG_PREFIX)).thenReturn("");
+ when(config.getInitParameter("a")).thenReturn("A");
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(Arrays.asList("a")).elements());
+ Properties props = filter.getConfiguration("", config);
+ assertEquals("A", props.getProperty("a"));
+
+ config = mock(FilterConfig.class);
+ when(config.getInitParameter(AuthenticationFilter.CONFIG_PREFIX)).thenReturn("foo");
+ when(config.getInitParameter("foo.a")).thenReturn("A");
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(Arrays.asList("foo.a")).elements());
+ props = filter.getConfiguration("foo.", config);
+ assertEquals("A", props.getProperty("a"));
+ }
+
+ @Test
+ public void testInitEmpty() throws Exception {
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameterNames()).thenReturn(new Vector().elements());
+ filter.init(config);
+ fail();
+ } catch (ServletException ex) {
+ // Expected
+ assertEquals("Authentication type must be specified: simple|kerberos|",
+ ex.getMessage());
+ } catch (Exception ex) {
+ fail();
+ } finally {
+ filter.destroy();
+ }
+ }
+
+ public static class DummyAuthenticationHandler implements AuthenticationHandler {
+ public static boolean init;
+ public static boolean managementOperationReturn;
+ public static boolean destroy;
+ public static boolean expired;
+
+ public static final String TYPE = "dummy";
+
+ public static void reset() {
+ init = false;
+ destroy = false;
+ }
+
+ @Override
+ public void init(Properties config) throws ServletException {
+ init = true;
+ managementOperationReturn =
+ config.getProperty("management.operation.return", "true").equals("true");
+ expired = config.getProperty("expired.token", "false").equals("true");
+ }
+
+ @Override
+ public boolean managementOperation(AuthenticationToken token,
+ HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ if (!managementOperationReturn) {
+ response.setStatus(HttpServletResponse.SC_ACCEPTED);
+ }
+ return managementOperationReturn;
+ }
+
+ @Override
+ public void destroy() {
+ destroy = true;
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+
+ @Override
+ public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ AuthenticationToken token = null;
+ String param = request.getParameter("authenticated");
+ if (param != null && param.equals("true")) {
+ token = new AuthenticationToken("u", "p", "t");
+ token.setExpires((expired) ? 0 : System.currentTimeMillis() + TOKEN_VALIDITY_SEC);
+ } else {
+ if (request.getHeader("WWW-Authenticate") == null) {
+ response.setHeader("WWW-Authenticate", "dummyauth");
+ } else {
+ throw new AuthenticationException("AUTH FAILED");
+ }
+ }
+ return token;
+ }
+ }
+
+ @Test
+ public void testFallbackToRandomSecretProvider() throws Exception {
+ // minimal configuration & simple auth handler (Pseudo)
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("simple");
+ when(config.getInitParameter(
+ AuthenticationFilter.AUTH_TOKEN_VALIDITY)).thenReturn(
+ (new Long(TOKEN_VALIDITY_SEC)).toString());
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector<>(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ AuthenticationFilter.AUTH_TOKEN_VALIDITY)).elements());
+ ServletContext context = mock(ServletContext.class);
+ when(context.getAttribute(AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE))
+ .thenReturn(null);
+ when(config.getServletContext()).thenReturn(context);
+ filter.init(config);
+ assertEquals(PseudoAuthenticationHandler.class, filter.getAuthenticationHandler().getClass());
+ assertTrue(filter.isRandomSecret());
+ assertFalse(filter.isCustomSignerSecretProvider());
+ assertNull(filter.getCookieDomain());
+ assertNull(filter.getCookiePath());
+ assertEquals(TOKEN_VALIDITY_SEC, filter.getValidity());
+ } finally {
+ filter.destroy();
+ }
+ }
+ @Test
+ public void testInit() throws Exception {
+ // custom secret as inline
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("simple");
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector<>(Arrays.asList(AuthenticationFilter.AUTH_TYPE))
+ .elements());
+ ServletContext context = mock(ServletContext.class);
+ when(context.getAttribute(
+ AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)).thenReturn(
+ new SignerSecretProvider() {
+ @Override
+ public void init(Properties config, ServletContext servletContext,
+ long tokenValidity) {
+ }
+ @Override
+ public byte[] getCurrentSecret() {
+ return null;
+ }
+ @Override
+ public byte[][] getAllSecrets() {
+ return null;
+ }
+ });
+ when(config.getServletContext()).thenReturn(context);
+ filter.init(config);
+ assertFalse(filter.isRandomSecret());
+ assertTrue(filter.isCustomSignerSecretProvider());
+ } finally {
+ filter.destroy();
+ }
+
+ // custom secret by file
+ File testDir = new File(System.getProperty("test.build.data",
+ "target/test-dir"));
+ testDir.mkdirs();
+ String secretValue = "hadoop";
+ File secretFile = new File(testDir, "http-secret.txt");
+ Writer writer = new FileWriter(secretFile);
+ writer.write(secretValue);
+ writer.close();
+
+ filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter(
+ AuthenticationFilter.AUTH_TYPE)).thenReturn("simple");
+ when(config.getInitParameter(
+ AuthenticationFilter.SIGNATURE_SECRET_FILE))
+ .thenReturn(secretFile.getAbsolutePath());
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ AuthenticationFilter.SIGNATURE_SECRET_FILE)).elements());
+ ServletContext context = mock(ServletContext.class);
+ when(context.getAttribute(
+ AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE))
+ .thenReturn(null);
+ when(config.getServletContext()).thenReturn(context);
+ filter.init(config);
+ assertFalse(filter.isRandomSecret());
+ assertFalse(filter.isCustomSignerSecretProvider());
+ } finally {
+ filter.destroy();
+ }
+
+ // custom cookie domain and cookie path
+ filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("simple");
+ when(config.getInitParameter(AuthenticationFilter.COOKIE_DOMAIN)).thenReturn(".foo.com");
+ when(config.getInitParameter(AuthenticationFilter.COOKIE_PATH)).thenReturn("/bar");
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ AuthenticationFilter.COOKIE_DOMAIN,
+ AuthenticationFilter.COOKIE_PATH)).elements());
+ getMockedServletContextWithStringSigner(config);
+ filter.init(config);
+ assertEquals(".foo.com", filter.getCookieDomain());
+ assertEquals("/bar", filter.getCookiePath());
+ } finally {
+ filter.destroy();
+ }
+
+ // authentication handler lifecycle, and custom impl
+ DummyAuthenticationHandler.reset();
+ filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter("management.operation.return")).
+ thenReturn("true");
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn(
+ DummyAuthenticationHandler.class.getName());
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(
+ Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ "management.operation.return")).elements());
+ getMockedServletContextWithStringSigner(config);
+ filter.init(config);
+ assertTrue(DummyAuthenticationHandler.init);
+ } finally {
+ filter.destroy();
+ assertTrue(DummyAuthenticationHandler.destroy);
+ }
+
+ // kerberos auth handler
+ filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ ServletContext sc = mock(ServletContext.class);
+ when(config.getServletContext()).thenReturn(sc);
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("kerberos");
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE)).elements());
+ filter.init(config);
+ } catch (ServletException ex) {
+ // Expected
+ } finally {
+ assertEquals(KerberosAuthenticationHandler.class,
+ filter.getAuthenticationHandler().getClass());
+ filter.destroy();
+ }
+ }
+
+ @Test
+ public void testEmptySecretFileFallbacksToRandomSecret() throws Exception {
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter(
+ AuthenticationFilter.AUTH_TYPE)).thenReturn("simple");
+ File secretFile = File.createTempFile("test_empty_secret", ".txt");
+ secretFile.deleteOnExit();
+ assertTrue(secretFile.exists());
+ when(config.getInitParameter(
+ AuthenticationFilter.SIGNATURE_SECRET_FILE))
+ .thenReturn(secretFile.getAbsolutePath());
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector<>(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ AuthenticationFilter.SIGNATURE_SECRET_FILE)).elements());
+ ServletContext context = mock(ServletContext.class);
+ when(context.getAttribute(
+ AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE))
+ .thenReturn(null);
+ when(config.getServletContext()).thenReturn(context);
+ filter.init(config);
+ assertTrue(filter.isRandomSecret());
+ } finally {
+ filter.destroy();
+ }
+ }
+
+ @Test
+ public void testInitCaseSensitivity() throws Exception {
+ // minimal configuration & simple auth handler (Pseudo)
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("SimPle");
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TOKEN_VALIDITY)).thenReturn(
+ (new Long(TOKEN_VALIDITY_SEC)).toString());
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ AuthenticationFilter.AUTH_TOKEN_VALIDITY)).elements());
+ getMockedServletContextWithStringSigner(config);
+
+ filter.init(config);
+ assertEquals(PseudoAuthenticationHandler.class,
+ filter.getAuthenticationHandler().getClass());
+ } finally {
+ filter.destroy();
+ }
+ }
+
+ @Test
+ public void testGetRequestURL() throws Exception {
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter("management.operation.return")).
+ thenReturn("true");
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn(
+ DummyAuthenticationHandler.class.getName());
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(
+ Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ "management.operation.return")).elements());
+ getMockedServletContextWithStringSigner(config);
+ filter.init(config);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar"));
+ when(request.getQueryString()).thenReturn("a=A&b=B");
+
+ assertEquals("http://foo:8080/bar?a=A&b=B", filter.getRequestURL(request));
+ } finally {
+ filter.destroy();
+ }
+ }
+
+ @Test
+ public void testGetToken() throws Exception {
+ AuthenticationFilter filter = new AuthenticationFilter();
+
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter("management.operation.return")).
+ thenReturn("true");
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn(
+ DummyAuthenticationHandler.class.getName());
+ when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn("secret");
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(
+ Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ AuthenticationFilter.SIGNATURE_SECRET,
+ "management.operation.return")).elements());
+ SignerSecretProvider secretProvider =
+ getMockedServletContextWithStringSigner(config);
+ filter.init(config);
+
+ AuthenticationToken token = new AuthenticationToken("u", "p", DummyAuthenticationHandler.TYPE);
+ token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC);
+
+ Signer signer = new Signer(secretProvider);
+ String tokenSigned = signer.sign(token.toString());
+
+ Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned);
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getCookies()).thenReturn(new Cookie[]{cookie});
+
+ AuthenticationToken newToken = filter.getToken(request);
+
+ assertEquals(token.toString(), newToken.toString());
+ } finally {
+ filter.destroy();
+ }
+ }
+
+ @Test
+ public void testGetTokenExpired() throws Exception {
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter("management.operation.return")).thenReturn("true");
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn(
+ DummyAuthenticationHandler.class.getName());
+ when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn("secret");
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(
+ Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ AuthenticationFilter.SIGNATURE_SECRET,
+ "management.operation.return")).elements());
+ getMockedServletContextWithStringSigner(config);
+ filter.init(config);
+
+ AuthenticationToken token =
+ new AuthenticationToken("u", "p", DummyAuthenticationHandler.TYPE);
+ token.setExpires(System.currentTimeMillis() - TOKEN_VALIDITY_SEC);
+ SignerSecretProvider secretProvider =
+ StringSignerSecretProviderCreator.newStringSignerSecretProvider();
+ Properties secretProviderProps = new Properties();
+ secretProviderProps.setProperty(
+ AuthenticationFilter.SIGNATURE_SECRET, "secret");
+ secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC);
+ Signer signer = new Signer(secretProvider);
+ String tokenSigned = signer.sign(token.toString());
+
+ Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned);
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getCookies()).thenReturn(new Cookie[]{cookie});
+
+ boolean failed = false;
+ try {
+ filter.getToken(request);
+ } catch (AuthenticationException ex) {
+ assertEquals("AuthenticationToken expired", ex.getMessage());
+ failed = true;
+ } finally {
+ assertTrue(failed);
+ }
+ } finally {
+ filter.destroy();
+ }
+ }
+
+ @Test
+ public void testGetTokenInvalidType() throws Exception {
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter("management.operation.return")).
+ thenReturn("true");
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn(
+ DummyAuthenticationHandler.class.getName());
+ when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn("secret");
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(
+ Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ AuthenticationFilter.SIGNATURE_SECRET,
+ "management.operation.return")).elements());
+ getMockedServletContextWithStringSigner(config);
+ filter.init(config);
+
+ AuthenticationToken token = new AuthenticationToken("u", "p", "invalidtype");
+ token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC);
+ SignerSecretProvider secretProvider =
+ StringSignerSecretProviderCreator.newStringSignerSecretProvider();
+ Properties secretProviderProps = new Properties();
+ secretProviderProps.setProperty(
+ AuthenticationFilter.SIGNATURE_SECRET, "secret");
+ secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC);
+ Signer signer = new Signer(secretProvider);
+ String tokenSigned = signer.sign(token.toString());
+
+ Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned);
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getCookies()).thenReturn(new Cookie[]{cookie});
+
+ boolean failed = false;
+ try {
+ filter.getToken(request);
+ } catch (AuthenticationException ex) {
+ assertEquals("Invalid AuthenticationToken type", ex.getMessage());
+ failed = true;
+ } finally {
+ assertTrue(failed);
+ }
+ } finally {
+ filter.destroy();
+ }
+ }
+
+ private static SignerSecretProvider getMockedServletContextWithStringSigner(
+ FilterConfig config) throws Exception {
+ Properties secretProviderProps = new Properties();
+ secretProviderProps.setProperty(AuthenticationFilter.SIGNATURE_SECRET,
+ "secret");
+ SignerSecretProvider secretProvider =
+ StringSignerSecretProviderCreator.newStringSignerSecretProvider();
+ secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC);
+
+ ServletContext context = mock(ServletContext.class);
+ when(context.getAttribute(
+ AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE))
+ .thenReturn(secretProvider);
+ when(config.getServletContext()).thenReturn(context);
+ return secretProvider;
+ }
+
+ @Test
+ public void testDoFilterNotAuthenticated() throws Exception {
+ AuthenticationFilter filter = new AuthenticationFilter();
+ try {
+ FilterConfig config = mock(FilterConfig.class);
+ when(config.getInitParameter("management.operation.return")).
+ thenReturn("true");
+ when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn(
+ DummyAuthenticationHandler.class.getName());
+ when(config.getInitParameterNames()).thenReturn(
+ new Vector(
+ Arrays.asList(AuthenticationFilter.AUTH_TYPE,
+ "management.operation.return")).elements());
+ getMockedServletContextWithStringSigner(config);
+ filter.init(config);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar"));
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ FilterChain chain = mock(FilterChain.class);
+
+ doAnswer(
+ new Answer