hadoop-12050. Enable MaxInactiveInterval for hadoop http auth token. Contributed by Huizhi Lu.

(cherry picked from commit 71aedfabf3)
This commit is contained in:
Benoy Antony 2015-08-18 13:43:34 -07:00
parent 40d8faf868
commit d84e4a90d0
5 changed files with 258 additions and 22 deletions

View File

@ -145,6 +145,13 @@ public class AuthenticationFilter implements Filter {
public static final String SIGNATURE_SECRET_FILE = SIGNATURE_SECRET + ".file"; 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. * 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 Signer signer;
private SignerSecretProvider secretProvider; private SignerSecretProvider secretProvider;
private AuthenticationHandler authHandler; private AuthenticationHandler authHandler;
private long maxInactiveInterval;
private long validity; private long validity;
private String cookieDomain; private String cookieDomain;
private String cookiePath; private String cookiePath;
@ -227,6 +235,8 @@ public class AuthenticationFilter implements Filter {
authHandlerClassName = authHandlerName; 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")) validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, "36000"))
* 1000; //10 hours * 1000; //10 hours
initializeSecretProvider(filterConfig); initializeSecretProvider(filterConfig);
@ -354,6 +364,15 @@ public class AuthenticationFilter implements Filter {
.class; .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. * Returns the validity time of the generated tokens.
* *
@ -510,8 +529,10 @@ public class AuthenticationFilter implements Filter {
* @throws ServletException thrown if a processing error occurred. * @throws ServletException thrown if a processing error occurred.
*/ */
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) public void doFilter(ServletRequest request,
throws IOException, ServletException { ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
boolean unauthorizedResponse = true; boolean unauthorizedResponse = true;
int errCode = HttpServletResponse.SC_UNAUTHORIZED; int errCode = HttpServletResponse.SC_UNAUTHORIZED;
AuthenticationException authenticationEx = null; AuthenticationException authenticationEx = null;
@ -533,19 +554,27 @@ public class AuthenticationFilter implements Filter {
if (authHandler.managementOperation(token, httpRequest, httpResponse)) { if (authHandler.managementOperation(token, httpRequest, httpResponse)) {
if (token == null) { if (token == null) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Request [{}] triggering authentication", getRequestURL(httpRequest)); LOG.debug("Request [{}] triggering authentication",
getRequestURL(httpRequest));
} }
token = authHandler.authenticate(httpRequest, httpResponse); token = authHandler.authenticate(httpRequest, httpResponse);
if (token != null && token.getExpires() != 0 && if (token != null && token != AuthenticationToken.ANONYMOUS) {
token != AuthenticationToken.ANONYMOUS) { if (token.getMaxInactives() != 0) {
token.setExpires(System.currentTimeMillis() + getValidity() * 1000); token.setMaxInactives(System.currentTimeMillis()
+ getMaxInactiveInterval() * 1000);
}
if (token.getExpires() != 0) {
token.setExpires(System.currentTimeMillis()
+ getValidity() * 1000);
}
} }
newToken = true; newToken = true;
} }
if (token != null) { if (token != null) {
unauthorizedResponse = false; unauthorizedResponse = false;
if (LOG.isDebugEnabled()) { 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; final AuthenticationToken authToken = token;
httpRequest = new HttpServletRequestWrapper(httpRequest) { httpRequest = new HttpServletRequestWrapper(httpRequest) {
@ -562,10 +591,22 @@ public class AuthenticationFilter implements Filter {
@Override @Override
public Principal getUserPrincipal() { 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()); String signedToken = signer.sign(token.toString());
createAuthCookie(httpResponse, signedToken, getCookieDomain(), createAuthCookie(httpResponse, signedToken, getCookieDomain(),
getCookiePath(), token.getExpires(), getCookiePath(), token.getExpires(),
@ -628,12 +669,10 @@ public class AuthenticationFilter implements Filter {
* @param resp the response object. * @param resp the response object.
* @param token authentication token for the cookie. * @param token authentication token for the cookie.
* @param domain the cookie domain. * @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 * @param expires UNIX timestamp that indicates the expire date of the
* cookie. It has no effect if its value < 0. * cookie. It has no effect if its value < 0.
* @param isSecure is the cookie secure? * @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. * @param isCookiePersistent whether the cookie is persistent or not.
* *
* XXX the following code duplicate some logic in Jetty / Servlet API, * XXX the following code duplicate some logic in Jetty / Servlet API,

View File

@ -42,6 +42,7 @@ public class AuthenticationToken extends AuthToken {
private AuthenticationToken(AuthToken token) { private AuthenticationToken(AuthToken token) {
super(token.getUserName(), token.getName(), token.getType()); super(token.getUserName(), token.getName(), token.getType());
setMaxInactives(token.getMaxInactives());
setExpires(token.getExpires()); setExpires(token.getExpires());
} }
@ -58,6 +59,17 @@ public class AuthenticationToken extends AuthToken {
super(userName, principal, 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. * Sets the expiration of the token.
* *

View File

@ -34,15 +34,18 @@ public class AuthToken implements Principal {
private static final String ATTR_SEPARATOR = "&"; private static final String ATTR_SEPARATOR = "&";
private static final String USER_NAME = "u"; private static final String USER_NAME = "u";
private static final String PRINCIPAL = "p"; private static final String PRINCIPAL = "p";
private static final String MAX_INACTIVES = "i";
private static final String EXPIRES = "e"; private static final String EXPIRES = "e";
private static final String TYPE = "t"; private static final String TYPE = "t";
private final static Set<String> ATTRIBUTES = private final static Set<String> ATTRIBUTES =
new HashSet<String>(Arrays.asList(USER_NAME, PRINCIPAL, EXPIRES, TYPE)); new HashSet<String>(Arrays.asList(USER_NAME, PRINCIPAL,
MAX_INACTIVES, EXPIRES, TYPE));
private String userName; private String userName;
private String principal; private String principal;
private String type; private String type;
private long maxInactives;
private long expires; private long expires;
private String tokenStr; private String tokenStr;
@ -50,6 +53,7 @@ public class AuthToken implements Principal {
userName = null; userName = null;
principal = null; principal = null;
type = null; type = null;
maxInactives = -1;
expires = -1; expires = -1;
tokenStr = "ANONYMOUS"; tokenStr = "ANONYMOUS";
generateToken(); generateToken();
@ -73,6 +77,7 @@ public class AuthToken implements Principal {
this.userName = userName; this.userName = userName;
this.principal = principal; this.principal = principal;
this.type = type; this.type = type;
this.maxInactives = -1;
this.expires = -1; this.expires = -1;
} }
@ -88,6 +93,15 @@ public class AuthToken implements Principal {
} }
} }
/**
* 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. * Sets the expiration of the token.
* *
@ -104,7 +118,10 @@ public class AuthToken implements Principal {
* @return true if the token has expired. * @return true if the token has expired.
*/ */
public boolean isExpired() { public boolean isExpired() {
return getExpires() != -1 && System.currentTimeMillis() > getExpires(); return (getMaxInactives() != -1 &&
System.currentTimeMillis() > getMaxInactives())
|| (getExpires() != -1 &&
System.currentTimeMillis() > getExpires());
} }
/** /**
@ -115,6 +132,8 @@ public class AuthToken implements Principal {
sb.append(USER_NAME).append("=").append(getUserName()).append(ATTR_SEPARATOR); sb.append(USER_NAME).append("=").append(getUserName()).append(ATTR_SEPARATOR);
sb.append(PRINCIPAL).append("=").append(getName()).append(ATTR_SEPARATOR); sb.append(PRINCIPAL).append("=").append(getName()).append(ATTR_SEPARATOR);
sb.append(TYPE).append("=").append(getType()).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()); sb.append(EXPIRES).append("=").append(getExpires());
tokenStr = sb.toString(); tokenStr = sb.toString();
} }
@ -147,6 +166,15 @@ public class AuthToken implements Principal {
return type; 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. * Returns the expiration time of the token.
* *
@ -183,8 +211,10 @@ public class AuthToken implements Principal {
if (!map.keySet().equals(ATTRIBUTES)) { if (!map.keySet().equals(ATTRIBUTES)) {
throw new AuthenticationException("Invalid token string, missing attributes"); throw new AuthenticationException("Invalid token string, missing attributes");
} }
long maxInactives = Long.parseLong(map.get(MAX_INACTIVES));
long expires = Long.parseLong(map.get(EXPIRES)); long expires = Long.parseLong(map.get(EXPIRES));
AuthToken token = new AuthToken(map.get(USER_NAME), map.get(PRINCIPAL), map.get(TYPE)); AuthToken token = new AuthToken(map.get(USER_NAME), map.get(PRINCIPAL), map.get(TYPE));
token.setMaxInactives(maxInactives);
token.setExpires(expires); token.setExpires(expires);
return token; return token;
} }

View File

@ -18,11 +18,10 @@ import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.net.HttpCookie; import java.net.HttpCookie;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Vector; import java.util.Vector;
@ -53,6 +52,7 @@ import static org.junit.Assert.assertThat;
public class TestAuthenticationFilter { public class TestAuthenticationFilter {
private static final long TOKEN_VALIDITY_SEC = 1000; private static final long TOKEN_VALIDITY_SEC = 1000;
private static final long TOKEN_MAX_INACTIVE_INTERVAL = 1000;
@Test @Test
public void testGetConfiguration() throws Exception { public void testGetConfiguration() throws Exception {
@ -595,7 +595,7 @@ public class TestAuthenticationFilter {
HttpServletResponse response = Mockito.mock(HttpServletResponse.class); HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
FilterChain chain = Mockito.mock(FilterChain.class); FilterChain chain = Mockito.mock(FilterChain.class);
final HashMap<String, String> cookieMap = new HashMap<String, String>(); final Map<String, String> cookieMap = new HashMap<String, String>();
Mockito.doAnswer(new Answer<Object>() { Mockito.doAnswer(new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
@ -644,7 +644,7 @@ public class TestAuthenticationFilter {
} }
} }
private static void parseCookieMap(String cookieHeader, HashMap<String, private static void parseCookieMap(String cookieHeader, Map<String,
String> cookieMap) { String> cookieMap) {
List<HttpCookie> cookies = HttpCookie.parse(cookieHeader); List<HttpCookie> cookies = HttpCookie.parse(cookieHeader);
for (HttpCookie cookie : cookies) { for (HttpCookie cookie : cookies) {
@ -761,7 +761,7 @@ public class TestAuthenticationFilter {
FilterChain chain = Mockito.mock(FilterChain.class); FilterChain chain = Mockito.mock(FilterChain.class);
final HashMap<String, String> cookieMap = new HashMap<String, String>(); final Map<String, String> cookieMap = new HashMap<String, String>();
Mockito.doAnswer( Mockito.doAnswer(
new Answer<Object>() { new Answer<Object>() {
@Override @Override
@ -844,13 +844,164 @@ public class TestAuthenticationFilter {
} }
} }
@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<String>(
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<String, String> cookieMap = new HashMap<>();
Mockito.doAnswer(new Answer<Object>() {
@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, private static void verifyUnauthorized(AuthenticationFilter filter,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
FilterChain chain) throws FilterChain chain) throws
IOException, IOException,
ServletException { ServletException {
final HashMap<String, String> cookieMap = new HashMap<String, String>(); final Map<String, String> cookieMap = new HashMap<String, String>();
Mockito.doAnswer(new Answer<Object>() { Mockito.doAnswer(new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {

View File

@ -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.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.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.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. 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.