HADOOP-14687. AuthenticatedURL will reuse bad/expired session cookies. Contributed by Daryn Sharp
This commit is contained in:
parent
657dd59cc8
commit
c379310212
@ -19,8 +19,14 @@
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.HttpCookie;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -69,14 +75,99 @@ public class AuthenticatedURL {
|
||||
*/
|
||||
public static final String AUTH_COOKIE = "hadoop.auth";
|
||||
|
||||
private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
|
||||
// a lightweight cookie handler that will be attached to url connections.
|
||||
// client code is not required to extract or inject auth cookies.
|
||||
private static class AuthCookieHandler extends CookieHandler {
|
||||
private HttpCookie authCookie;
|
||||
private Map<String, List<String>> cookieHeaders = Collections.emptyMap();
|
||||
|
||||
@Override
|
||||
public synchronized Map<String, List<String>> get(URI uri,
|
||||
Map<String, List<String>> requestHeaders) throws IOException {
|
||||
// call getter so it will reset headers if token is expiring.
|
||||
getAuthCookie();
|
||||
return cookieHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(URI uri, Map<String, List<String>> responseHeaders) {
|
||||
List<String> headers = responseHeaders.get("Set-Cookie");
|
||||
if (headers != null) {
|
||||
for (String header : headers) {
|
||||
List<HttpCookie> cookies;
|
||||
try {
|
||||
cookies = HttpCookie.parse(header);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
// don't care. just skip malformed cookie headers.
|
||||
LOG.debug("Cannot parse cookie header: " + header, iae);
|
||||
continue;
|
||||
}
|
||||
for (HttpCookie cookie : cookies) {
|
||||
if (AUTH_COOKIE.equals(cookie.getName())) {
|
||||
setAuthCookie(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return the auth cookie if still valid.
|
||||
private synchronized HttpCookie getAuthCookie() {
|
||||
if (authCookie != null && authCookie.hasExpired()) {
|
||||
setAuthCookie(null);
|
||||
}
|
||||
return authCookie;
|
||||
}
|
||||
|
||||
private synchronized void setAuthCookie(HttpCookie cookie) {
|
||||
final HttpCookie oldCookie = authCookie;
|
||||
// will redefine if new cookie is valid.
|
||||
authCookie = null;
|
||||
cookieHeaders = Collections.emptyMap();
|
||||
boolean valid = cookie != null && !cookie.getValue().isEmpty() &&
|
||||
!cookie.hasExpired();
|
||||
if (valid) {
|
||||
// decrease lifetime to avoid using a cookie soon to expire.
|
||||
// allows authenticators to pre-emptively reauthenticate to
|
||||
// prevent clients unnecessarily receiving a 401.
|
||||
long maxAge = cookie.getMaxAge();
|
||||
if (maxAge != -1) {
|
||||
cookie.setMaxAge(maxAge * 9/10);
|
||||
valid = !cookie.hasExpired();
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
// v0 cookies value aren't quoted by default but tomcat demands
|
||||
// quoting.
|
||||
if (cookie.getVersion() == 0) {
|
||||
String value = cookie.getValue();
|
||||
if (!value.startsWith("\"")) {
|
||||
value = "\"" + value + "\"";
|
||||
cookie.setValue(value);
|
||||
}
|
||||
}
|
||||
authCookie = cookie;
|
||||
cookieHeaders = new HashMap<>();
|
||||
cookieHeaders.put("Cookie", Arrays.asList(cookie.toString()));
|
||||
}
|
||||
LOG.trace("Setting token value to {} ({})", authCookie, oldCookie);
|
||||
}
|
||||
|
||||
private void setAuthCookieValue(String value) {
|
||||
HttpCookie c = null;
|
||||
if (value != null) {
|
||||
c = new HttpCookie(AUTH_COOKIE, value);
|
||||
}
|
||||
setAuthCookie(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Client side authentication token.
|
||||
*/
|
||||
public static class Token {
|
||||
|
||||
private String token;
|
||||
private final AuthCookieHandler cookieHandler = new AuthCookieHandler();
|
||||
|
||||
/**
|
||||
* Creates a token.
|
||||
@ -102,7 +193,7 @@ public Token(String tokenStr) {
|
||||
* @return if a token from the server has been set.
|
||||
*/
|
||||
public boolean isSet() {
|
||||
return token != null;
|
||||
return cookieHandler.getAuthCookie() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,7 +202,36 @@ public boolean isSet() {
|
||||
* @param tokenStr string representation of the tokenStr.
|
||||
*/
|
||||
void set(String tokenStr) {
|
||||
token = tokenStr;
|
||||
cookieHandler.setAuthCookieValue(tokenStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a cookie handler for the http request to manage session
|
||||
* cookies.
|
||||
* @param url
|
||||
* @return HttpUrlConnection
|
||||
* @throws IOException
|
||||
*/
|
||||
HttpURLConnection openConnection(URL url,
|
||||
ConnectionConfigurator connConfigurator) throws IOException {
|
||||
// the cookie handler is unfortunately a global static. it's a
|
||||
// synchronized class method so we can safely swap the handler while
|
||||
// instantiating the connection object to prevent it leaking into
|
||||
// other connections.
|
||||
final HttpURLConnection conn;
|
||||
synchronized(CookieHandler.class) {
|
||||
CookieHandler current = CookieHandler.getDefault();
|
||||
CookieHandler.setDefault(cookieHandler);
|
||||
try {
|
||||
conn = (HttpURLConnection)url.openConnection();
|
||||
} finally {
|
||||
CookieHandler.setDefault(current);
|
||||
}
|
||||
}
|
||||
if (connConfigurator != null) {
|
||||
connConfigurator.configure(conn);
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,7 +241,15 @@ void set(String tokenStr) {
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return token;
|
||||
String value = "";
|
||||
HttpCookie authCookie = cookieHandler.getAuthCookie();
|
||||
if (authCookie != null) {
|
||||
value = authCookie.getValue();
|
||||
if (value.startsWith("\"")) { // tests don't want the quotes.
|
||||
value = value.substring(1, value.length()-1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@ -218,27 +346,25 @@ public HttpURLConnection openConnection(URL url, Token token) throws IOException
|
||||
throw new IllegalArgumentException("token cannot be NULL");
|
||||
}
|
||||
authenticator.authenticate(url, token);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
if (connConfigurator != null) {
|
||||
conn = connConfigurator.configure(conn);
|
||||
}
|
||||
injectToken(conn, token);
|
||||
return conn;
|
||||
|
||||
// allow the token to create the connection with a cookie handler for
|
||||
// managing session cookies.
|
||||
return token.openConnection(url, connConfigurator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that injects an authentication token to send with a connection.
|
||||
* Helper method that injects an authentication token to send with a
|
||||
* connection. Callers should prefer using
|
||||
* {@link Token#openConnection(URL, ConnectionConfigurator)} which
|
||||
* automatically manages authentication tokens.
|
||||
*
|
||||
* @param conn connection to inject the authentication token into.
|
||||
* @param token authentication token to inject.
|
||||
*/
|
||||
public static void injectToken(HttpURLConnection conn, Token token) {
|
||||
String t = token.token;
|
||||
if (t != null) {
|
||||
if (!t.startsWith("\"")) {
|
||||
t = "\"" + t + "\"";
|
||||
}
|
||||
conn.addRequestProperty("Cookie", AUTH_COOKIE_EQ + t);
|
||||
HttpCookie authCookie = token.cookieHandler.getAuthCookie();
|
||||
if (authCookie != null) {
|
||||
conn.addRequestProperty("Cookie", authCookie.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,24 +384,10 @@ public static void extractToken(HttpURLConnection conn, Token token) throws IOEx
|
||||
if (respCode == HttpURLConnection.HTTP_OK
|
||||
|| respCode == HttpURLConnection.HTTP_CREATED
|
||||
|| respCode == HttpURLConnection.HTTP_ACCEPTED) {
|
||||
Map<String, List<String>> headers = conn.getHeaderFields();
|
||||
List<String> cookies = headers.get("Set-Cookie");
|
||||
if (cookies != null) {
|
||||
for (String cookie : cookies) {
|
||||
if (cookie.startsWith(AUTH_COOKIE_EQ)) {
|
||||
String value = cookie.substring(AUTH_COOKIE_EQ.length());
|
||||
int separator = value.indexOf(";");
|
||||
if (separator > -1) {
|
||||
value = value.substring(0, separator);
|
||||
}
|
||||
if (value.length() > 0) {
|
||||
LOG.trace("Setting token value to {} ({}), resp={}", value,
|
||||
token, respCode);
|
||||
token.set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// cookie handler should have already extracted the token. try again
|
||||
// for backwards compatibility if this method is called on a connection
|
||||
// not opened via this instance.
|
||||
token.cookieHandler.put(null, conn.getHeaderFields());
|
||||
} else if (respCode == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
LOG.trace("Setting token value to null ({}), resp={}", token, respCode);
|
||||
token.set(null);
|
||||
|
@ -147,7 +147,6 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String appName) {
|
||||
}
|
||||
|
||||
private URL url;
|
||||
private HttpURLConnection conn;
|
||||
private Base64 base64;
|
||||
private ConnectionConfigurator connConfigurator;
|
||||
|
||||
@ -182,10 +181,7 @@ public void authenticate(URL url, AuthenticatedURL.Token token)
|
||||
if (!token.isSet()) {
|
||||
this.url = url;
|
||||
base64 = new Base64(0);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
if (connConfigurator != null) {
|
||||
conn = connConfigurator.configure(conn);
|
||||
}
|
||||
HttpURLConnection conn = token.openConnection(url, connConfigurator);
|
||||
conn.setRequestMethod(AUTH_HTTP_METHOD);
|
||||
conn.connect();
|
||||
|
||||
@ -200,7 +196,7 @@ public void authenticate(URL url, AuthenticatedURL.Token token)
|
||||
}
|
||||
needFallback = true;
|
||||
}
|
||||
if (!needFallback && isNegotiate()) {
|
||||
if (!needFallback && isNegotiate(conn)) {
|
||||
LOG.debug("Performing our own SPNEGO sequence.");
|
||||
doSpnegoSequence(token);
|
||||
} else {
|
||||
@ -249,7 +245,7 @@ private boolean isTokenKerberos(AuthenticatedURL.Token token)
|
||||
/*
|
||||
* Indicates if the response is starting a SPNEGO negotiation.
|
||||
*/
|
||||
private boolean isNegotiate() throws IOException {
|
||||
private boolean isNegotiate(HttpURLConnection conn) throws IOException {
|
||||
boolean negotiate = false;
|
||||
if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
String authHeader = conn.getHeaderField(WWW_AUTHENTICATE);
|
||||
@ -267,7 +263,8 @@ private boolean isNegotiate() throws IOException {
|
||||
* @throws IOException if an IO error occurred.
|
||||
* @throws AuthenticationException if an authentication error occurred.
|
||||
*/
|
||||
private void doSpnegoSequence(AuthenticatedURL.Token token) throws IOException, AuthenticationException {
|
||||
private void doSpnegoSequence(final AuthenticatedURL.Token token)
|
||||
throws IOException, AuthenticationException {
|
||||
try {
|
||||
AccessControlContext context = AccessController.getContext();
|
||||
Subject subject = Subject.getSubject(context);
|
||||
@ -308,13 +305,15 @@ public Void run() throws Exception {
|
||||
|
||||
// Loop while the context is still not established
|
||||
while (!established) {
|
||||
HttpURLConnection conn =
|
||||
token.openConnection(url, connConfigurator);
|
||||
outToken = gssContext.initSecContext(inToken, 0, inToken.length);
|
||||
if (outToken != null) {
|
||||
sendToken(outToken);
|
||||
sendToken(conn, outToken);
|
||||
}
|
||||
|
||||
if (!gssContext.isEstablished()) {
|
||||
inToken = readToken();
|
||||
inToken = readToken(conn);
|
||||
} else {
|
||||
established = true;
|
||||
}
|
||||
@ -337,18 +336,14 @@ public Void run() throws Exception {
|
||||
} catch (LoginException ex) {
|
||||
throw new AuthenticationException(ex);
|
||||
}
|
||||
AuthenticatedURL.extractToken(conn, token);
|
||||
}
|
||||
|
||||
/*
|
||||
* Sends the Kerberos token to the server.
|
||||
*/
|
||||
private void sendToken(byte[] outToken) throws IOException {
|
||||
private void sendToken(HttpURLConnection conn, byte[] outToken)
|
||||
throws IOException {
|
||||
String token = base64.encodeToString(outToken);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
if (connConfigurator != null) {
|
||||
conn = connConfigurator.configure(conn);
|
||||
}
|
||||
conn.setRequestMethod(AUTH_HTTP_METHOD);
|
||||
conn.setRequestProperty(AUTHORIZATION, NEGOTIATE + " " + token);
|
||||
conn.connect();
|
||||
@ -357,7 +352,8 @@ private void sendToken(byte[] outToken) throws IOException {
|
||||
/*
|
||||
* Retrieves the Kerberos token returned by the server.
|
||||
*/
|
||||
private byte[] readToken() throws IOException, AuthenticationException {
|
||||
private byte[] readToken(HttpURLConnection conn)
|
||||
throws IOException, AuthenticationException {
|
||||
int status = conn.getResponseCode();
|
||||
if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
String authHeader = conn.getHeaderField(WWW_AUTHENTICATE);
|
||||
|
@ -68,10 +68,7 @@ public void authenticate(URL url, AuthenticatedURL.Token token) throws IOExcepti
|
||||
String paramSeparator = (strUrl.contains("?")) ? "&" : "?";
|
||||
strUrl += paramSeparator + USER_NAME_EQ + getUserName();
|
||||
url = new URL(strUrl);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
if (connConfigurator != null) {
|
||||
conn = connConfigurator.configure(conn);
|
||||
}
|
||||
HttpURLConnection conn = token.openConnection(url, connConfigurator);
|
||||
conn.setRequestMethod("OPTIONS");
|
||||
conn.connect();
|
||||
AuthenticatedURL.extractToken(conn, token);
|
||||
|
@ -32,8 +32,6 @@
|
||||
import org.apache.hadoop.security.ProviderUtils;
|
||||
import org.apache.hadoop.security.SecurityUtil;
|
||||
import org.apache.hadoop.security.UserGroupInformation;
|
||||
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
|
||||
import org.apache.hadoop.security.authentication.client.AuthenticationException;
|
||||
import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
|
||||
import org.apache.hadoop.security.ssl.SSLFactory;
|
||||
import org.apache.hadoop.security.token.Token;
|
||||
@ -522,17 +520,6 @@ private <T> T call(HttpURLConnection conn, Object jsonOutput,
|
||||
authRetryCount - 1);
|
||||
}
|
||||
}
|
||||
try {
|
||||
AuthenticatedURL.extractToken(conn, authToken);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Extracted token, authToken={}, its dt={}", authToken,
|
||||
authToken.getDelegationToken());
|
||||
}
|
||||
} catch (AuthenticationException e) {
|
||||
// Ignore the AuthExceptions.. since we are just using the method to
|
||||
// extract and set the authToken.. (Workaround till we actually fix
|
||||
// AuthenticatedURL properly to set authToken post initialization)
|
||||
}
|
||||
HttpExceptionUtils.validateResponse(conn, expectedResponse);
|
||||
if (conn.getContentType() != null
|
||||
&& conn.getContentType().trim().toLowerCase()
|
||||
|
@ -22,9 +22,12 @@
|
||||
import org.apache.hadoop.minikdc.MiniKdc;
|
||||
import org.apache.hadoop.net.NetUtils;
|
||||
import org.apache.hadoop.security.AuthenticationFilterInitializer;
|
||||
import org.apache.hadoop.security.SecurityUtil;
|
||||
import org.apache.hadoop.security.UserGroupInformation;
|
||||
import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
|
||||
import org.apache.hadoop.security.authentication.KerberosTestUtils;
|
||||
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
|
||||
import org.apache.hadoop.security.authentication.client.AuthenticationException;
|
||||
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
|
||||
import org.apache.hadoop.security.authentication.server.AuthenticationToken;
|
||||
import org.apache.hadoop.security.authentication.util.Signer;
|
||||
@ -32,6 +35,7 @@
|
||||
import org.apache.hadoop.security.authentication.util.StringSignerSecretProviderCreator;
|
||||
import org.apache.hadoop.security.authorize.AccessControlList;
|
||||
import org.apache.hadoop.security.authorize.ProxyUsers;
|
||||
import org.ietf.jgss.GSSException;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
@ -45,7 +49,14 @@
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import javax.security.auth.Subject;
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
@ -72,16 +83,25 @@ public class TestHttpServerWithSpengo {
|
||||
private static MiniKdc testMiniKDC;
|
||||
private static File secretFile = new File(testRootDir, SECRET_STR);
|
||||
|
||||
private static UserGroupInformation authUgi;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp() throws Exception {
|
||||
try {
|
||||
testMiniKDC = new MiniKdc(MiniKdc.createConf(), testRootDir);
|
||||
testMiniKDC.start();
|
||||
testMiniKDC.createPrincipal(
|
||||
httpSpnegoKeytabFile, HTTP_USER + "/localhost");
|
||||
httpSpnegoKeytabFile, HTTP_USER + "/localhost", "keytab-user");
|
||||
} catch (Exception e) {
|
||||
assertTrue("Couldn't setup MiniKDC", false);
|
||||
}
|
||||
|
||||
System.setProperty("sun.security.krb5.debug", "true");
|
||||
Configuration conf = new Configuration();
|
||||
SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf);
|
||||
UserGroupInformation.setConfiguration(conf);
|
||||
authUgi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
|
||||
"keytab-user", httpSpnegoKeytabFile.toString());
|
||||
Writer w = new FileWriter(secretFile);
|
||||
w.write("secret");
|
||||
w.close();
|
||||
@ -209,6 +229,226 @@ public void testAuthenticationWithProxyUser() throws Exception {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionCookie() throws Exception {
|
||||
Configuration conf = new Configuration();
|
||||
conf.set(HttpServer2.FILTER_INITIALIZER_PROPERTY,
|
||||
AuthenticationFilterInitializer.class.getName());
|
||||
conf.set(PREFIX + "type", "kerberos");
|
||||
conf.setBoolean(PREFIX + "simple.anonymous.allowed", false);
|
||||
conf.set(PREFIX + "signer.secret.provider",
|
||||
TestSignerSecretProvider.class.getName());
|
||||
|
||||
conf.set(PREFIX + "kerberos.keytab",
|
||||
httpSpnegoKeytabFile.getAbsolutePath());
|
||||
conf.set(PREFIX + "kerberos.principal", httpSpnegoPrincipal);
|
||||
conf.set(PREFIX + "cookie.domain", realm);
|
||||
conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION,
|
||||
true);
|
||||
|
||||
//setup logs dir
|
||||
System.setProperty("hadoop.log.dir", testRootDir.getAbsolutePath());
|
||||
|
||||
HttpServer2 httpServer = null;
|
||||
// Create http server to test.
|
||||
httpServer = getCommonBuilder()
|
||||
.setConf(conf)
|
||||
.build();
|
||||
httpServer.start();
|
||||
|
||||
// Get signer to encrypt token
|
||||
final Signer signer = new Signer(new TestSignerSecretProvider());
|
||||
final AuthenticatedURL authUrl = new AuthenticatedURL();
|
||||
|
||||
final URL url = new URL("http://" + NetUtils.getHostPortString(
|
||||
httpServer.getConnectorAddress(0)) + "/conf");
|
||||
|
||||
// this illustrates an inconsistency with AuthenticatedURL. the
|
||||
// authenticator is only called when the token is not set. if the
|
||||
// authenticator fails then it must throw an AuthenticationException to
|
||||
// the caller, yet the caller may see 401 for subsequent requests
|
||||
// that require re-authentication like token expiration.
|
||||
final UserGroupInformation simpleUgi =
|
||||
UserGroupInformation.createRemoteUser("simple-user");
|
||||
|
||||
authUgi.doAs(new PrivilegedExceptionAction<Void>() {
|
||||
@Override
|
||||
public Void run() throws Exception {
|
||||
TestSignerSecretProvider.rollSecret();
|
||||
HttpURLConnection conn = null;
|
||||
AuthenticatedURL.Token token = new AuthenticatedURL.Token();
|
||||
|
||||
// initial request should trigger authentication and set the token.
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
Assert.assertTrue(token.isSet());
|
||||
String cookie = token.toString();
|
||||
|
||||
// token should not change.
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
Assert.assertTrue(token.isSet());
|
||||
Assert.assertEquals(cookie, token.toString());
|
||||
|
||||
// roll secret to invalidate token.
|
||||
TestSignerSecretProvider.rollSecret();
|
||||
conn = authUrl.openConnection(url, token);
|
||||
// this may or may not happen. under normal circumstances the
|
||||
// jdk will silently renegotiate and the client never sees a 401.
|
||||
// however in some cases the jdk will give up doing spnego. since
|
||||
// the token is already set, the authenticator isn't invoked (which
|
||||
// would do the spnego if the jdk doesn't), which causes the client
|
||||
// to see a 401.
|
||||
if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
// if this happens, the token should be cleared which means the
|
||||
// next request should succeed and receive a new token.
|
||||
Assert.assertFalse(token.isSet());
|
||||
conn = authUrl.openConnection(url, token);
|
||||
}
|
||||
|
||||
// token should change.
|
||||
Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
Assert.assertTrue(token.isSet());
|
||||
Assert.assertNotEquals(cookie, token.toString());
|
||||
cookie = token.toString();
|
||||
|
||||
// token should not change.
|
||||
for (int i=0; i < 3; i++) {
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals("attempt"+i,
|
||||
HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
Assert.assertTrue(token.isSet());
|
||||
Assert.assertEquals(cookie, token.toString());
|
||||
}
|
||||
|
||||
// blow out the kerberos creds test only auth token is used.
|
||||
Subject s = Subject.getSubject(AccessController.getContext());
|
||||
Set<Object> oldCreds = new HashSet<>(s.getPrivateCredentials());
|
||||
s.getPrivateCredentials().clear();
|
||||
|
||||
// token should not change.
|
||||
for (int i=0; i < 3; i++) {
|
||||
try {
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals("attempt"+i,
|
||||
HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
} catch (AuthenticationException ae) {
|
||||
Assert.fail("attempt"+i+" "+ae);
|
||||
}
|
||||
Assert.assertTrue(token.isSet());
|
||||
Assert.assertEquals(cookie, token.toString());
|
||||
}
|
||||
|
||||
// invalidate token. connections should fail now and token should be
|
||||
// unset.
|
||||
TestSignerSecretProvider.rollSecret();
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals(
|
||||
HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
|
||||
Assert.assertFalse(token.isSet());
|
||||
Assert.assertEquals("", token.toString());
|
||||
|
||||
// restore the kerberos creds, should work again.
|
||||
s.getPrivateCredentials().addAll(oldCreds);
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals(
|
||||
HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
Assert.assertTrue(token.isSet());
|
||||
cookie = token.toString();
|
||||
|
||||
// token should not change.
|
||||
for (int i=0; i < 3; i++) {
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals("attempt"+i,
|
||||
HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
Assert.assertTrue(token.isSet());
|
||||
Assert.assertEquals(cookie, token.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
simpleUgi.doAs(new PrivilegedExceptionAction<Void>() {
|
||||
@Override
|
||||
public Void run() throws Exception {
|
||||
TestSignerSecretProvider.rollSecret();
|
||||
AuthenticatedURL authUrl = new AuthenticatedURL();
|
||||
AuthenticatedURL.Token token = new AuthenticatedURL.Token();
|
||||
HttpURLConnection conn = null;
|
||||
|
||||
// initial connect with unset token will trigger authenticator which
|
||||
// should fail since we have no creds and leave token unset.
|
||||
try {
|
||||
authUrl.openConnection(url, token);
|
||||
Assert.fail("should fail with no credentials");
|
||||
} catch (AuthenticationException ae) {
|
||||
Assert.assertNotNull(ae.getCause());
|
||||
Assert.assertEquals(GSSException.class, ae.getCause().getClass());
|
||||
GSSException gsse = (GSSException)ae.getCause();
|
||||
Assert.assertEquals(GSSException.NO_CRED, gsse.getMajor());
|
||||
} catch (Throwable t) {
|
||||
Assert.fail("Unexpected exception" + t);
|
||||
}
|
||||
Assert.assertFalse(token.isSet());
|
||||
|
||||
// create a valid token and save its value.
|
||||
token = getEncryptedAuthToken(signer, "valid");
|
||||
String cookie = token.toString();
|
||||
|
||||
// server should accept token. after the request the token should
|
||||
// be set to the same value (ie. server didn't reissue cookie)
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
Assert.assertTrue(token.isSet());
|
||||
Assert.assertEquals(cookie, token.toString());
|
||||
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
|
||||
Assert.assertTrue(token.isSet());
|
||||
Assert.assertEquals(cookie, token.toString());
|
||||
|
||||
// change the secret to effectively invalidate the cookie. see above
|
||||
// regarding inconsistency. the authenticator has no way to know the
|
||||
// token is bad, so the client will encounter a 401 instead of
|
||||
// AuthenticationException.
|
||||
TestSignerSecretProvider.rollSecret();
|
||||
conn = authUrl.openConnection(url, token);
|
||||
Assert.assertEquals(
|
||||
HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
|
||||
Assert.assertFalse(token.isSet());
|
||||
Assert.assertEquals("", token.toString());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class TestSignerSecretProvider extends SignerSecretProvider {
|
||||
static int n = 0;
|
||||
static byte[] secret;
|
||||
|
||||
static void rollSecret() {
|
||||
secret = ("secret[" + (n++) + "]").getBytes();
|
||||
}
|
||||
|
||||
public TestSignerSecretProvider() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Properties config, ServletContext servletContext,
|
||||
long tokenValidity) throws Exception {
|
||||
rollSecret();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCurrentSecret() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[][] getAllSecrets() {
|
||||
return new byte[][]{secret};
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticatedURL.Token getEncryptedAuthToken(Signer signer,
|
||||
String user) throws Exception {
|
||||
|
Loading…
Reference in New Issue
Block a user