HADOOP-10379. Protect authentication cookies with the HttpOnly and Secure flags. Contributed by Haohui Mai.
git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@1574283 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
e90687f90e
commit
95ebf9ecc4
|
@ -13,6 +13,8 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.hadoop.security.authentication.server;
|
package org.apache.hadoop.security.authentication.server;
|
||||||
|
|
||||||
|
import org.apache.hadoop.classification.InterfaceAudience;
|
||||||
|
import org.apache.hadoop.classification.InterfaceStability;
|
||||||
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
|
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
|
||||||
import org.apache.hadoop.security.authentication.client.AuthenticationException;
|
import org.apache.hadoop.security.authentication.client.AuthenticationException;
|
||||||
import org.apache.hadoop.security.authentication.util.Signer;
|
import org.apache.hadoop.security.authentication.util.Signer;
|
||||||
|
@ -32,9 +34,8 @@ import javax.servlet.http.HttpServletRequestWrapper;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Enumeration;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Properties;
|
import java.util.*;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link AuthenticationFilter} enables protecting web application resources with different (pluggable)
|
* The {@link AuthenticationFilter} enables protecting web application resources with different (pluggable)
|
||||||
|
@ -69,6 +70,9 @@ import java.util.Random;
|
||||||
* the prefix from it and it will pass them to the the authentication handler for initialization. Properties that do
|
* 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.
|
* not start with the prefix will not be passed to the authentication handler initialization.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@InterfaceAudience.Private
|
||||||
|
@InterfaceStability.Unstable
|
||||||
public class AuthenticationFilter implements Filter {
|
public class AuthenticationFilter implements Filter {
|
||||||
|
|
||||||
private static Logger LOG = LoggerFactory.getLogger(AuthenticationFilter.class);
|
private static Logger LOG = LoggerFactory.getLogger(AuthenticationFilter.class);
|
||||||
|
@ -331,6 +335,7 @@ public class AuthenticationFilter implements Filter {
|
||||||
String unauthorizedMsg = "";
|
String unauthorizedMsg = "";
|
||||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||||
|
boolean isHttps = "https".equals(httpRequest.getScheme());
|
||||||
try {
|
try {
|
||||||
boolean newToken = false;
|
boolean newToken = false;
|
||||||
AuthenticationToken token;
|
AuthenticationToken token;
|
||||||
|
@ -378,8 +383,8 @@ public class AuthenticationFilter implements Filter {
|
||||||
};
|
};
|
||||||
if (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) {
|
if (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) {
|
||||||
String signedToken = signer.sign(token.toString());
|
String signedToken = signer.sign(token.toString());
|
||||||
Cookie cookie = createCookie(signedToken);
|
createAuthCookie(httpResponse, signedToken, getCookieDomain(),
|
||||||
httpResponse.addCookie(cookie);
|
getCookiePath(), token.getExpires(), isHttps);
|
||||||
}
|
}
|
||||||
filterChain.doFilter(httpRequest, httpResponse);
|
filterChain.doFilter(httpRequest, httpResponse);
|
||||||
}
|
}
|
||||||
|
@ -392,31 +397,52 @@ public class AuthenticationFilter implements Filter {
|
||||||
}
|
}
|
||||||
if (unauthorizedResponse) {
|
if (unauthorizedResponse) {
|
||||||
if (!httpResponse.isCommitted()) {
|
if (!httpResponse.isCommitted()) {
|
||||||
Cookie cookie = createCookie("");
|
createAuthCookie(httpResponse, "", getCookieDomain(),
|
||||||
cookie.setMaxAge(0);
|
getCookiePath(), 0, isHttps);
|
||||||
httpResponse.addCookie(cookie);
|
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
|
||||||
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, unauthorizedMsg);
|
unauthorizedMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the Hadoop authentiation HTTP cookie.
|
* Creates the Hadoop authentication HTTP cookie.
|
||||||
* <p/>
|
|
||||||
* It sets the domain and path specified in the configuration.
|
|
||||||
*
|
*
|
||||||
* @param token authentication token for the cookie.
|
* @param token authentication token for the cookie.
|
||||||
|
* @param expires UNIX timestamp that indicates the expire date of the
|
||||||
|
* cookie. It has no effect if its value < 0.
|
||||||
*
|
*
|
||||||
* @return the HTTP cookie.
|
* XXX the following code duplicate some logic in Jetty / Servlet API,
|
||||||
|
* because of the fact that Hadoop is stuck at servlet 3.0 and jetty 6
|
||||||
|
* right now.
|
||||||
*/
|
*/
|
||||||
protected Cookie createCookie(String token) {
|
public static void createAuthCookie(HttpServletResponse resp, String token,
|
||||||
Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, token);
|
String domain, String path, long expires,
|
||||||
if (getCookieDomain() != null) {
|
boolean isSecure) {
|
||||||
cookie.setDomain(getCookieDomain());
|
StringBuilder sb = new StringBuilder(AuthenticatedURL.AUTH_COOKIE).append
|
||||||
|
("=").append(token);
|
||||||
|
|
||||||
|
if (path != null) {
|
||||||
|
sb.append("; Path=").append(path);
|
||||||
}
|
}
|
||||||
if (getCookiePath() != null) {
|
|
||||||
cookie.setPath(getCookiePath());
|
if (domain != null) {
|
||||||
|
sb.append("; Domain=").append(domain);
|
||||||
}
|
}
|
||||||
return cookie;
|
|
||||||
|
if (expires >= 0) {
|
||||||
|
Date date = new Date(expires);
|
||||||
|
SimpleDateFormat df = new SimpleDateFormat("EEE, " +
|
||||||
|
"dd-MMM-yyyy HH:mm:ss zzz");
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import org.apache.hadoop.security.authentication.client.AuthenticationException;
|
||||||
import org.apache.hadoop.security.authentication.util.Signer;
|
import org.apache.hadoop.security.authentication.util.Signer;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
import org.mockito.stubbing.Answer;
|
import org.mockito.stubbing.Answer;
|
||||||
|
@ -31,9 +32,7 @@ import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
public class TestAuthenticationFilter {
|
public class TestAuthenticationFilter {
|
||||||
|
|
||||||
|
@ -400,103 +399,87 @@ public class TestAuthenticationFilter {
|
||||||
boolean invalidToken,
|
boolean invalidToken,
|
||||||
boolean expired) throws Exception {
|
boolean expired) throws Exception {
|
||||||
AuthenticationFilter filter = new AuthenticationFilter();
|
AuthenticationFilter filter = new AuthenticationFilter();
|
||||||
|
FilterConfig config = Mockito.mock(FilterConfig.class);
|
||||||
|
Mockito.when(config.getInitParameter("management.operation.return")).
|
||||||
|
thenReturn("true");
|
||||||
|
Mockito.when(config.getInitParameter("expired.token")).
|
||||||
|
thenReturn(Boolean.toString(expired));
|
||||||
|
Mockito.when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE))
|
||||||
|
.thenReturn(DummyAuthenticationHandler.class.getName());
|
||||||
|
Mockito.when(config.getInitParameter(AuthenticationFilter
|
||||||
|
.AUTH_TOKEN_VALIDITY)).thenReturn("1000");
|
||||||
|
Mockito.when(config.getInitParameter(AuthenticationFilter
|
||||||
|
.SIGNATURE_SECRET)).thenReturn("secret");
|
||||||
|
Mockito.when(config.getInitParameterNames()).thenReturn(new
|
||||||
|
Vector<String>(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
|
||||||
|
AuthenticationFilter.AUTH_TOKEN_VALIDITY,
|
||||||
|
AuthenticationFilter.SIGNATURE_SECRET, "management.operation" +
|
||||||
|
".return", "expired.token")).elements());
|
||||||
|
|
||||||
|
if (withDomainPath) {
|
||||||
|
Mockito.when(config.getInitParameter(AuthenticationFilter
|
||||||
|
.COOKIE_DOMAIN)).thenReturn(".foo.com");
|
||||||
|
Mockito.when(config.getInitParameter(AuthenticationFilter.COOKIE_PATH))
|
||||||
|
.thenReturn("/bar");
|
||||||
|
Mockito.when(config.getInitParameterNames()).thenReturn(new
|
||||||
|
Vector<String>(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
|
||||||
|
AuthenticationFilter.AUTH_TOKEN_VALIDITY,
|
||||||
|
AuthenticationFilter.SIGNATURE_SECRET,
|
||||||
|
AuthenticationFilter.COOKIE_DOMAIN, AuthenticationFilter
|
||||||
|
.COOKIE_PATH, "management.operation.return")).elements());
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
||||||
|
Mockito.when(request.getParameter("authenticated")).thenReturn("true");
|
||||||
|
Mockito.when(request.getRequestURL()).thenReturn(new StringBuffer
|
||||||
|
("http://foo:8080/bar"));
|
||||||
|
Mockito.when(request.getQueryString()).thenReturn("authenticated=true");
|
||||||
|
|
||||||
|
if (invalidToken) {
|
||||||
|
Mockito.when(request.getCookies()).thenReturn(new Cookie[]{new Cookie
|
||||||
|
(AuthenticatedURL.AUTH_COOKIE, "foo")});
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||||
|
FilterChain chain = Mockito.mock(FilterChain.class);
|
||||||
|
|
||||||
|
final HashMap<String, String> cookieMap = new HashMap<String, String>();
|
||||||
|
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());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FilterConfig config = Mockito.mock(FilterConfig.class);
|
|
||||||
Mockito.when(config.getInitParameter("management.operation.return")).
|
|
||||||
thenReturn("true");
|
|
||||||
Mockito.when(config.getInitParameter("expired.token")).
|
|
||||||
thenReturn(Boolean.toString(expired));
|
|
||||||
Mockito.when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn(
|
|
||||||
DummyAuthenticationHandler.class.getName());
|
|
||||||
Mockito.when(config.getInitParameter(AuthenticationFilter.AUTH_TOKEN_VALIDITY)).thenReturn("1000");
|
|
||||||
Mockito.when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn("secret");
|
|
||||||
Mockito.when(config.getInitParameterNames()).thenReturn(
|
|
||||||
new Vector<String>(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
|
|
||||||
AuthenticationFilter.AUTH_TOKEN_VALIDITY,
|
|
||||||
AuthenticationFilter.SIGNATURE_SECRET,
|
|
||||||
"management.operation.return",
|
|
||||||
"expired.token")).elements());
|
|
||||||
|
|
||||||
if (withDomainPath) {
|
|
||||||
Mockito.when(config.getInitParameter(AuthenticationFilter.COOKIE_DOMAIN)).thenReturn(".foo.com");
|
|
||||||
Mockito.when(config.getInitParameter(AuthenticationFilter.COOKIE_PATH)).thenReturn("/bar");
|
|
||||||
Mockito.when(config.getInitParameterNames()).thenReturn(
|
|
||||||
new Vector<String>(Arrays.asList(AuthenticationFilter.AUTH_TYPE,
|
|
||||||
AuthenticationFilter.AUTH_TOKEN_VALIDITY,
|
|
||||||
AuthenticationFilter.SIGNATURE_SECRET,
|
|
||||||
AuthenticationFilter.COOKIE_DOMAIN,
|
|
||||||
AuthenticationFilter.COOKIE_PATH,
|
|
||||||
"management.operation.return")).elements());
|
|
||||||
}
|
|
||||||
|
|
||||||
filter.init(config);
|
filter.init(config);
|
||||||
|
|
||||||
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
|
||||||
Mockito.when(request.getParameter("authenticated")).thenReturn("true");
|
|
||||||
Mockito.when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar"));
|
|
||||||
Mockito.when(request.getQueryString()).thenReturn("authenticated=true");
|
|
||||||
|
|
||||||
if (invalidToken) {
|
|
||||||
Mockito.when(request.getCookies()).thenReturn(
|
|
||||||
new Cookie[] { new Cookie(AuthenticatedURL.AUTH_COOKIE, "foo")}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
|
||||||
|
|
||||||
FilterChain chain = Mockito.mock(FilterChain.class);
|
|
||||||
|
|
||||||
final boolean[] calledDoFilter = new boolean[1];
|
|
||||||
|
|
||||||
Mockito.doAnswer(
|
|
||||||
new Answer<Object>() {
|
|
||||||
@Override
|
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
calledDoFilter[0] = true;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).when(chain).doFilter(Mockito.<ServletRequest>anyObject(), Mockito.<ServletResponse>anyObject());
|
|
||||||
|
|
||||||
final Cookie[] setCookie = new Cookie[1];
|
|
||||||
Mockito.doAnswer(
|
|
||||||
new Answer<Object>() {
|
|
||||||
@Override
|
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
Object[] args = invocation.getArguments();
|
|
||||||
setCookie[0] = (Cookie) args[0];
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).when(response).addCookie(Mockito.<Cookie>anyObject());
|
|
||||||
|
|
||||||
filter.doFilter(request, response, chain);
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
if (expired) {
|
if (expired) {
|
||||||
Mockito.verify(response, Mockito.never()).
|
Mockito.verify(response, Mockito.never()).
|
||||||
addCookie(Mockito.any(Cookie.class));
|
addCookie(Mockito.any(Cookie.class));
|
||||||
} else {
|
} else {
|
||||||
Assert.assertNotNull(setCookie[0]);
|
String v = cookieMap.get(AuthenticatedURL.AUTH_COOKIE);
|
||||||
Assert.assertEquals(AuthenticatedURL.AUTH_COOKIE, setCookie[0].getName());
|
Assert.assertNotNull(v);
|
||||||
Assert.assertTrue(setCookie[0].getValue().contains("u="));
|
Assert.assertTrue(v.contains("u=") && v.contains("p=") && v.contains
|
||||||
Assert.assertTrue(setCookie[0].getValue().contains("p="));
|
("t=") && v.contains("e=") && v.contains("s="));
|
||||||
Assert.assertTrue(setCookie[0].getValue().contains("t="));
|
Mockito.verify(chain).doFilter(Mockito.any(ServletRequest.class),
|
||||||
Assert.assertTrue(setCookie[0].getValue().contains("e="));
|
Mockito.any(ServletResponse.class));
|
||||||
Assert.assertTrue(setCookie[0].getValue().contains("s="));
|
|
||||||
Assert.assertTrue(calledDoFilter[0]);
|
|
||||||
|
|
||||||
Signer signer = new Signer("secret".getBytes());
|
Signer signer = new Signer("secret".getBytes());
|
||||||
String value = signer.verifyAndExtract(setCookie[0].getValue());
|
String value = signer.verifyAndExtract(v);
|
||||||
AuthenticationToken token = AuthenticationToken.parse(value);
|
AuthenticationToken token = AuthenticationToken.parse(value);
|
||||||
Assert.assertEquals(System.currentTimeMillis() + 1000 * 1000,
|
Assert.assertEquals(System.currentTimeMillis() + 1000 * 1000,
|
||||||
token.getExpires(), 100);
|
token.getExpires(), 100);
|
||||||
|
|
||||||
if (withDomainPath) {
|
if (withDomainPath) {
|
||||||
Assert.assertEquals(".foo.com", setCookie[0].getDomain());
|
Assert.assertEquals(".foo.com", cookieMap.get("Domain"));
|
||||||
Assert.assertEquals("/bar", setCookie[0].getPath());
|
Assert.assertEquals("/bar", cookieMap.get("Path"));
|
||||||
} else {
|
} else {
|
||||||
Assert.assertNull(setCookie[0].getDomain());
|
Assert.assertFalse(cookieMap.containsKey("Domain"));
|
||||||
Assert.assertNull(setCookie[0].getPath());
|
Assert.assertFalse(cookieMap.containsKey("Path"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -504,6 +487,26 @@ public class TestAuthenticationFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void parseCookieMap(String cookieHeader, HashMap<String,
|
||||||
|
String> cookieMap) {
|
||||||
|
for (String pair : cookieHeader.split(";")) {
|
||||||
|
String p = pair.trim();
|
||||||
|
int idx = p.indexOf('=');
|
||||||
|
final String k, v;
|
||||||
|
if (idx == -1) {
|
||||||
|
k = p;
|
||||||
|
v = null;
|
||||||
|
} else if (idx == p.length()) {
|
||||||
|
k = p.substring(0, idx - 1);
|
||||||
|
v = null;
|
||||||
|
} else {
|
||||||
|
k = p.substring(0, idx);
|
||||||
|
v = p.substring(idx + 1);
|
||||||
|
}
|
||||||
|
cookieMap.put(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDoFilterAuthentication() throws Exception {
|
public void testDoFilterAuthentication() throws Exception {
|
||||||
_testDoFilterAuthentication(false, false, false);
|
_testDoFilterAuthentication(false, false, false);
|
||||||
|
@ -601,43 +604,41 @@ public class TestAuthenticationFilter {
|
||||||
Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie});
|
Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie});
|
||||||
|
|
||||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||||
|
|
||||||
FilterChain chain = Mockito.mock(FilterChain.class);
|
FilterChain chain = Mockito.mock(FilterChain.class);
|
||||||
|
|
||||||
Mockito.doAnswer(
|
verifyUnauthorized(filter, request, response, chain);
|
||||||
new Answer<Object>() {
|
|
||||||
@Override
|
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
Assert.fail();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).when(chain).doFilter(Mockito.<ServletRequest>anyObject(), Mockito.<ServletResponse>anyObject());
|
|
||||||
|
|
||||||
final Cookie[] setCookie = new Cookie[1];
|
|
||||||
Mockito.doAnswer(
|
|
||||||
new Answer<Object>() {
|
|
||||||
@Override
|
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
Object[] args = invocation.getArguments();
|
|
||||||
setCookie[0] = (Cookie) args[0];
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).when(response).addCookie(Mockito.<Cookie>anyObject());
|
|
||||||
|
|
||||||
filter.doFilter(request, response, chain);
|
|
||||||
|
|
||||||
Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse.SC_UNAUTHORIZED), Mockito.anyString());
|
|
||||||
|
|
||||||
Assert.assertNotNull(setCookie[0]);
|
|
||||||
Assert.assertEquals(AuthenticatedURL.AUTH_COOKIE, setCookie[0].getName());
|
|
||||||
Assert.assertEquals("", setCookie[0].getValue());
|
|
||||||
} finally {
|
} finally {
|
||||||
filter.destroy();
|
filter.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void verifyUnauthorized(AuthenticationFilter filter,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain chain) throws
|
||||||
|
IOException,
|
||||||
|
ServletException {
|
||||||
|
final HashMap<String, String> cookieMap = new HashMap<String, String>();
|
||||||
|
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);
|
||||||
|
|
||||||
|
Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse
|
||||||
|
.SC_UNAUTHORIZED), Mockito.anyString());
|
||||||
|
Mockito.verify(chain, Mockito.never()).doFilter(Mockito.any
|
||||||
|
(ServletRequest.class), Mockito.any(ServletResponse.class));
|
||||||
|
|
||||||
|
Assert.assertTrue(cookieMap.containsKey(AuthenticatedURL.AUTH_COOKIE));
|
||||||
|
Assert.assertEquals("", cookieMap.get(AuthenticatedURL.AUTH_COOKIE));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDoFilterAuthenticatedInvalidType() throws Exception {
|
public void testDoFilterAuthenticatedInvalidType() throws Exception {
|
||||||
AuthenticationFilter filter = new AuthenticationFilter();
|
AuthenticationFilter filter = new AuthenticationFilter();
|
||||||
|
@ -665,38 +666,9 @@ public class TestAuthenticationFilter {
|
||||||
Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie});
|
Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie});
|
||||||
|
|
||||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||||
|
|
||||||
FilterChain chain = Mockito.mock(FilterChain.class);
|
FilterChain chain = Mockito.mock(FilterChain.class);
|
||||||
|
|
||||||
Mockito.doAnswer(
|
verifyUnauthorized(filter, request, response, chain);
|
||||||
new Answer<Object>() {
|
|
||||||
@Override
|
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
Assert.fail();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).when(chain).doFilter(Mockito.<ServletRequest>anyObject(), Mockito.<ServletResponse>anyObject());
|
|
||||||
|
|
||||||
final Cookie[] setCookie = new Cookie[1];
|
|
||||||
Mockito.doAnswer(
|
|
||||||
new Answer<Object>() {
|
|
||||||
@Override
|
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
Object[] args = invocation.getArguments();
|
|
||||||
setCookie[0] = (Cookie) args[0];
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).when(response).addCookie(Mockito.<Cookie>anyObject());
|
|
||||||
|
|
||||||
filter.doFilter(request, response, chain);
|
|
||||||
|
|
||||||
Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse.SC_UNAUTHORIZED), Mockito.anyString());
|
|
||||||
|
|
||||||
Assert.assertNotNull(setCookie[0]);
|
|
||||||
Assert.assertEquals(AuthenticatedURL.AUTH_COOKIE, setCookie[0].getName());
|
|
||||||
Assert.assertEquals("", setCookie[0].getValue());
|
|
||||||
} finally {
|
} finally {
|
||||||
filter.destroy();
|
filter.destroy();
|
||||||
}
|
}
|
||||||
|
|
|
@ -361,6 +361,9 @@ Release 2.4.0 - UNRELEASED
|
||||||
HADOOP-8691. FsShell can print "Found xxx items" unnecessarily often.
|
HADOOP-8691. FsShell can print "Found xxx items" unnecessarily often.
|
||||||
(Daryn Sharp via wheat9)
|
(Daryn Sharp via wheat9)
|
||||||
|
|
||||||
|
HADOOP-10379. Protect authentication cookies with the HttpOnly and Secure
|
||||||
|
flags. (wheat9)
|
||||||
|
|
||||||
OPTIMIZATIONS
|
OPTIMIZATIONS
|
||||||
|
|
||||||
BUG FIXES
|
BUG FIXES
|
||||||
|
|
|
@ -67,12 +67,14 @@ import org.mortbay.jetty.Handler;
|
||||||
import org.mortbay.jetty.MimeTypes;
|
import org.mortbay.jetty.MimeTypes;
|
||||||
import org.mortbay.jetty.RequestLog;
|
import org.mortbay.jetty.RequestLog;
|
||||||
import org.mortbay.jetty.Server;
|
import org.mortbay.jetty.Server;
|
||||||
|
import org.mortbay.jetty.SessionManager;
|
||||||
import org.mortbay.jetty.handler.ContextHandler;
|
import org.mortbay.jetty.handler.ContextHandler;
|
||||||
import org.mortbay.jetty.handler.ContextHandlerCollection;
|
import org.mortbay.jetty.handler.ContextHandlerCollection;
|
||||||
import org.mortbay.jetty.handler.HandlerCollection;
|
import org.mortbay.jetty.handler.HandlerCollection;
|
||||||
import org.mortbay.jetty.handler.RequestLogHandler;
|
import org.mortbay.jetty.handler.RequestLogHandler;
|
||||||
import org.mortbay.jetty.nio.SelectChannelConnector;
|
import org.mortbay.jetty.nio.SelectChannelConnector;
|
||||||
import org.mortbay.jetty.security.SslSocketConnector;
|
import org.mortbay.jetty.security.SslSocketConnector;
|
||||||
|
import org.mortbay.jetty.servlet.AbstractSessionManager;
|
||||||
import org.mortbay.jetty.servlet.Context;
|
import org.mortbay.jetty.servlet.Context;
|
||||||
import org.mortbay.jetty.servlet.DefaultServlet;
|
import org.mortbay.jetty.servlet.DefaultServlet;
|
||||||
import org.mortbay.jetty.servlet.FilterHolder;
|
import org.mortbay.jetty.servlet.FilterHolder;
|
||||||
|
@ -356,6 +358,13 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
threadPool.setDaemon(true);
|
threadPool.setDaemon(true);
|
||||||
webServer.setThreadPool(threadPool);
|
webServer.setThreadPool(threadPool);
|
||||||
|
|
||||||
|
SessionManager sm = webAppContext.getSessionHandler().getSessionManager();
|
||||||
|
if (sm instanceof AbstractSessionManager) {
|
||||||
|
AbstractSessionManager asm = (AbstractSessionManager)sm;
|
||||||
|
asm.setHttpOnly(true);
|
||||||
|
asm.setSecureCookies(true);
|
||||||
|
}
|
||||||
|
|
||||||
ContextHandlerCollection contexts = new ContextHandlerCollection();
|
ContextHandlerCollection contexts = new ContextHandlerCollection();
|
||||||
RequestLog requestLog = HttpRequestLog.getRequestLog(name);
|
RequestLog requestLog = HttpRequestLog.getRequestLog(name);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
/**
|
||||||
|
* 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.http;
|
||||||
|
|
||||||
|
import junit.framework.Assert;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.fs.FileUtil;
|
||||||
|
import org.apache.hadoop.net.NetUtils;
|
||||||
|
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
|
||||||
|
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
|
||||||
|
import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
|
||||||
|
import org.apache.hadoop.security.ssl.SSLFactory;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.servlet.*;
|
||||||
|
import javax.servlet.http.Cookie;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
|
public class TestHttpCookieFlag {
|
||||||
|
private static final String BASEDIR = System.getProperty("test.build.dir",
|
||||||
|
"target/test-dir") + "/" + TestHttpCookieFlag.class.getSimpleName();
|
||||||
|
private static String keystoresDir;
|
||||||
|
private static String sslConfDir;
|
||||||
|
private static SSLFactory clientSslFactory;
|
||||||
|
private static HttpServer2 server;
|
||||||
|
|
||||||
|
public static class DummyAuthenticationFilter implements Filter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(FilterConfig filterConfig) throws ServletException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response,
|
||||||
|
FilterChain chain) throws IOException,
|
||||||
|
ServletException {
|
||||||
|
HttpServletResponse resp = (HttpServletResponse) response;
|
||||||
|
boolean isHttps = "https".equals(request.getScheme());
|
||||||
|
AuthenticationFilter.createAuthCookie(resp, "token", null, null, -1,
|
||||||
|
isHttps);
|
||||||
|
chain.doFilter(request, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class DummyFilterInitializer extends FilterInitializer {
|
||||||
|
@Override
|
||||||
|
public void initFilter(FilterContainer container, Configuration conf) {
|
||||||
|
container.addFilter("DummyAuth", DummyAuthenticationFilter.class
|
||||||
|
.getName(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() throws Exception {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
conf.set(HttpServer2.FILTER_INITIALIZER_PROPERTY,
|
||||||
|
DummyFilterInitializer.class.getName());
|
||||||
|
|
||||||
|
File base = new File(BASEDIR);
|
||||||
|
FileUtil.fullyDelete(base);
|
||||||
|
base.mkdirs();
|
||||||
|
keystoresDir = new File(BASEDIR).getAbsolutePath();
|
||||||
|
sslConfDir = KeyStoreTestUtil.getClasspathDir(TestSSLHttpServer.class);
|
||||||
|
|
||||||
|
KeyStoreTestUtil.setupSSLConfig(keystoresDir, sslConfDir, conf, false);
|
||||||
|
Configuration sslConf = new Configuration(false);
|
||||||
|
sslConf.addResource("ssl-server.xml");
|
||||||
|
sslConf.addResource("ssl-client.xml");
|
||||||
|
|
||||||
|
clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, sslConf);
|
||||||
|
clientSslFactory.init();
|
||||||
|
|
||||||
|
server = new HttpServer2.Builder()
|
||||||
|
.setName("test")
|
||||||
|
.addEndpoint(new URI("http://localhost"))
|
||||||
|
.addEndpoint(new URI("https://localhost"))
|
||||||
|
.setConf(conf)
|
||||||
|
.keyPassword(sslConf.get("ssl.server.keystore.keypassword"))
|
||||||
|
.keyStore(sslConf.get("ssl.server.keystore.location"),
|
||||||
|
sslConf.get("ssl.server.keystore.password"),
|
||||||
|
sslConf.get("ssl.server.keystore.type", "jks"))
|
||||||
|
.trustStore(sslConf.get("ssl.server.truststore.location"),
|
||||||
|
sslConf.get("ssl.server.truststore.password"),
|
||||||
|
sslConf.get("ssl.server.truststore.type", "jks")).build();
|
||||||
|
server.addServlet("echo", "/echo", TestHttpServer.EchoServlet.class);
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHttpCookie() throws IOException {
|
||||||
|
URL base = new URL("http://" + NetUtils.getHostPortString(server
|
||||||
|
.getConnectorAddress(0)));
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) new URL(base,
|
||||||
|
"/echo").openConnection();
|
||||||
|
Assert.assertEquals(AuthenticatedURL.AUTH_COOKIE + "=token; " +
|
||||||
|
"HttpOnly", conn.getHeaderField("Set-Cookie"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHttpsCookie() throws IOException, GeneralSecurityException {
|
||||||
|
URL base = new URL("https://" + NetUtils.getHostPortString(server
|
||||||
|
.getConnectorAddress(1)));
|
||||||
|
HttpsURLConnection conn = (HttpsURLConnection) new URL(base,
|
||||||
|
"/echo").openConnection();
|
||||||
|
conn.setSSLSocketFactory(clientSslFactory.createSSLSocketFactory());
|
||||||
|
Assert.assertEquals(AuthenticatedURL.AUTH_COOKIE + "=token; " +
|
||||||
|
"Secure; HttpOnly", conn.getHeaderField("Set-Cookie"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void cleanup() throws Exception {
|
||||||
|
server.stop();
|
||||||
|
FileUtil.fullyDelete(new File(BASEDIR));
|
||||||
|
KeyStoreTestUtil.cleanupSSLConfig(keystoresDir, sslConfDir);
|
||||||
|
clientSslFactory.destroy();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue