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)

Reply via email to