/** * 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.security.authentication.server; import org.apache.hadoop.security.authentication.client.AuthenticatedURL; import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authentication.util.Signer; import org.apache.hadoop.security.authentication.util.SignerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; 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.io.IOException; import java.security.Principal; import java.util.Enumeration; import java.util.Properties; import java.util.Random; /** * The {@link AuthenticationFilter} enables protecting web application resources with different (pluggable) * authentication mechanisms. * <p/> * Out of the box it provides 2 authentication mechanisms: Pseudo and Kerberos SPNEGO. * <p/> * Additional authentication mechanisms are supported via the {@link AuthenticationHandler} interface. * <p/> * 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. * <p/> * The supported configuration properties are: * <ul> * <li>config.prefix: indicates the prefix to be used by all other configuration properties, the default value * is no prefix. See below for details on how/why this prefix is used.</li> * <li>[#PREFIX#.]type: simple|kerberos|#CLASS#, 'simple' is short for the * {@link PseudoAuthenticationHandler}, 'kerberos' is short for {@link KerberosAuthenticationHandler}, otherwise * the full class name of the {@link AuthenticationHandler} must be specified.</li> * <li>[#PREFIX#.]signature.secret: the secret used to sign the HTTP cookie value. The default value is a random * value. Unless multiple webapp instances need to share the secret the random value is adequate.</li> * <li>[#PREFIX#.]token.validity: time -in seconds- that the generated token is valid before a * new authentication is triggered, default value is <code>3600</code> seconds.</li> * <li>[#PREFIX#.]cookie.domain: domain to use for the HTTP cookie that stores the authentication token.</li> * <li>[#PREFIX#.]cookie.path: path to use for the HTTP cookie that stores the authentication token.</li> * </ul> * <p/> * 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. */ 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"; /** * 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"; private static final Random RAN = new Random(); private Signer signer; private AuthenticationHandler authHandler; private boolean randomSecret; private long validity; private String cookieDomain; private String cookiePath; /** * Initializes the authentication filter. * <p/> * It instantiates and initializes the specified {@link AuthenticationHandler}. * <p/> * * @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 + "." : ""; Properties config = getConfiguration(configPrefix, filterConfig); String authHandlerName = config.getProperty(AUTH_TYPE, null); String authHandlerClassName; if (authHandlerName == null) { throw new ServletException("Authentication type must be specified: simple|kerberos|<class>"); } if (authHandlerName.equals("simple")) { authHandlerClassName = PseudoAuthenticationHandler.class.getName(); } else if (authHandlerName.equals("kerberos")) { authHandlerClassName = KerberosAuthenticationHandler.class.getName(); } else { authHandlerClassName = authHandlerName; } try { Class<?> klass = Thread.currentThread().getContextClassLoader().loadClass(authHandlerClassName); authHandler = (AuthenticationHandler) klass.newInstance(); authHandler.init(config); } catch (ClassNotFoundException ex) { throw new ServletException(ex); } catch (InstantiationException ex) { throw new ServletException(ex); } catch (IllegalAccessException ex) { throw new ServletException(ex); } String signatureSecret = config.getProperty(configPrefix + SIGNATURE_SECRET); if (signatureSecret == null) { signatureSecret = Long.toString(RAN.nextLong()); randomSecret = true; LOG.warn("'signature.secret' configuration not set, using a random value as secret"); } signer = new Signer(signatureSecret.getBytes()); validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, "36000")) * 1000; //10 hours cookieDomain = config.getProperty(COOKIE_DOMAIN, null); cookiePath = config.getProperty(COOKIE_PATH, null); } /** * 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 randomSecret; } /** * 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; } /** * Destroys the filter. * <p/> * 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; } } /** * 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}. * <p/> * 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. * <p/> * 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. * <p/> * 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. * <p/> * If this method returns <code>null</code> 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, <code>null</code> 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(); try { tokenStr = signer.verifyAndExtract(tokenStr); } catch (SignerException ex) { throw new AuthenticationException(ex); } break; } } } if (tokenStr != null) { token = AuthenticationToken.parse(tokenStr); if (!token.getType().equals(authHandler.getType())) { throw new AuthenticationException("Invalid AuthenticationToken type"); } if (token.isExpired()) { throw new AuthenticationException("AuthenticationToken expired"); } } return token; } /** * 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; String unauthorizedMsg = ""; HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; try { boolean newToken = false; AuthenticationToken token; try { token = getToken(httpRequest); } catch (AuthenticationException ex) { LOG.warn("AuthenticationToken ignored: " + ex.getMessage()); token = null; } if (authHandler.managementOperation(token, httpRequest, httpResponse)) { if (token == null) { if (LOG.isDebugEnabled()) { LOG.debug("Request [{}] triggering authentication", getRequestURL(httpRequest)); } token = authHandler.authenticate(httpRequest, httpResponse); if (token != null && token.getExpires() != 0 && token != AuthenticationToken.ANONYMOUS) { 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 (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) { String signedToken = signer.sign(token.toString()); Cookie cookie = createCookie(signedToken); httpResponse.addCookie(cookie); } filterChain.doFilter(httpRequest, httpResponse); } } else { unauthorizedResponse = false; } } catch (AuthenticationException ex) { unauthorizedMsg = ex.toString(); LOG.warn("Authentication exception: " + ex.getMessage(), ex); } if (unauthorizedResponse) { if (!httpResponse.isCommitted()) { Cookie cookie = createCookie(""); cookie.setMaxAge(0); httpResponse.addCookie(cookie); httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, unauthorizedMsg); } } } /** * Creates the Hadoop authentiation HTTP cookie. * <p/> * It sets the domain and path specified in the configuration. * * @param token authentication token for the cookie. * * @return the HTTP cookie. */ protected Cookie createCookie(String token) { Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, token); if (getCookieDomain() != null) { cookie.setDomain(getCookieDomain()); } if (getCookiePath() != null) { cookie.setPath(getCookiePath()); } return cookie; } }