From 875256834b892b574499d5fe68f95a9aed244f7d Mon Sep 17 00:00:00 2001 From: Robert Kanter Date: Fri, 13 Feb 2015 14:01:46 -0800 Subject: [PATCH] HADOOP-11467. KerberosAuthenticator can connect to a non-secure cluster. (yzhangal via rkanter) --- .../client/KerberosAuthenticator.java | 26 ++- .../server/AuthenticationToken.java | 162 ++----------- .../authentication/util/AuthToken.java | 218 ++++++++++++++++++ .../server/TestAuthenticationToken.java | 100 -------- .../authentication/util/TestAuthToken.java | 127 ++++++++++ .../hadoop-common/CHANGES.txt | 3 + 6 files changed, 385 insertions(+), 251 deletions(-) create mode 100644 hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/AuthToken.java create mode 100644 hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestAuthToken.java diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java index 323b019eb8..e107810790 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java @@ -14,6 +14,7 @@ package org.apache.hadoop.security.authentication.client; import org.apache.commons.codec.binary.Base64; +import org.apache.hadoop.security.authentication.util.AuthToken; import org.apache.hadoop.security.authentication.util.KerberosUtil; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSManager; @@ -29,6 +30,7 @@ import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; + import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; @@ -187,13 +189,18 @@ public void authenticate(URL url, AuthenticatedURL.Token token) conn.setRequestMethod(AUTH_HTTP_METHOD); conn.connect(); + boolean needFallback = false; if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { LOG.debug("JDK performed authentication on our behalf."); // If the JDK already did the SPNEGO back-and-forth for // us, just pull out the token. AuthenticatedURL.extractToken(conn, token); - return; - } else if (isNegotiate()) { + if (isTokenKerberos(token)) { + return; + } + needFallback = true; + } + if (!needFallback && isNegotiate()) { LOG.debug("Performing our own SPNEGO sequence."); doSpnegoSequence(token); } else { @@ -224,6 +231,21 @@ protected Authenticator getFallBackAuthenticator() { return auth; } + /* + * Check if the passed token is of type "kerberos" or "kerberos-dt" + */ + private boolean isTokenKerberos(AuthenticatedURL.Token token) + throws AuthenticationException { + if (token.isSet()) { + AuthToken aToken = AuthToken.parse(token.toString()); + if (aToken.getType().equals("kerberos") || + aToken.getType().equals("kerberos-dt")) { + return true; + } + } + return false; + } + /* * Indicates if the response is starting a SPNEGO negotiation. */ diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java index bb3e71da61..0e2b45d9de 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java @@ -14,14 +14,9 @@ package org.apache.hadoop.security.authentication.server; import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.util.AuthToken; import java.security.Principal; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; import javax.servlet.http.HttpServletRequest; @@ -34,38 +29,21 @@ * and received in HTTP client responses and requests as a HTTP cookie (this is * done by the {@link AuthenticationFilter}). */ -public class AuthenticationToken implements Principal { +public class AuthenticationToken extends AuthToken { /** * Constant that identifies an anonymous request. */ public static final AuthenticationToken ANONYMOUS = new AuthenticationToken(); - private static final String ATTR_SEPARATOR = "&"; - private static final String USER_NAME = "u"; - private static final String PRINCIPAL = "p"; - private static final String EXPIRES = "e"; - private static final String TYPE = "t"; - - private final static Set ATTRIBUTES = - new HashSet(Arrays.asList(USER_NAME, PRINCIPAL, EXPIRES, TYPE)); - - private String userName; - private String principal; - private String type; - private long expires; - private String token; - private AuthenticationToken() { - userName = null; - principal = null; - type = null; - expires = -1; - token = "ANONYMOUS"; - generateToken(); + super(); } - private static final String ILLEGAL_ARG_MSG = " is NULL, empty or contains a '" + ATTR_SEPARATOR + "'"; + private AuthenticationToken(AuthToken token) { + super(token.getUserName(), token.getName(), token.getType()); + setExpires(token.getExpires()); + } /** * Creates an authentication token. @@ -77,25 +55,7 @@ private AuthenticationToken() { * (System.currentTimeMillis() + validityPeriod). */ public AuthenticationToken(String userName, String principal, String type) { - checkForIllegalArgument(userName, "userName"); - checkForIllegalArgument(principal, "principal"); - checkForIllegalArgument(type, "type"); - this.userName = userName; - this.principal = principal; - this.type = type; - this.expires = -1; - } - - /** - * Check if the provided value is invalid. Throw an error if it is invalid, NOP otherwise. - * - * @param value the value to check. - * @param name the parameter name to use in an error message if the value is invalid. - */ - private static void checkForIllegalArgument(String value, String name) { - if (value == null || value.length() == 0 || value.contains(ATTR_SEPARATOR)) { - throw new IllegalArgumentException(name + ILLEGAL_ARG_MSG); - } + super(userName, principal, type); } /** @@ -105,79 +65,17 @@ private static void checkForIllegalArgument(String value, String name) { */ public void setExpires(long expires) { if (this != AuthenticationToken.ANONYMOUS) { - this.expires = expires; - generateToken(); + super.setExpires(expires); } } /** - * Generates the token. - */ - private void generateToken() { - StringBuffer sb = new StringBuffer(); - sb.append(USER_NAME).append("=").append(getUserName()).append(ATTR_SEPARATOR); - sb.append(PRINCIPAL).append("=").append(getName()).append(ATTR_SEPARATOR); - sb.append(TYPE).append("=").append(getType()).append(ATTR_SEPARATOR); - sb.append(EXPIRES).append("=").append(getExpires()); - token = sb.toString(); - } - - /** - * Returns the user name. + * Returns true if the token has expired. * - * @return the user name. - */ - public String getUserName() { - return userName; - } - - /** - * Returns the principal name (this method name comes from the JDK {@link Principal} interface). - * - * @return the principal name. - */ - @Override - public String getName() { - return principal; - } - - /** - * Returns the authentication mechanism of the token. - * - * @return the authentication mechanism of the token. - */ - public String getType() { - return type; - } - - /** - * Returns the expiration time of the token. - * - * @return the expiration time of the token, in milliseconds since Epoc. - */ - public long getExpires() { - return expires; - } - - /** - * Returns if the token has expired. - * - * @return if the token has expired. + * @return true if the token has expired. */ public boolean isExpired() { - return getExpires() != -1 && System.currentTimeMillis() > getExpires(); - } - - /** - * Returns the string representation of the token. - *

- * This string representation is parseable by the {@link #parse} method. - * - * @return the string representation of the token. - */ - @Override - public String toString() { - return token; + return super.isExpired(); } /** @@ -191,40 +89,6 @@ public String toString() { * an authentication token. */ public static AuthenticationToken parse(String tokenStr) throws AuthenticationException { - Map map = split(tokenStr); - if (!map.keySet().equals(ATTRIBUTES)) { - throw new AuthenticationException("Invalid token string, missing attributes"); - } - long expires = Long.parseLong(map.get(EXPIRES)); - AuthenticationToken token = new AuthenticationToken(map.get(USER_NAME), map.get(PRINCIPAL), map.get(TYPE)); - token.setExpires(expires); - return token; + return new AuthenticationToken(AuthToken.parse(tokenStr)); } - - /** - * Splits the string representation of a token into attributes pairs. - * - * @param tokenStr string representation of a token. - * - * @return a map with the attribute pairs of the token. - * - * @throws AuthenticationException thrown if the string representation of the token could not be broken into - * attribute pairs. - */ - private static Map split(String tokenStr) throws AuthenticationException { - Map map = new HashMap(); - StringTokenizer st = new StringTokenizer(tokenStr, ATTR_SEPARATOR); - while (st.hasMoreTokens()) { - String part = st.nextToken(); - int separator = part.indexOf('='); - if (separator == -1) { - throw new AuthenticationException("Invalid authentication token"); - } - String key = part.substring(0, separator); - String value = part.substring(separator + 1); - map.put(key, value); - } - return map; - } - } diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/AuthToken.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/AuthToken.java new file mode 100644 index 0000000000..7269eb2744 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/AuthToken.java @@ -0,0 +1,218 @@ +/** + * 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.security.authentication.util; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; + +import java.security.Principal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +/** + */ +public class AuthToken implements Principal { + + /** + * Constant that identifies an anonymous request. + */ + + private static final String ATTR_SEPARATOR = "&"; + private static final String USER_NAME = "u"; + private static final String PRINCIPAL = "p"; + private static final String EXPIRES = "e"; + private static final String TYPE = "t"; + + private final static Set ATTRIBUTES = + new HashSet(Arrays.asList(USER_NAME, PRINCIPAL, EXPIRES, TYPE)); + + private String userName; + private String principal; + private String type; + private long expires; + private String tokenStr; + + protected AuthToken() { + userName = null; + principal = null; + type = null; + expires = -1; + tokenStr = "ANONYMOUS"; + generateToken(); + } + + private static final String ILLEGAL_ARG_MSG = " is NULL, empty or contains a '" + ATTR_SEPARATOR + "'"; + + /** + * Creates an authentication token. + * + * @param userName user name. + * @param principal principal (commonly matches the user name, with Kerberos is the full/long principal + * name while the userName is the short name). + * @param type the authentication mechanism name. + * (System.currentTimeMillis() + validityPeriod). + */ + public AuthToken(String userName, String principal, String type) { + checkForIllegalArgument(userName, "userName"); + checkForIllegalArgument(principal, "principal"); + checkForIllegalArgument(type, "type"); + this.userName = userName; + this.principal = principal; + this.type = type; + this.expires = -1; + } + + /** + * Check if the provided value is invalid. Throw an error if it is invalid, NOP otherwise. + * + * @param value the value to check. + * @param name the parameter name to use in an error message if the value is invalid. + */ + protected static void checkForIllegalArgument(String value, String name) { + if (value == null || value.length() == 0 || value.contains(ATTR_SEPARATOR)) { + throw new IllegalArgumentException(name + ILLEGAL_ARG_MSG); + } + } + + /** + * Sets the expiration of the token. + * + * @param expires expiration time of the token in milliseconds since the epoch. + */ + public void setExpires(long expires) { + this.expires = expires; + generateToken(); + } + + /** + * Returns true if the token has expired. + * + * @return true if the token has expired. + */ + public boolean isExpired() { + return getExpires() != -1 && System.currentTimeMillis() > getExpires(); + } + + /** + * Generates the token. + */ + private void generateToken() { + StringBuffer sb = new StringBuffer(); + sb.append(USER_NAME).append("=").append(getUserName()).append(ATTR_SEPARATOR); + sb.append(PRINCIPAL).append("=").append(getName()).append(ATTR_SEPARATOR); + sb.append(TYPE).append("=").append(getType()).append(ATTR_SEPARATOR); + sb.append(EXPIRES).append("=").append(getExpires()); + tokenStr = sb.toString(); + } + + /** + * Returns the user name. + * + * @return the user name. + */ + public String getUserName() { + return userName; + } + + /** + * Returns the principal name (this method name comes from the JDK {@link Principal} interface). + * + * @return the principal name. + */ + @Override + public String getName() { + return principal; + } + + /** + * Returns the authentication mechanism of the token. + * + * @return the authentication mechanism of the token. + */ + public String getType() { + return type; + } + + /** + * Returns the expiration time of the token. + * + * @return the expiration time of the token, in milliseconds since Epoc. + */ + public long getExpires() { + return expires; + } + + /** + * Returns the string representation of the token. + *

+ * This string representation is parseable by the {@link #parse} method. + * + * @return the string representation of the token. + */ + @Override + public String toString() { + return tokenStr; + } + + public static AuthToken parse(String tokenStr) throws AuthenticationException { + if (tokenStr.length() >= 2) { + // strip the \" at the two ends of the tokenStr + if (tokenStr.charAt(0) == '\"' && + tokenStr.charAt(tokenStr.length()-1) == '\"') { + tokenStr = tokenStr.substring(1, tokenStr.length()-1); + } + } + Map map = split(tokenStr); + // remove the signature part, since client doesn't care about it + map.remove("s"); + + if (!map.keySet().equals(ATTRIBUTES)) { + throw new AuthenticationException("Invalid token string, missing attributes"); + } + long expires = Long.parseLong(map.get(EXPIRES)); + AuthToken token = new AuthToken(map.get(USER_NAME), map.get(PRINCIPAL), map.get(TYPE)); + token.setExpires(expires); + return token; + } + + /** + * Splits the string representation of a token into attributes pairs. + * + * @param tokenStr string representation of a token. + * + * @return a map with the attribute pairs of the token. + * + * @throws AuthenticationException thrown if the string representation of the token could not be broken into + * attribute pairs. + */ + private static Map split(String tokenStr) throws AuthenticationException { + Map map = new HashMap(); + StringTokenizer st = new StringTokenizer(tokenStr, ATTR_SEPARATOR); + while (st.hasMoreTokens()) { + String part = st.nextToken(); + int separator = part.indexOf('='); + if (separator == -1) { + throw new AuthenticationException("Invalid authentication token"); + } + String key = part.substring(0, separator); + String value = part.substring(separator + 1); + map.put(key, value); + } + return map; + } + +} diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationToken.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationToken.java index c17c71033a..6727def596 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationToken.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationToken.java @@ -13,7 +13,6 @@ */ package org.apache.hadoop.security.authentication.server; -import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.junit.Assert; import org.junit.Test; @@ -28,103 +27,4 @@ public void testAnonymous() { Assert.assertEquals(-1, AuthenticationToken.ANONYMOUS.getExpires()); Assert.assertFalse(AuthenticationToken.ANONYMOUS.isExpired()); } - - @Test - public void testConstructor() throws Exception { - try { - new AuthenticationToken(null, "p", "t"); - Assert.fail(); - } catch (IllegalArgumentException ex) { - // Expected - } catch (Throwable ex) { - Assert.fail(); - } - try { - new AuthenticationToken("", "p", "t"); - Assert.fail(); - } catch (IllegalArgumentException ex) { - // Expected - } catch (Throwable ex) { - Assert.fail(); - } - try { - new AuthenticationToken("u", null, "t"); - Assert.fail(); - } catch (IllegalArgumentException ex) { - // Expected - } catch (Throwable ex) { - Assert.fail(); - } - try { - new AuthenticationToken("u", "", "t"); - Assert.fail(); - } catch (IllegalArgumentException ex) { - // Expected - } catch (Throwable ex) { - Assert.fail(); - } - try { - new AuthenticationToken("u", "p", null); - Assert.fail(); - } catch (IllegalArgumentException ex) { - // Expected - } catch (Throwable ex) { - Assert.fail(); - } - try { - new AuthenticationToken("u", "p", ""); - Assert.fail(); - } catch (IllegalArgumentException ex) { - // Expected - } catch (Throwable ex) { - Assert.fail(); - } - new AuthenticationToken("u", "p", "t"); - } - - @Test - public void testGetters() throws Exception { - long expires = System.currentTimeMillis() + 50; - AuthenticationToken token = new AuthenticationToken("u", "p", "t"); - token.setExpires(expires); - Assert.assertEquals("u", token.getUserName()); - Assert.assertEquals("p", token.getName()); - Assert.assertEquals("t", token.getType()); - Assert.assertEquals(expires, token.getExpires()); - Assert.assertFalse(token.isExpired()); - Thread.sleep(70); // +20 msec fuzz for timer granularity. - Assert.assertTrue(token.isExpired()); - } - - @Test - public void testToStringAndParse() throws Exception { - long expires = System.currentTimeMillis() + 50; - AuthenticationToken token = new AuthenticationToken("u", "p", "t"); - token.setExpires(expires); - String str = token.toString(); - token = AuthenticationToken.parse(str); - Assert.assertEquals("p", token.getName()); - Assert.assertEquals("t", token.getType()); - Assert.assertEquals(expires, token.getExpires()); - Assert.assertFalse(token.isExpired()); - Thread.sleep(70); // +20 msec fuzz for timer granularity. - Assert.assertTrue(token.isExpired()); - } - - @Test - public void testParseInvalid() throws Exception { - long expires = System.currentTimeMillis() + 50; - AuthenticationToken token = new AuthenticationToken("u", "p", "t"); - token.setExpires(expires); - String str = token.toString(); - str = str.substring(0, str.indexOf("e=")); - try { - AuthenticationToken.parse(str); - Assert.fail(); - } catch (AuthenticationException ex) { - // Expected - } catch (Exception ex) { - Assert.fail(); - } - } } diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestAuthToken.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestAuthToken.java new file mode 100644 index 0000000000..1cb9bf3326 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestAuthToken.java @@ -0,0 +1,127 @@ +/** + * 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.security.authentication.util; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.junit.Assert; +import org.junit.Test; + +public class TestAuthToken { + + @Test + public void testConstructor() throws Exception { + try { + new AuthToken(null, "p", "t"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + // Expected + } catch (Throwable ex) { + Assert.fail(); + } + try { + new AuthToken("", "p", "t"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + // Expected + } catch (Throwable ex) { + Assert.fail(); + } + try { + new AuthToken("u", null, "t"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + // Expected + } catch (Throwable ex) { + Assert.fail(); + } + try { + new AuthToken("u", "", "t"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + // Expected + } catch (Throwable ex) { + Assert.fail(); + } + try { + new AuthToken("u", "p", null); + Assert.fail(); + } catch (IllegalArgumentException ex) { + // Expected + } catch (Throwable ex) { + Assert.fail(); + } + try { + new AuthToken("u", "p", ""); + Assert.fail(); + } catch (IllegalArgumentException ex) { + // Expected + } catch (Throwable ex) { + Assert.fail(); + } + new AuthToken("u", "p", "t"); + } + + @Test + public void testGetters() throws Exception { + long expires = System.currentTimeMillis() + 50; + AuthToken token = new AuthToken("u", "p", "t"); + token.setExpires(expires); + Assert.assertEquals("u", token.getUserName()); + Assert.assertEquals("p", token.getName()); + Assert.assertEquals("t", token.getType()); + Assert.assertEquals(expires, token.getExpires()); + Assert.assertFalse(token.isExpired()); + Thread.sleep(70); // +20 msec fuzz for timer granularity. + Assert.assertTrue(token.isExpired()); + } + + @Test + public void testToStringAndParse() throws Exception { + long expires = System.currentTimeMillis() + 50; + AuthToken token = new AuthToken("u", "p", "t"); + token.setExpires(expires); + String str = token.toString(); + token = AuthToken.parse(str); + Assert.assertEquals("p", token.getName()); + Assert.assertEquals("t", token.getType()); + Assert.assertEquals(expires, token.getExpires()); + Assert.assertFalse(token.isExpired()); + Thread.sleep(70); // +20 msec fuzz for timer granularity. + Assert.assertTrue(token.isExpired()); + } + + @Test + public void testParseValidAndInvalid() throws Exception { + long expires = System.currentTimeMillis() + 50; + AuthToken token = new AuthToken("u", "p", "t"); + token.setExpires(expires); + String ostr = token.toString(); + + String str1 = "\"" + ostr + "\""; + AuthToken.parse(str1); + + String str2 = ostr + "&s=1234"; + AuthToken.parse(str2); + + String str = ostr.substring(0, ostr.indexOf("e=")); + try { + AuthToken.parse(str); + Assert.fail(); + } catch (AuthenticationException ex) { + // Expected + } catch (Exception ex) { + Assert.fail(); + } + } +} diff --git a/hadoop-common-project/hadoop-common/CHANGES.txt b/hadoop-common-project/hadoop-common/CHANGES.txt index 0d8c02f0f9..99320cbfb2 100644 --- a/hadoop-common-project/hadoop-common/CHANGES.txt +++ b/hadoop-common-project/hadoop-common/CHANGES.txt @@ -928,6 +928,9 @@ Release 2.7.0 - UNRELEASED HADOOP-11587. TestMapFile#testMainMethodMapFile creates test files in hadoop-common project root. (Xiaoyu Yao via wheat9) + HADOOP-11467. KerberosAuthenticator can connect to a non-secure cluster. + (yzhangal via rkanter) + Release 2.6.1 - UNRELEASED INCOMPATIBLE CHANGES