OOZIE-2485 Oozie client keeps trying to use expired auth token (rkanter)
Project: http://git-wip-us.apache.org/repos/asf/oozie/repo Commit: http://git-wip-us.apache.org/repos/asf/oozie/commit/b50d642a Tree: http://git-wip-us.apache.org/repos/asf/oozie/tree/b50d642a Diff: http://git-wip-us.apache.org/repos/asf/oozie/diff/b50d642a Branch: refs/heads/master Commit: b50d642a8f44e44682786a79275310b7f66f08e1 Parents: a3d25f2 Author: Robert Kanter <[email protected]> Authored: Thu Apr 7 18:10:54 2016 -0700 Committer: Robert Kanter <[email protected]> Committed: Thu Apr 7 18:10:54 2016 -0700 ---------------------------------------------------------------------- .../apache/oozie/client/AuthOozieClient.java | 76 +++++++++++++++++--- .../servlet/TestAuthFilterAuthOozieClient.java | 61 +++++++++++++++- release-log.txt | 1 + 3 files changed, 127 insertions(+), 11 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/oozie/blob/b50d642a/client/src/main/java/org/apache/oozie/client/AuthOozieClient.java ---------------------------------------------------------------------- diff --git a/client/src/main/java/org/apache/oozie/client/AuthOozieClient.java b/client/src/main/java/org/apache/oozie/client/AuthOozieClient.java index b62887b..a840dac 100644 --- a/client/src/main/java/org/apache/oozie/client/AuthOozieClient.java +++ b/client/src/main/java/org/apache/oozie/client/AuthOozieClient.java @@ -103,46 +103,104 @@ public class AuthOozieClient extends XOozieClient { @Override protected HttpURLConnection createConnection(URL url, String method) throws IOException, OozieClientException { boolean useAuthFile = System.getProperty(USE_AUTH_TOKEN_CACHE_SYS_PROP, "false").equalsIgnoreCase("true"); - AuthenticatedURL.Token readToken = new AuthenticatedURL.Token(); - AuthenticatedURL.Token currentToken = new AuthenticatedURL.Token(); + AuthenticatedURL.Token readToken = null; + AuthenticatedURL.Token currentToken = null; + // Read the token in from the file if (useAuthFile) { readToken = readAuthToken(); - if (readToken != null) { - currentToken = new AuthenticatedURL.Token(readToken.toString()); + } + if (readToken == null) { + currentToken = new AuthenticatedURL.Token(); + } else { + currentToken = new AuthenticatedURL.Token(readToken.toString()); + } + + // To prevent rare race conditions and to save a call to the Server, lets check the token's expiration time locally, and + // consider it expired if its expiration time has passed or will pass in the next 5 minutes (or if there's a problem parsing + // it) + if (currentToken.isSet()) { + long expires = getExpirationTime(currentToken); + if (expires < System.currentTimeMillis() + 300000) { + if (useAuthFile) { + AUTH_TOKEN_CACHE_FILE.delete(); + } + currentToken = new AuthenticatedURL.Token(); } } + // If we have a token, double check with the Server to make sure it hasn't expired yet if (currentToken.isSet()) { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("OPTIONS"); AuthenticatedURL.injectToken(conn, currentToken); - if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { - AUTH_TOKEN_CACHE_FILE.delete(); + if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED + || conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) { + if (useAuthFile) { + AUTH_TOKEN_CACHE_FILE.delete(); + } currentToken = new AuthenticatedURL.Token(); + } else { + // After HADOOP-10301, with Kerberos the above token expiration check will now send 200 even with an expired token + // if you still have valid Kerberos credentials. Previously, it would send 401 so the client knows that it needs to + // use the KerberosAuthenticator to get a new token. Now, it may even provide a token back from this call, so we + // need to check for a new token and update ours. If no new token was given and we got a 20X code, this will do a + // no-op. + // With Pseudo, the above token expiration check will now send 403 instead of the 401; we're now checking for either + // response code above. However, unlike with Kerberos, Pseudo doesn't give us a new token here; we'll have to get + // one later. + try { + AuthenticatedURL.extractToken(conn, currentToken); + } catch (AuthenticationException ex) { + if (useAuthFile) { + AUTH_TOKEN_CACHE_FILE.delete(); + } + currentToken = new AuthenticatedURL.Token(); + } } } + // If we didn't have a token, or it had expired, let's get a new one from the Server using the configured Authenticator if (!currentToken.isSet()) { Authenticator authenticator = getAuthenticator(); try { - new AuthenticatedURL(authenticator).openConnection(url, currentToken); + authenticator.authenticate(url, currentToken); } catch (AuthenticationException ex) { - AUTH_TOKEN_CACHE_FILE.delete(); + if (useAuthFile) { + AUTH_TOKEN_CACHE_FILE.delete(); + } throw new OozieClientException(OozieClientException.AUTHENTICATION, "Could not authenticate, " + ex.getMessage(), ex); } } + + // If we got a new token, save it to the cache file if (useAuthFile && currentToken.isSet() && !currentToken.equals(readToken)) { writeAuthToken(currentToken); } + + // Now create a connection using the token and return it to the caller HttpURLConnection conn = super.createConnection(url, method); AuthenticatedURL.injectToken(conn, currentToken); - return conn; } + private static long getExpirationTime(AuthenticatedURL.Token token) { + long expires = 0L; + String[] splits = token.toString().split("&"); + for (String split : splits) { + if (split.startsWith("e=")) { + try { + expires = Long.parseLong(split.substring(2)); + } catch (Exception e) { + // token is somehow invalid, assume it expired already + break; + } + } + } + return expires; + } /** * Read a authentication token cached in the user home directory. http://git-wip-us.apache.org/repos/asf/oozie/blob/b50d642a/core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java b/core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java index 456b4a5..04fde73 100644 --- a/core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java +++ b/core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java @@ -18,10 +18,12 @@ package org.apache.oozie.servlet; +import org.apache.commons.codec.binary.Base64; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.security.authentication.client.AuthenticatedURL; import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authentication.client.PseudoAuthenticator; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; import org.apache.oozie.cli.OozieCLI; import org.apache.oozie.client.AuthOozieClient; import org.apache.oozie.client.HeaderTestingVersionServlet; @@ -36,8 +38,12 @@ import org.apache.oozie.util.IOUtils; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.io.PrintWriter; import java.net.URL; import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.concurrent.Callable; @@ -45,6 +51,7 @@ import java.util.concurrent.Callable; * */ public class TestAuthFilterAuthOozieClient extends XTestCase { + private static final String SECRET = "secret"; private EmbeddedServletContainer container; protected String getContextURL() { @@ -175,7 +182,7 @@ public class TestAuthFilterAuthOozieClient extends XTestCase { FileWriter fw = null; try { fw = new FileWriter(secretFile); - fw.write("secret"); + fw.write(SECRET); } finally { if (fw != null) { fw.close(); @@ -185,7 +192,7 @@ public class TestAuthFilterAuthOozieClient extends XTestCase { } catch (ClassNotFoundException cnfe) { // ignore } - conf.set("oozie.authentication.signature.secret", "secret"); + conf.set("oozie.authentication.signature.secret", SECRET); conf.set("oozie.authentication.simple.anonymous.allowed", "false"); //not using cache @@ -229,6 +236,56 @@ public class TestAuthFilterAuthOozieClient extends XTestCase { assertTrue(AuthOozieClient.AUTH_TOKEN_CACHE_FILE.exists()); String newCache = IOUtils.getReaderAsString(new FileReader(AuthOozieClient.AUTH_TOKEN_CACHE_FILE), -1); assertEquals(currentCache, newCache); + + //re-using cache with token that will expire within 5 minutes + currentCache = writeTokenCache(System.currentTimeMillis() + 300000); + setSystemProperty("oozie.auth.token.cache", "true"); + runTest(new Callable<Void>() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new OozieCLI().run(args)); + return null; + } + }, conf); + assertTrue(AuthOozieClient.AUTH_TOKEN_CACHE_FILE.exists()); + newCache = IOUtils.getReaderAsString(new FileReader(AuthOozieClient.AUTH_TOKEN_CACHE_FILE), -1); + assertFalse("Almost expired token should have been updated but was not", currentCache.equals(newCache)); + + //re-using cache with expired token + currentCache = writeTokenCache(System.currentTimeMillis() - 1000); + setSystemProperty("oozie.auth.token.cache", "true"); + runTest(new Callable<Void>() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new OozieCLI().run(args)); + return null; + } + }, conf); + assertTrue(AuthOozieClient.AUTH_TOKEN_CACHE_FILE.exists()); + newCache = IOUtils.getReaderAsString(new FileReader(AuthOozieClient.AUTH_TOKEN_CACHE_FILE), -1); + assertFalse("Expired token should have been updated but was not", currentCache.equals(newCache)); + } + + private static String writeTokenCache(long expirationTime) throws Exception { + AuthenticationToken authToken = new AuthenticationToken(getOozieUser(), getOozieUser(), "simple"); + authToken.setExpires(expirationTime); + String signedTokenStr = computeSignature(SECRET.getBytes(Charset.forName("UTF-8")), authToken.toString()); + signedTokenStr = authToken.toString() + "&s=" + signedTokenStr; + PrintWriter pw = new PrintWriter(AuthOozieClient.AUTH_TOKEN_CACHE_FILE); + pw.write(signedTokenStr); + pw.close(); + return signedTokenStr; + } + + // Borrowed from org.apache.hadoop.security.authentication.util.Signer#computeSignature + private static String computeSignature(byte[] secret, String str) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA"); + md.update(str.getBytes()); + md.update(secret); + byte[] digest = md.digest(); + return new Base64(0).encodeToString(digest); } /** http://git-wip-us.apache.org/repos/asf/oozie/blob/b50d642a/release-log.txt ---------------------------------------------------------------------- diff --git a/release-log.txt b/release-log.txt index 50e76a2..7a2f58e 100644 --- a/release-log.txt +++ b/release-log.txt @@ -1,5 +1,6 @@ -- Oozie 4.3.0 release (trunk - unreleased) +OOZIE-2485 Oozie client keeps trying to use expired auth token (rkanter) OOZIE-2490 Oozie can't set hadoop.security.token.service.use_ip (rkanter) OOZIE-2474 <job-xml> is not being applied to the launcher job (rkanter) OOZIE-2486 TestSLAEventsGetForFilterJPAExecutor is flakey (rkanter)
