diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java index bf44f48ca2..e0da38b424 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java @@ -145,6 +145,13 @@ public class AuthenticationFilter implements Filter { 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.MaxInactiveInterval"; + /** * Constant for the configuration property that indicates the validity of the generated token. */ @@ -190,6 +197,7 @@ public class AuthenticationFilter implements Filter { private Signer signer; private SignerSecretProvider secretProvider; private AuthenticationHandler authHandler; + private long maxInactiveInterval; private long validity; private String cookieDomain; private String cookiePath; @@ -227,6 +235,8 @@ public void init(FilterConfig filterConfig) throws ServletException { authHandlerClassName = authHandlerName; } + maxInactiveInterval = Long.parseLong(config.getProperty( + AUTH_TOKEN_MAX_INACTIVE_INTERVAL, "1800")) * 1000; // 30 minutes; validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, "36000")) * 1000; //10 hours initializeSecretProvider(filterConfig); @@ -354,6 +364,15 @@ protected boolean isCustomSignerSecretProvider() { .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. * @@ -510,8 +529,10 @@ protected AuthenticationToken getToken(HttpServletRequest request) throws IOExce * @throws ServletException thrown if a processing error occurred. */ @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) - throws IOException, ServletException { + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain filterChain) + throws IOException, ServletException { boolean unauthorizedResponse = true; int errCode = HttpServletResponse.SC_UNAUTHORIZED; AuthenticationException authenticationEx = null; @@ -533,19 +554,27 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha if (authHandler.managementOperation(token, httpRequest, httpResponse)) { if (token == null) { if (LOG.isDebugEnabled()) { - LOG.debug("Request [{}] triggering authentication", getRequestURL(httpRequest)); + 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); + 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()); + LOG.debug("Request [{}] user [{}] authenticated", + getRequestURL(httpRequest), token.getUserName()); } final AuthenticationToken authToken = token; httpRequest = new HttpServletRequestWrapper(httpRequest) { @@ -562,10 +591,22 @@ public String getRemoteUser() { @Override public Principal getUserPrincipal() { - return (authToken != AuthenticationToken.ANONYMOUS) ? authToken : null; + return (authToken != AuthenticationToken.ANONYMOUS) ? + authToken : null; } }; - if (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) { + + // 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); + newToken = true; + } + if (newToken && !token.isExpired() + && token != AuthenticationToken.ANONYMOUS) { String signedToken = signer.sign(token.toString()); createAuthCookie(httpResponse, signedToken, getCookieDomain(), getCookiePath(), token.getExpires(), @@ -628,12 +669,10 @@ protected void doFilter(FilterChain filterChain, HttpServletRequest request, * @param resp the response object. * @param token authentication token for the cookie. * @param domain the cookie domain. - * @param path the cokie path. + * @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 token the token. - * @param expires the cookie expiration time. * @param isCookiePersistent whether the cookie is persistent or not. * * XXX the following code duplicate some logic in Jetty / Servlet API, diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java index 0e2b45d9de..6303c956ab 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java @@ -42,6 +42,7 @@ private AuthenticationToken() { private AuthenticationToken(AuthToken token) { super(token.getUserName(), token.getName(), token.getType()); + setMaxInactives(token.getMaxInactives()); setExpires(token.getExpires()); } @@ -58,6 +59,17 @@ public AuthenticationToken(String userName, String principal, String type) { super(userName, principal, type); } + /** + * Sets the max inactive time of the token. + * + * @param max 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. * diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/AuthToken.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/AuthToken.java index 7269eb2744..870b267030 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/AuthToken.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/AuthToken.java @@ -34,15 +34,18 @@ public class AuthToken implements Principal { private static final String ATTR_SEPARATOR = "&"; private static final String USER_NAME = "u"; private static final String PRINCIPAL = "p"; + private static final String MAX_INACTIVES = "i"; private static final String EXPIRES = "e"; private static final String TYPE = "t"; private final static Set ATTRIBUTES = - new HashSet(Arrays.asList(USER_NAME, PRINCIPAL, EXPIRES, TYPE)); + new HashSet(Arrays.asList(USER_NAME, PRINCIPAL, + MAX_INACTIVES, EXPIRES, TYPE)); private String userName; private String principal; private String type; + private long maxInactives; private long expires; private String tokenStr; @@ -50,6 +53,7 @@ protected AuthToken() { userName = null; principal = null; type = null; + maxInactives = -1; expires = -1; tokenStr = "ANONYMOUS"; generateToken(); @@ -73,6 +77,7 @@ public AuthToken(String userName, String principal, String type) { this.userName = userName; this.principal = principal; this.type = type; + this.maxInactives = -1; this.expires = -1; } @@ -88,6 +93,15 @@ protected static void checkForIllegalArgument(String value, String name) { } } + /** + * Sets the max inactive interval of the token. + * + * @param max inactive interval of the token in milliseconds since the epoch. + */ + public void setMaxInactives(long interval) { + this.maxInactives = interval; + } + /** * Sets the expiration of the token. * @@ -104,7 +118,10 @@ public void setExpires(long expires) { * @return true if the token has expired. */ public boolean isExpired() { - return getExpires() != -1 && System.currentTimeMillis() > getExpires(); + return (getMaxInactives() != -1 && + System.currentTimeMillis() > getMaxInactives()) + || (getExpires() != -1 && + System.currentTimeMillis() > getExpires()); } /** @@ -115,6 +132,8 @@ private void generateToken() { sb.append(USER_NAME).append("=").append(getUserName()).append(ATTR_SEPARATOR); sb.append(PRINCIPAL).append("=").append(getName()).append(ATTR_SEPARATOR); sb.append(TYPE).append("=").append(getType()).append(ATTR_SEPARATOR); + sb.append(MAX_INACTIVES).append("=") + .append(getMaxInactives()).append(ATTR_SEPARATOR); sb.append(EXPIRES).append("=").append(getExpires()); tokenStr = sb.toString(); } @@ -147,6 +166,15 @@ public String getType() { return type; } + /** + * Returns the max inactive time of the token. + * + * @return the max inactive time of the token, in milliseconds since Epoc. + */ + public long getMaxInactives() { + return maxInactives; + } + /** * Returns the expiration time of the token. * @@ -183,8 +211,10 @@ public static AuthToken parse(String tokenStr) throws AuthenticationException { if (!map.keySet().equals(ATTRIBUTES)) { throw new AuthenticationException("Invalid token string, missing attributes"); } + long maxInactives = Long.parseLong(map.get(MAX_INACTIVES)); long expires = Long.parseLong(map.get(EXPIRES)); AuthToken token = new AuthToken(map.get(USER_NAME), map.get(PRINCIPAL), map.get(TYPE)); + token.setMaxInactives(maxInactives); token.setExpires(expires); return token; } diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationFilter.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationFilter.java index 63b812d5e3..a6176902b7 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationFilter.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationFilter.java @@ -18,11 +18,10 @@ import java.io.IOException; import java.io.Writer; import java.net.HttpCookie; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Enumeration; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Vector; @@ -53,6 +52,7 @@ public class TestAuthenticationFilter { private static final long TOKEN_VALIDITY_SEC = 1000; + private static final long TOKEN_MAX_INACTIVE_INTERVAL = 1000; @Test public void testGetConfiguration() throws Exception { @@ -595,7 +595,7 @@ private void _testDoFilterAuthentication(boolean withDomainPath, HttpServletResponse response = Mockito.mock(HttpServletResponse.class); FilterChain chain = Mockito.mock(FilterChain.class); - final HashMap cookieMap = new HashMap(); + final Map cookieMap = new HashMap(); Mockito.doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -644,7 +644,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } } - private static void parseCookieMap(String cookieHeader, HashMap cookieMap) { List cookies = HttpCookie.parse(cookieHeader); for (HttpCookie cookie : cookies) { @@ -761,7 +761,7 @@ public void testDoFilterAuthenticationFailure() throws Exception { FilterChain chain = Mockito.mock(FilterChain.class); - final HashMap cookieMap = new HashMap(); + final Map cookieMap = new HashMap(); Mockito.doAnswer( new Answer() { @Override @@ -844,13 +844,164 @@ public void testDoFilterAuthenticatedExpired() throws Exception { } } + @Test + public void + testDoFilterAuthenticationAuthorized() throws Exception { + // Both expired period and MaxInActiveInterval are not reached. + long maxInactives = System.currentTimeMillis() + + TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + boolean authorized = true; + _testDoFilterAuthenticationMaxInactiveInterval(maxInactives, + expires, + authorized); + } + + @Test + public void + testDoFilterAuthenticationUnauthorizedExpired() throws Exception { + // Expired period is reached, MaxInActiveInterval is not reached. + long maxInactives = System.currentTimeMillis() + + TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() - TOKEN_VALIDITY_SEC; + boolean authorized = false; + _testDoFilterAuthenticationMaxInactiveInterval(maxInactives, + expires, + authorized); + } + + @Test + public void + testDoFilterAuthenticationUnauthorizedInactived() throws Exception { + // Expired period is not reached, MaxInActiveInterval is reached. + long maxInactives = System.currentTimeMillis() + - TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + boolean authorized = false; + _testDoFilterAuthenticationMaxInactiveInterval(maxInactives, + expires, + authorized); + } + + @Test + public void + testDoFilterAuthenticationUnauthorizedInactivedExpired() + throws Exception { + // Both expired period and MaxInActiveInterval is reached. + long maxInactives = System.currentTimeMillis() + - TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() - TOKEN_VALIDITY_SEC; + boolean authorized = false; + _testDoFilterAuthenticationMaxInactiveInterval(maxInactives, + expires, + authorized); + } + + private void + _testDoFilterAuthenticationMaxInactiveInterval(long maxInactives, + long expires, + boolean authorized) + throws Exception { + String secret = "secret"; + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = Mockito.mock(FilterConfig.class); + Mockito.when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + Mockito.when(config.getInitParameter( + AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + Mockito.when(config.getInitParameter( + AuthenticationFilter.SIGNATURE_SECRET)).thenReturn(secret); + Mockito.when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Mockito.when(request.getRequestURL()).thenReturn( + new StringBuffer("http://foo:8080/bar")); + + AuthenticationToken token = new AuthenticationToken("u", "p", + DummyAuthenticationHandler.TYPE); + token.setMaxInactives(maxInactives); + token.setExpires(expires); + + 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); + Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(response.containsHeader("WWW-Authenticate")) + .thenReturn(true); + FilterChain chain = Mockito.mock(FilterChain.class); + + if (authorized) { + verifyAuthorized(filter, request, response, chain); + } else { + verifyUnauthorized(filter, request, response, chain); + } + } finally { + filter.destroy(); + } + } + + private static void verifyAuthorized(AuthenticationFilter filter, + HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) throws + Exception { + final Map cookieMap = new HashMap<>(); + Mockito.doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + String cookieHeader = (String) invocation.getArguments()[1]; + parseCookieMap(cookieHeader, cookieMap); + return null; + } + }).when(response).addHeader(Mockito.eq("Set-Cookie"), Mockito.anyString()); + + filter.doFilter(request, response, chain); + + String v = cookieMap.get(AuthenticatedURL.AUTH_COOKIE); + Assert.assertNotNull("cookie missing", v); + Assert.assertTrue(v.contains("u=") && v.contains("p=") && v.contains + ("t=") && v.contains("i=") && v.contains("e=") + && v.contains("s=")); + Mockito.verify(chain).doFilter(Mockito.any(ServletRequest.class), + Mockito.any(ServletResponse.class)); + + 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 value = signer.verifyAndExtract(v); + AuthenticationToken token = AuthenticationToken.parse(value); + assertThat(token.getMaxInactives(), not(0L)); + assertThat(token.getExpires(), not(0L)); + Assert.assertFalse("Token is expired.", token.isExpired()); + } + private static void verifyUnauthorized(AuthenticationFilter filter, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - final HashMap cookieMap = new HashMap(); + final Map cookieMap = new HashMap(); Mockito.doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/HttpAuthentication.md b/hadoop-common-project/hadoop-common/src/site/markdown/HttpAuthentication.md index e0a2693a33..46daaa9e0c 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/HttpAuthentication.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/HttpAuthentication.md @@ -41,13 +41,17 @@ The following properties should be in the `core-site.xml` of all the nodes in th `hadoop.http.filter.initializers`: add to this property the `org.apache.hadoop.security.AuthenticationFilterInitializer` initializer class. -`hadoop.http.authentication.type`: Defines authentication used for the HTTP web-consoles. The supported values are: `simple` | `kerberos` | `#AUTHENTICATION_HANDLER_CLASSNAME#`. The dfeault value is `simple`. +`hadoop.http.authentication.type`: Defines authentication used for the HTTP web-consoles. The supported values are: `simple` | `kerberos` | `#AUTHENTICATION_HANDLER_CLASSNAME#`. The default value is `simple`. `hadoop.http.authentication.token.validity`: Indicates how long (in seconds) an authentication token is valid before it has to be renewed. The default value is `36000`. +`hadoop.http.authentication.token.MaxInactiveInterval`: Specifies the time, in seconds, between client requests the server will invalidate the token. The default value is `1800` (30 minutes). + `hadoop.http.authentication.signature.secret.file`: The signature secret file for signing the authentication tokens. The same secret should be used for all nodes in the cluster, JobTracker, NameNode, DataNode and TastTracker. The default value is `$user.home/hadoop-http-auth-signature-secret`. IMPORTANT: This file should be readable only by the Unix user running the daemons. -`hadoop.http.authentication.cookie.domain`: The domain to use for the HTTP cookie that stores the authentication token. In order to authentiation to work correctly across all nodes in the cluster the domain must be correctly set. There is no default value, the HTTP cookie will not have a domain working only with the hostname issuing the HTTP cookie. +`hadoop.http.authentication.cookie.domain`: The domain to use for the HTTP cookie that stores the authentication token. In order to authentication to work correctly across all nodes in the cluster the domain must be correctly set. There is no default value, the HTTP cookie will not have a domain working only with the hostname issuing the HTTP cookie. + +`hadoop.http.authentication.cookie.persistent`: Specifies the persistence of the HTTP cookie. If the value is true, the cookie is a persistent one. Otherwise, it is a session cookie. The default value is `false`(session cookie). IMPORTANT: when using IP addresses, browsers ignore cookies with domain settings. For this setting to work properly all nodes in the cluster must be configured to generate URLs with `hostname.domain` names on it.