diff --git a/hadoop-common-project/hadoop-auth/pom.xml b/hadoop-common-project/hadoop-auth/pom.xml index d7e1a016f72..0f9a6d1ddab 100644 --- a/hadoop-common-project/hadoop-auth/pom.xml +++ b/hadoop-common-project/hadoop-auth/pom.xml @@ -110,6 +110,7 @@ **/${test.exclude}.java ${test.exclude.pattern} **/TestKerberosAuth*.java + **/TestAltKerberosAuth*.java **/Test*$*.java diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AltKerberosAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AltKerberosAuthenticationHandler.java new file mode 100644 index 00000000000..e786e37df8e --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AltKerberosAuthenticationHandler.java @@ -0,0 +1,150 @@ +/** + * 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.server; + +import java.io.IOException; +import java.util.Properties; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.hadoop.security.authentication.client.AuthenticationException; + + /** + * The {@link AltKerberosAuthenticationHandler} behaves exactly the same way as + * the {@link KerberosAuthenticationHandler}, except that it allows for an + * alternative form of authentication for browsers while still using Kerberos + * for Java access. This is an abstract class that should be subclassed + * to allow a developer to implement their own custom authentication for browser + * access. The alternateAuthenticate method will be called whenever a request + * comes from a browser. + *

+ */ +public abstract class AltKerberosAuthenticationHandler + extends KerberosAuthenticationHandler { + + /** + * Constant that identifies the authentication mechanism. + */ + public static final String TYPE = "alt-kerberos"; + + /** + * Constant for the configuration property that indicates which user agents + * are not considered browsers (comma separated) + */ + public static final String NON_BROWSER_USER_AGENTS = + TYPE + ".non-browser.user-agents"; + private static final String NON_BROWSER_USER_AGENTS_DEFAULT = + "java,curl,wget,perl"; + + private String[] nonBrowserUserAgents; + + /** + * Returns the authentication type of the authentication handler, + * 'alt-kerberos'. + *

+ * + * @return the authentication type of the authentication handler, + * 'alt-kerberos'. + */ + @Override + public String getType() { + return TYPE; + } + + @Override + public void init(Properties config) throws ServletException { + super.init(config); + + nonBrowserUserAgents = config.getProperty( + NON_BROWSER_USER_AGENTS, NON_BROWSER_USER_AGENTS_DEFAULT) + .split("\\W*,\\W*"); + for (int i = 0; i < nonBrowserUserAgents.length; i++) { + nonBrowserUserAgents[i] = nonBrowserUserAgents[i].toLowerCase(); + } + } + + /** + * It enforces the the Kerberos SPNEGO authentication sequence returning an + * {@link AuthenticationToken} only after the Kerberos SPNEGO sequence has + * completed successfully (in the case of Java access) and only after the + * custom authentication implemented by the subclass in alternateAuthenticate + * has completed successfully (in the case of browser access). + *

+ * + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return an authentication token if the request is authorized or null + * + * @throws IOException thrown if an IO error occurred + * @throws AuthenticationException thrown if an authentication error occurred + */ + @Override + public AuthenticationToken authenticate(HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + AuthenticationToken token; + if (isBrowser(request.getHeader("User-Agent"))) { + token = alternateAuthenticate(request, response); + } + else { + token = super.authenticate(request, response); + } + return token; + } + + /** + * This method parses the User-Agent String and returns whether or not it + * refers to a browser. If its not a browser, then Kerberos authentication + * will be used; if it is a browser, alternateAuthenticate from the subclass + * will be used. + *

+ * A User-Agent String is considered to be a browser if it does not contain + * any of the values from alt-kerberos.non-browser.user-agents; the default + * behavior is to consider everything a browser unless it contains one of: + * "java", "curl", "wget", or "perl". Subclasses can optionally override + * this method to use different behavior. + * + * @param userAgent The User-Agent String, or null if there isn't one + * @return true if the User-Agent String refers to a browser, false if not + */ + protected boolean isBrowser(String userAgent) { + if (userAgent == null) { + return false; + } + userAgent = userAgent.toLowerCase(); + boolean isBrowser = true; + for (String nonBrowserUserAgent : nonBrowserUserAgents) { + if (userAgent.contains(nonBrowserUserAgent)) { + isBrowser = false; + break; + } + } + return isBrowser; + } + + /** + * Subclasses should implement this method to provide the custom + * authentication to be used for browsers. + * + * @param request the HTTP client request. + * @param response the HTTP client response. + * @return an authentication token if the request is authorized, or null + * @throws IOException thrown if an IO error occurs + * @throws AuthenticationException thrown if an authentication error occurs + */ + public abstract AuthenticationToken alternateAuthenticate( + HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException; +} diff --git a/hadoop-common-project/hadoop-auth/src/site/apt/Configuration.apt.vm b/hadoop-common-project/hadoop-auth/src/site/apt/Configuration.apt.vm index e42ee8b4c30..f2fe11d8f6d 100644 --- a/hadoop-common-project/hadoop-auth/src/site/apt/Configuration.apt.vm +++ b/hadoop-common-project/hadoop-auth/src/site/apt/Configuration.apt.vm @@ -176,6 +176,73 @@ Configuration ... ++---+ + +** AltKerberos Configuration + + <>: A KDC must be configured and running. + + The AltKerberos authentication mechanism is a partially implemented derivative + of the Kerberos SPNEGO authentication mechanism which allows a "mixed" form of + authentication where Kerberos SPNEGO is used by non-browsers while an + alternate form of authentication (to be implemented by the user) is used for + browsers. To use AltKerberos as the authentication mechanism (besides + providing an implementation), the authentication filter must be configured + with the following init parameters, in addition to the previously mentioned + Kerberos SPNEGO ones: + + * <<<[PREFIX.]type>>>: the full class name of the implementation of + AltKerberosAuthenticationHandler to use. + + * <<<[PREFIX.]alt-kerberos.non-browser.user-agents>>>: a comma-separated + list of which user-agents should be considered non-browsers. + + <>: + ++---+ + + ... + + + kerberosFilter + org.apache.hadoop.security.auth.server.AuthenticationFilter + + type + org.my.subclass.of.AltKerberosAuthenticationHandler + + + alt-kerberos.non-browser.user-agents + java,curl,wget,perl + + + token.validity + 30 + + + cookie.domain + .foo.com + + + cookie.path + / + + + kerberos.principal + HTTP/localhost@LOCALHOST + + + kerberos.keytab + /tmp/auth.keytab + + + + + kerberosFilter + /kerberos/* + + + ... + +---+ \[ {{{./index.html}Go Back}} \] diff --git a/hadoop-common-project/hadoop-auth/src/site/apt/index.apt.vm b/hadoop-common-project/hadoop-auth/src/site/apt/index.apt.vm index a2e7b5e915a..26fc2492ca0 100644 --- a/hadoop-common-project/hadoop-auth/src/site/apt/index.apt.vm +++ b/hadoop-common-project/hadoop-auth/src/site/apt/index.apt.vm @@ -24,6 +24,11 @@ Hadoop Auth, Java HTTP SPNEGO ${project.version} Hadoop Auth also supports additional authentication mechanisms on the client and the server side via 2 simple interfaces. + Additionally, it provides a partially implemented derivative of the Kerberos + SPNEGO authentication to allow a "mixed" form of authentication where Kerberos + SPNEGO is used by non-browsers while an alternate form of authentication + (to be implemented by the user) is used for browsers. + * License Hadoop Auth is distributed under {{{http://www.apache.org/licenses/}Apache diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAltKerberosAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAltKerberosAuthenticationHandler.java new file mode 100644 index 00000000000..c2d43ebb3ca --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAltKerberosAuthenticationHandler.java @@ -0,0 +1,110 @@ +/** + * 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.server; + +import java.io.IOException; +import java.util.Properties; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.mockito.Mockito; + +public class TestAltKerberosAuthenticationHandler + extends TestKerberosAuthenticationHandler { + + @Override + protected KerberosAuthenticationHandler getNewAuthenticationHandler() { + // AltKerberosAuthenticationHandler is abstract; a subclass would normally + // perform some other authentication when alternateAuthenticate() is called. + // For the test, we'll just return an AuthenticationToken as the other + // authentication is left up to the developer of the subclass + return new AltKerberosAuthenticationHandler() { + @Override + public AuthenticationToken alternateAuthenticate( + HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + return new AuthenticationToken("A", "B", getType()); + } + }; + } + + @Override + protected String getExpectedType() { + return AltKerberosAuthenticationHandler.TYPE; + } + + public void testAlternateAuthenticationAsBrowser() throws Exception { + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + // By default, a User-Agent without "java", "curl", "wget", or "perl" in it + // is considered a browser + Mockito.when(request.getHeader("User-Agent")).thenReturn("Some Browser"); + + AuthenticationToken token = handler.authenticate(request, response); + assertEquals("A", token.getUserName()); + assertEquals("B", token.getName()); + assertEquals(getExpectedType(), token.getType()); + } + + public void testNonDefaultNonBrowserUserAgentAsBrowser() throws Exception { + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + if (handler != null) { + handler.destroy(); + handler = null; + } + handler = getNewAuthenticationHandler(); + Properties props = getDefaultProperties(); + props.setProperty("alt-kerberos.non-browser.user-agents", "foo, bar"); + try { + handler.init(props); + } catch (Exception ex) { + handler = null; + throw ex; + } + + // Pretend we're something that will not match with "foo" (or "bar") + Mockito.when(request.getHeader("User-Agent")).thenReturn("blah"); + // Should use alt authentication + AuthenticationToken token = handler.authenticate(request, response); + assertEquals("A", token.getUserName()); + assertEquals("B", token.getName()); + assertEquals(getExpectedType(), token.getType()); + } + + public void testNonDefaultNonBrowserUserAgentAsNonBrowser() throws Exception { + if (handler != null) { + handler.destroy(); + handler = null; + } + handler = getNewAuthenticationHandler(); + Properties props = getDefaultProperties(); + props.setProperty("alt-kerberos.non-browser.user-agents", "foo, bar"); + try { + handler.init(props); + } catch (Exception ex) { + handler = null; + throw ex; + } + + // Run the kerberos tests again + testRequestWithoutAuthorization(); + testRequestWithInvalidAuthorization(); + testRequestWithAuthorization(); + testRequestWithInvalidKerberosAuthorization(); + } +} diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestKerberosAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestKerberosAuthenticationHandler.java index 692ceab92da..d198e58431d 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestKerberosAuthenticationHandler.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestKerberosAuthenticationHandler.java @@ -28,23 +28,37 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.lang.reflect.Field; import java.util.Properties; import java.util.concurrent.Callable; public class TestKerberosAuthenticationHandler extends TestCase { - private KerberosAuthenticationHandler handler; + protected KerberosAuthenticationHandler handler; + + protected KerberosAuthenticationHandler getNewAuthenticationHandler() { + return new KerberosAuthenticationHandler(); + } + + protected String getExpectedType() { + return KerberosAuthenticationHandler.TYPE; + } + + protected Properties getDefaultProperties() { + Properties props = new Properties(); + props.setProperty(KerberosAuthenticationHandler.PRINCIPAL, + KerberosTestUtils.getServerPrincipal()); + props.setProperty(KerberosAuthenticationHandler.KEYTAB, + KerberosTestUtils.getKeytabFile()); + props.setProperty(KerberosAuthenticationHandler.NAME_RULES, + "RULE:[1:$1@$0](.*@" + KerberosTestUtils.getRealm()+")s/@.*//\n"); + return props; + } @Override protected void setUp() throws Exception { super.setUp(); - handler = new KerberosAuthenticationHandler(); - Properties props = new Properties(); - props.setProperty(KerberosAuthenticationHandler.PRINCIPAL, KerberosTestUtils.getServerPrincipal()); - props.setProperty(KerberosAuthenticationHandler.KEYTAB, KerberosTestUtils.getKeytabFile()); - props.setProperty(KerberosAuthenticationHandler.NAME_RULES, - "RULE:[1:$1@$0](.*@" + KerberosTestUtils.getRealm()+")s/@.*//\n"); + handler = getNewAuthenticationHandler(); + Properties props = getDefaultProperties(); try { handler.init(props); } catch (Exception ex) { @@ -71,10 +85,8 @@ public void testNameRules() throws Exception { KerberosName.setRules("RULE:[1:$1@$0](.*@FOO)s/@.*//\nDEFAULT"); - handler = new KerberosAuthenticationHandler(); - Properties props = new Properties(); - props.setProperty(KerberosAuthenticationHandler.PRINCIPAL, KerberosTestUtils.getServerPrincipal()); - props.setProperty(KerberosAuthenticationHandler.KEYTAB, KerberosTestUtils.getKeytabFile()); + handler = getNewAuthenticationHandler(); + Properties props = getDefaultProperties(); props.setProperty(KerberosAuthenticationHandler.NAME_RULES, "RULE:[1:$1@$0](.*@BAR)s/@.*//\nDEFAULT"); try { handler.init(props); @@ -97,8 +109,7 @@ public void testInit() throws Exception { } public void testType() throws Exception { - KerberosAuthenticationHandler handler = new KerberosAuthenticationHandler(); - assertEquals(KerberosAuthenticationHandler.TYPE, handler.getType()); + assertEquals(getExpectedType(), handler.getType()); } public void testRequestWithoutAuthorization() throws Exception { @@ -182,7 +193,7 @@ public String call() throws Exception { assertEquals(KerberosTestUtils.getClientPrincipal(), authToken.getName()); assertTrue(KerberosTestUtils.getClientPrincipal().startsWith(authToken.getUserName())); - assertEquals(KerberosAuthenticationHandler.TYPE, authToken.getType()); + assertEquals(getExpectedType(), authToken.getType()); } else { Mockito.verify(response).setHeader(Mockito.eq(KerberosAuthenticator.WWW_AUTHENTICATE), Mockito.matches(KerberosAuthenticator.NEGOTIATE + " .*")); diff --git a/hadoop-common-project/hadoop-common/CHANGES.txt b/hadoop-common-project/hadoop-common/CHANGES.txt index c72a153e2ac..8cdf3264061 100644 --- a/hadoop-common-project/hadoop-common/CHANGES.txt +++ b/hadoop-common-project/hadoop-common/CHANGES.txt @@ -14,6 +14,8 @@ Release 2.0.3-alpha - Unreleased HADOOP-9090. Support on-demand publish of metrics. (Mostafa Elhemali via suresh) + HADOOP-9054. Add AuthenticationHandler that uses Kerberos but allows for + an alternate form of authentication for browsers. (rkanter via tucu) IMPROVEMENTS