Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial drop, v 0.1.0

  • Loading branch information...
commit 47638dde13d84ecef43b55292e0115c4c9417b14 1 parent 1158713
@tucu00 tucu00 authored
Showing with 4,985 additions and 0 deletions.
  1. +203 −0 LICENSE.txt
  2. +39 −0 README.txt
  3. +110 −0 alfredo/pom.xml
  4. +243 −0 alfredo/src/main/java/com/cloudera/alfredo/client/AuthenticatedURL.java
  5. +52 −0 alfredo/src/main/java/com/cloudera/alfredo/client/AuthenticationException.java
  6. +42 −0 alfredo/src/main/java/com/cloudera/alfredo/client/Authenticator.java
  7. +266 −0 alfredo/src/main/java/com/cloudera/alfredo/client/KerberosAuthenticator.java
  8. +78 −0 alfredo/src/main/java/com/cloudera/alfredo/client/PseudoAuthenticator.java
  9. +346 −0 alfredo/src/main/java/com/cloudera/alfredo/server/AuthenticationFilter.java
  10. +81 −0 alfredo/src/main/java/com/cloudera/alfredo/server/AuthenticationHandler.java
  11. +220 −0 alfredo/src/main/java/com/cloudera/alfredo/server/AuthenticationToken.java
  12. +287 −0 alfredo/src/main/java/com/cloudera/alfredo/server/KerberosAuthenticationHandler.java
  13. +127 −0 alfredo/src/main/java/com/cloudera/alfredo/server/PseudoAuthenticationHandler.java
  14. +102 −0 alfredo/src/main/java/com/cloudera/alfredo/util/Signer.java
  15. +33 −0 alfredo/src/main/java/com/cloudera/alfredo/util/SignerException.java
  16. +148 −0 alfredo/src/site/apt/Configuration.apt
  17. +178 −0 alfredo/src/site/apt/index.apt
  18. +19 −0 alfredo/src/site/site.xml
  19. +135 −0 alfredo/src/test/java/com/cloudera/alfredo/KerberosTestUtils.java
  20. +162 −0 alfredo/src/test/java/com/cloudera/alfredo/client/AuthenticatorTestCase.java
  21. +83 −0 alfredo/src/test/java/com/cloudera/alfredo/client/TestAuthenticatedURL.java
  22. +89 −0 alfredo/src/test/java/com/cloudera/alfredo/client/TestKerberosAuthenticator.java
  23. +92 −0 alfredo/src/test/java/com/cloudera/alfredo/client/TestPseudoAuthenticator.java
  24. +447 −0 alfredo/src/test/java/com/cloudera/alfredo/server/TestAuthenticationFilter.java
  25. +138 −0 alfredo/src/test/java/com/cloudera/alfredo/server/TestAuthenticationToken.java
  26. +184 −0 alfredo/src/test/java/com/cloudera/alfredo/server/TestKerberosAuthenticationHandler.java
  27. +120 −0 alfredo/src/test/java/com/cloudera/alfredo/server/TestPseudoAuthenticationHandler.java
  28. +87 −0 alfredo/src/test/java/com/cloudera/alfredo/util/TestSigner.java
  29. +91 −0 examples/pom.xml
  30. +187 −0 examples/src/main/java/com/cloudera/alfredo/examples/RequestLoggerFilter.java
  31. +61 −0 examples/src/main/java/com/cloudera/alfredo/examples/WhoClient.java
  32. +47 −0 examples/src/main/java/com/cloudera/alfredo/examples/WhoServlet.java
  33. +23 −0 examples/src/main/resources/log4j.properties
  34. +121 −0 examples/src/main/webapp/WEB-INF/web.xml
  35. +22 −0 examples/src/main/webapp/annonymous/index.html
  36. +22 −0 examples/src/main/webapp/index.html
  37. +22 −0 examples/src/main/webapp/kerberos/index.html
  38. +22 −0 examples/src/main/webapp/simple/index.html
  39. +256 −0 pom.xml
View
203 LICENSE.txt
@@ -0,0 +1,203 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
+
View
39 README.txt
@@ -0,0 +1,39 @@
+Alfredo, Java HTTP SPNEGO
+
+Alfredo is a Java library consisting of a client and a server components
+to enable Kerberos SPNEGO authentication for HTTP.
+
+Alfredo is distributed under Apache License 2.0.
+
+The client component is the AuthenticatedURL class.
+
+The server component is the AuthenticationFilter servlet filter class.
+
+Authentication mechanisms support is pluggable in both the client and
+the server components via interfaces.
+
+In addition to Kerberos SPNEGO, Alfredo also supports Pseudo/Simple
+authentication (trusting the value of the query string parameter
+'user.name').
+
+----------------------------------------------------------------------------
+Documentation:
+
+ http://cloudera.github.com/alfredo
+
+----------------------------------------------------------------------------
+Maven information:
+
+ Group Id: com.cloudera.alfredo
+ Artifact Id: alfredo
+ Type: jar
+ Repository: https://repository.cloudera.com/content/repositories/releases
+
+----------------------------------------------------------------------------
+
+If you have any questions/issues, please send an email to:
+
+ cdh-user@cloudera.org
+
+----------------------------------------------------------------------------
+
View
110 alfredo/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+-->
+<project>
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.cloudera.alfredo</groupId>
+ <artifactId>alfredo-main</artifactId>
+ <version>0.1.0</version>
+ </parent>
+ <groupId>com.cloudera.alfredo</groupId>
+ <artifactId>alfredo</artifactId>
+ <version>0.1.0</version>
+ <packaging>jar</packaging>
+
+ <name>Alfredo, Java HTTP SPNEGO</name>
+ <description>Alfredo, Java HTTP SPNEGO</description>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ </license>
+ </licenses>
+
+ <organization>
+ <name>Cloudera Inc.</name>
+ <url>http://www.cloudera.com</url>
+ </organization>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mortbay.jetty</groupId>
+ <artifactId>jetty</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-project-info-reports-plugin</artifactId>
+ <executions>
+ <execution>
+ <configuration>
+ <dependencyLocationsEnabled>false</dependencyLocationsEnabled>
+ </configuration>
+ <goals>
+ <goal>dependencies</goal>
+ </goals>
+ <phase>site</phase>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>javadoc</goal>
+ </goals>
+ <phase>site</phase>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
View
243 alfredo/src/main/java/com/cloudera/alfredo/client/AuthenticatedURL.java
@@ -0,0 +1,243 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.client;
+
+import com.cloudera.alfredo.server.AuthenticationFilter;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The <code>AuthenticatedURL</code> class enables the use of the JDK <code>URL</code> class
+ * against HTTP endpoints protected with the {@link AuthenticationFilter}.
+ * <p/>
+ * The authentication mechanism supported by default are Hadoop Simple authentication
+ * (also known as pseudo authentication) and Kerberos SPNEGO authentication.
+ * <p/>
+ * Additional authentication mechanisms can be supported via {@link Authenticator} implementations.
+ * <p/>
+ * The default {@link Authenticator} is the {@link KerberosAuthenticator} class which supports
+ * automatic fallback from Kerberos SPNEGO to Hadoop Simple authentication.
+ * <p/>
+ * <code>AuthenticatedURL</code> instances are not thread-safe.
+ * <p/>
+ * The usage pattern of the <code>AuthenticatedURL</code> is:
+ * <p/>
+ * <verbatim>
+ *
+ * // establishing an initial connection
+ *
+ * URL url = new URL("http://foo:8080/bar");
+ * AuthenticatedURL.Token token = new AuthenticatedURL.Token();
+ * AuthenticatedURL aUrl = new AuthenticatedURL();
+ * HttpURLConnection conn = new AuthenticatedURL(url, token).openConnection();
+ * ....
+ * // use the 'conn' instance
+ * ....
+ *
+ * // establishing a follow up connection using a token from the previous connection
+ *
+ * conn = new AuthenticatedURL(url, token).openConnection();
+ * ....
+ * // use the 'conn' instance
+ * ....
+ *
+ * </verbatim>
+ */
+public class AuthenticatedURL {
+
+ /**
+ * Name of the HTTP cookie used for the authentication token between the client and the server.
+ */
+ public static final String AUTH_COOKIE = "alfredo.auth";
+
+ private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
+
+ /**
+ * Client side authentication token.
+ */
+ public static class Token {
+
+ private String token;
+
+ /**
+ * Creates a token.
+ */
+ public Token() {
+ }
+
+ /**
+ * Returns if a token from the server has been set.
+ *
+ * @return if a token from the server has been set.
+ */
+ public boolean isSet() {
+ return token != null;
+ }
+
+ /**
+ * Sets a token.
+ *
+ * @param tokenStr string representation of the tokenStr.
+ */
+ void set(String tokenStr) {
+ token = tokenStr;
+ }
+
+ /**
+ * Returns the string representation of the token.
+ *
+ * @return the string representation of the token.
+ */
+ @Override
+ public String toString() {
+ return token;
+ }
+
+ }
+
+ private static Class<? extends Authenticator> DEFAULT_AUTHENTICATOR = KerberosAuthenticator.class;
+
+ /**
+ * Sets the default {@link Authenticator} class to use when an <code>AuthenticatedURL</code> instance
+ * is created without specifying an authenticator.
+ *
+ * @param authenticator the authenticator class to use as default.
+ */
+ public static void setDefaultAuthenticator(Class<? extends Authenticator> authenticator) {
+ DEFAULT_AUTHENTICATOR = authenticator;
+ }
+
+ /**
+ * Returns the default {@link Authenticator} class to use when an <code>AuthenticatedURL</code> instance
+ * is created without specifying an authenticator.
+ *
+ * @return the authenticator class to use as default.
+ */
+ public static Class<? extends Authenticator> getDefaultAuthenticator() {
+ return DEFAULT_AUTHENTICATOR;
+ }
+
+ private Authenticator authenticator;
+
+ /**
+ * Creates an <code>AuthenticatedURL</code>.
+ */
+ public AuthenticatedURL() {
+ this(null);
+ }
+
+ /**
+ * Creates an <code>AuthenticatedURL</code>.
+ *
+ * @param authenticator the {@link Authenticator} instance to use, if <code>null</code> a {@link
+ * KerberosAuthenticator} is used.
+ */
+ public AuthenticatedURL(Authenticator authenticator) {
+ try {
+ this.authenticator = (authenticator != null) ? authenticator : DEFAULT_AUTHENTICATOR.newInstance();
+ }
+ catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Returns an authenticated <code>HttpURLConnection</code>.
+ *
+ * @param url the URL to connect to. Only HTTP/S URLs are supported.
+ * @param token the authencation token being used for the user.
+ *
+ * @return an authenticated <code>HttpURLConnection</code>.
+ *
+ * @throws IOException if an IO error occurred.
+ * @throws AuthenticationException if an authentication exception occurred.
+ */
+ public HttpURLConnection openConnection(URL url, Token token) throws IOException, AuthenticationException {
+ if (url == null) {
+ throw new IllegalArgumentException("url cannot be NULL");
+ }
+ if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
+ throw new IllegalArgumentException("url must be for a HTTP or HTTPS resource");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("token cannot be NULL");
+ }
+ authenticator.authenticate(url, token);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ injectToken(conn, token);
+ return conn;
+ }
+
+ /**
+ * Helper method that injects an authentication token to send with a connection.
+ * <p/>
+ *
+ * @param conn connection to inject the authentication token.
+ * @param token authentication token to inject.
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Helper method that extracts an authentication token received from a connection.
+ * <p/>
+ * This method is used by {@link Authenticator} implementations.
+ *
+ * @param conn connection to extract the authentication token from.
+ * @param token the authentication token.
+ *
+ * @throws IOException if an IO error occurred.
+ * @throws AuthenticationException if an authentication exception occurred.
+ */
+ public static void extractToken(HttpURLConnection conn, Token token) throws IOException, AuthenticationException {
+ if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ 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) {
+ token.set(value);
+ }
+ }
+ }
+ }
+ }
+ else {
+ throw new AuthenticationException("Authentication failed, status: " + conn.getResponseCode() +
+ ", message: " + conn.getResponseMessage());
+ }
+ }
+
+}
View
52 alfredo/src/main/java/com/cloudera/alfredo/client/AuthenticationException.java
@@ -0,0 +1,52 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.client;
+
+/**
+ * Exception thrown when an authentication error occurrs.
+ */
+public class AuthenticationException extends Exception {
+
+ /**
+ * Creates an <code>AuthenticationException</code>.
+ *
+ * @param cause original exception.
+ */
+ public AuthenticationException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates an <code>AuthenticationException</code>.
+ *
+ * @param msg exception message.
+ */
+ public AuthenticationException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Creates an <code>AuthenticationException</code>.
+ *
+ * @param msg exception message.
+ * @param cause original exception.
+ */
+ public AuthenticationException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
View
42 alfredo/src/main/java/com/cloudera/alfredo/client/Authenticator.java
@@ -0,0 +1,42 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.client;
+
+
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * Interface for client authentication mechanisms.
+ * <p/>
+ * Implementations are use-once instances, they don't need to be thread safe.
+ */
+public interface Authenticator {
+
+ /**
+ * Authenticates against a URL and returns a {@link AuthenticatedURL.Token} to be
+ * used by subsequent requests.
+ *
+ * @param url the URl to authenticate against.
+ * @param token the authencation token being used for the user.
+ * @throws IOException if an IO error occurred.
+ * @throws AuthenticationException if an authentication error occurred.
+ */
+ public void authenticate(URL url, AuthenticatedURL.Token token) throws IOException, AuthenticationException;
+
+}
View
266 alfredo/src/main/java/com/cloudera/alfredo/client/KerberosAuthenticator.java
@@ -0,0 +1,266 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.client;
+
+import com.sun.security.auth.module.Krb5LoginModule;
+import org.apache.commons.codec.binary.Base64;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import sun.security.jgss.GSSUtil;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.AppConfigurationEntry;
+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;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The <code>KerberosAuthenticator</code> implements the Kerberos SPNEGO authentication sequence.
+ * <p/>
+ * It uses the default principal for the Kerberos cache (normally set via kinit).
+ * <p/>
+ * It falls back to the {@link PseudoAuthenticator} if the HTTP endpoint does not trigger an SPNEGO authentication
+ * sequence.
+ */
+public class KerberosAuthenticator implements Authenticator {
+
+ /**
+ * HTTP header used by the SPNEGO server endpoint during an authentication sequence.
+ */
+ public static String WWW_AUTHENTICATE = "WWW-Authenticate";
+
+ /**
+ * HTTP header used by the SPNEGO client endpoint during an authentication sequence.
+ */
+ public static String AUTHORIZATION = "Authorization";
+
+ /**
+ * HTTP header prefix used by the SPNEGO client/server endpoints during an authentication sequence.
+ */
+ public static String NEGOTIATE = "Negotiate";
+
+ private static final String AUTH_HTTP_METHOD = "OPTIONS";
+
+ /*
+ * Defines the Kerberos configuration that will be used to obtain the kerberos principal from the
+ * Kerberos cache.
+ */
+ private static class KerberosConfiguration extends Configuration {
+
+ private static final String OS_LOGIN_MODULE_NAME;
+ private static final boolean windows = System.getProperty("os.name").startsWith("Windows");
+
+ static {
+ if (windows) {
+ OS_LOGIN_MODULE_NAME = "com.sun.security.auth.module.NTLoginModule";
+ }
+ else {
+ OS_LOGIN_MODULE_NAME = "com.sun.security.auth.module.UnixLoginModule";
+ }
+ }
+
+ private static final AppConfigurationEntry OS_SPECIFIC_LOGIN =
+ new AppConfigurationEntry(OS_LOGIN_MODULE_NAME,
+ AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+ new HashMap<String, String>());
+
+ private static final Map<String, String> USER_KERBEROS_OPTIONS = new HashMap<String, String>();
+
+ static {
+ USER_KERBEROS_OPTIONS.put("doNotPrompt", "true");
+ USER_KERBEROS_OPTIONS.put("useTicketCache", "true");
+ USER_KERBEROS_OPTIONS.put("renewTGT", "true");
+ String ticketCache = System.getenv("KRB5CCNAME");
+ if (ticketCache != null) {
+ USER_KERBEROS_OPTIONS.put("ticketCache", ticketCache);
+ }
+ }
+
+ private static final AppConfigurationEntry USER_KERBEROS_LOGIN =
+ new AppConfigurationEntry(Krb5LoginModule.class.getName(),
+ AppConfigurationEntry.LoginModuleControlFlag.OPTIONAL,
+ USER_KERBEROS_OPTIONS);
+
+ private static final AppConfigurationEntry[] USER_KERBEROS_CONF =
+ new AppConfigurationEntry[]{OS_SPECIFIC_LOGIN, USER_KERBEROS_LOGIN};
+
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String appName) {
+ return USER_KERBEROS_CONF;
+ }
+ }
+
+ static {
+ javax.security.auth.login.Configuration.setConfiguration(new KerberosConfiguration());
+ }
+
+ private URL url;
+ private HttpURLConnection conn;
+ private Base64 base64;
+
+ /**
+ * Performs SPNEGO authentication against the specified URL.
+ * <p/>
+ * If a token is given if does a NOP and returns the given token.
+ * <p/>
+ * If no token is given, it will perform the SPNEGO authentication sequency using a
+ * HTTP <code>OPTIONS</code> request.
+ *
+ * @param url the URl to authenticate against.
+ * @param token the authencation token being used for the user.
+ * @throws IOException if an IO error occurred.
+ * @throws AuthenticationException if an authentication error occurred.
+ */
+ @Override
+ public void authenticate(URL url, AuthenticatedURL.Token token)
+ throws IOException, AuthenticationException {
+ if (!token.isSet()) {
+ this.url = url;
+ base64 = new Base64(0);
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod(AUTH_HTTP_METHOD);
+ conn.connect();
+ if (isNegotiate()) {
+ doSpnegoSequence(token);
+ }
+ else {
+ new PseudoAuthenticator().authenticate(url, token);
+ }
+ }
+ }
+
+ /*
+ * Indicates if the response is starting a SPNEGO negotiation.
+ */
+ private boolean isNegotiate() throws IOException {
+ boolean negotiate = false;
+ if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ String authHeader = conn.getHeaderField(WWW_AUTHENTICATE);
+ negotiate = authHeader != null && authHeader.trim().startsWith(NEGOTIATE);
+ }
+ return negotiate;
+ }
+
+ /**
+ * Implements the SPNEGO authentication sequence interaction using the current default principal
+ * in the Kerberos cache (normally set via kinit).
+ *
+ * @param token the authencation token being used for the user.
+ * @throws IOException if an IO error occurred.
+ * @throws AuthenticationException if an authentication error occurred.
+ */
+ private void doSpnegoSequence(AuthenticatedURL.Token token) throws IOException, AuthenticationException {
+ try {
+ AccessControlContext context = AccessController.getContext();
+ Subject subject = Subject.getSubject(context);
+ if (subject == null) {
+ subject = new Subject();
+ LoginContext login = new LoginContext("", subject);
+ login.login();
+ }
+ Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
+
+ @Override
+ public Void run() throws Exception {
+ GSSContext gssContext = null;
+ try {
+ GSSManager gssManager = GSSManager.getInstance();
+ String servicePrincipal = "HTTP/" + KerberosAuthenticator.this.url.getHost();
+ GSSName serviceName = gssManager.createName(servicePrincipal,
+ GSSUtil.NT_GSS_KRB5_PRINCIPAL);
+ gssContext = gssManager.createContext(serviceName, GSSUtil.GSS_KRB5_MECH_OID, null,
+ GSSContext.DEFAULT_LIFETIME);
+ gssContext.requestCredDeleg(true);
+ gssContext.requestMutualAuth(true);
+
+ byte[] inToken = new byte[0];
+ byte[] outToken;
+ boolean established = false;
+
+ // Loop while the context is still not established
+ while (!established) {
+ outToken = gssContext.initSecContext(inToken, 0, inToken.length);
+ if (outToken != null) {
+ sendToken(outToken);
+ }
+
+ if (!gssContext.isEstablished()) {
+ inToken = readToken();
+ }
+ else {
+ established = true;
+ }
+ }
+ }
+ finally {
+ if (gssContext != null) {
+ gssContext.dispose();
+ }
+ }
+ return null;
+ }
+ });
+ }
+ catch (PrivilegedActionException ex) {
+ throw new AuthenticationException(ex.getException());
+ }
+ 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, AuthenticationException {
+ String token = base64.encodeToString(outToken);
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod(AUTH_HTTP_METHOD);
+ conn.setRequestProperty(AUTHORIZATION, NEGOTIATE + " " + token);
+ conn.connect();
+ }
+
+ /*
+ * retrieves the Kerberos token returned by the server.
+ */
+ private byte[] readToken() throws IOException, AuthenticationException {
+ int status = conn.getResponseCode();
+ if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ String authHeader = conn.getHeaderField(WWW_AUTHENTICATE);
+ if (authHeader == null || !authHeader.trim().startsWith(NEGOTIATE)) {
+ throw new AuthenticationException("Invalid SPNEGO sequence, '" + WWW_AUTHENTICATE +
+ "' header incorrect: " + authHeader);
+ }
+ String negotiation = authHeader.trim().substring((NEGOTIATE + " ").length()).trim();
+ return base64.decode(negotiation);
+ }
+ throw new AuthenticationException("Invalid SPNEGO sequence, status code: " + status);
+ }
+
+}
View
78 alfredo/src/main/java/com/cloudera/alfredo/client/PseudoAuthenticator.java
@@ -0,0 +1,78 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.client;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * The <code>PseudoAuthenticator</code> implementation provides an authentication equivalent to Hadoop
+ * Simple authentication, it trust the value of the 'user.name' Java System property.
+ * <p/>
+ * The 'user.name' value is propagated using an additional query string parameter {@link #USER_NAME} ('user.name').
+ *
+ */
+public class PseudoAuthenticator implements Authenticator {
+
+ /**
+ * Name of the additional parameter that carries the 'user.name' value.
+ */
+ public static final String USER_NAME = "user.name";
+
+ private static final String USER_NAME_EQ = USER_NAME + "=";
+
+ /**
+ * Performs simple authentication against the specified URL.
+ * <p/>
+ * If a token is given if does a NOP and returns the given token.
+ * <p/>
+ * If no token is given, it will perform a HTTP <code>OPTIONS</code> request injecting an additional
+ * parameter {@link #USER_NAME} in the query string with the value returned by the {@link #getUserName()}
+ * method.
+ * <p>
+ * If the response is successful it will update the authentication token.
+ *
+ * @param url the URl to authenticate against.
+ * @param token the authencation token being used for the user.
+ * @throws IOException if an IO error occurred.
+ * @throws AuthenticationException if an authentication error occurred.
+ */
+ @Override
+ public void authenticate(URL url, AuthenticatedURL.Token token) throws IOException, AuthenticationException {
+ String strUrl = url.toString();
+ String paramSeparator = (strUrl.contains("?")) ? "&" : "?";
+ strUrl += paramSeparator + USER_NAME_EQ + getUserName();
+ url = new URL(strUrl);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("OPTIONS");
+ conn.connect();
+ AuthenticatedURL.extractToken(conn, token);
+ }
+
+ /**
+ * Returns the current user name.
+ * <p/>
+ * This implementation returns the value of the Java system property 'user.name'
+ *
+ * @return the current user name.
+ */
+ protected String getUserName() {
+ return System.getProperty("user.name");
+ }
+}
View
346 alfredo/src/main/java/com/cloudera/alfredo/server/AuthenticationFilter.java
@@ -0,0 +1,346 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.server;
+
+import com.cloudera.alfredo.client.AuthenticatedURL;
+import com.cloudera.alfredo.client.AuthenticationException;
+import com.cloudera.alfredo.util.Signer;
+import com.cloudera.alfredo.util.SignerException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.Random;
+
+/**
+ * The <code>AuthenticationFilter</code> enabled protecting web application resources with different (pluggable)
+ * authentication mechanisms.
+ * <p/>
+ * Out of the box it provides 2 authentication mechanims: Pseudo and Kerberos SPNEGO.
+ * <p/>
+ * Additional authentication mechanisms are supported via the {@link AuthenticationHandler} interface.
+ * <p/>
+ * This filter delegates to the configured authentication handler for authentication and once it obtains an
+ * {@link AuthenticationToken} from it sets a signed HTTP Cookie with the token. For client requests
+ * that provide the signed HTTP Cookie, it verifies the validity of the cookie, extract the user information
+ * and let the request proceed to the target resource.
+ * <p/>
+ * The supported configuration properties are:
+ * <ul>
+ * <li>config.prefix: indicates the prefix to be used by all other configuration properties, the default value
+ * is no prefix. See below for details on how/why this prefix is used.</li>
+ * <li>#PREFIX#.authentication.type: simple|kerberos|#CLASS#, 'simple' is short for the
+ * {@link PseudoAuthenticationHandler}, 'kerberos' is short for {@link KerberosAuthenticationHandler}, otherwise
+ * the full class name of the {@link AuthenticationHandler} must be specified.</li>
+ * <li>#PREFIX#.signature.secret: the secret used to sign the HTTP Cookie value, the default value is a random
+ * value (unless multiple webapp instances need to share the secret the random value is adequate.</li>
+ * <li>auth.token.validity: validity -in seconds- of the generated token is valid before a
+ * new authentication is triggered, default value is <code>3600</code> seconds</li>
+ * </ul>
+ * <p/>
+ * The rest of the configuration properties are specific to the {@link AuthenticationHandler} implementation and the
+ * <code>AuthenticationFilter</code> will take all the properties that start with the prefix #PREFIX#, it will remove
+ * the prefix from it and it will pass them to the the authentication handler for initialization. Properties that do
+ * not start with the prefix will not be passed to the authentication handler initialization.
+ */
+public class AuthenticationFilter implements Filter {
+
+ private static Logger LOG = LoggerFactory.getLogger(AuthenticationFilter.class);
+
+ /**
+ * Constant for the property that specifies the configuration prefix.
+ */
+ public static final String CONFIG_PREFIX = "config.prefix";
+
+ /**
+ * Constant for the property that specifies the authentication handler to use.
+ */
+ public static final String AUTH_TYPE = "authentication.type";
+
+ /**
+ * Constant for the property that specifies the secret to use for signing the HTTP Cookies.
+ */
+ public static final String SIGNATURE_SECRET = "signature.secret";
+
+ /**
+ * Constant for the configuration property that indicates the validity of the generated token.
+ */
+ public static final String AUTH_TOKEN_VALIDITY = "auth.token.validity";
+
+ private Signer signer;
+ private AuthenticationHandler authHandler;
+ private boolean randomSecret;
+ private long validity;
+
+ /**
+ * Initializes the authentication filter.
+ * <p/>
+ * It instantiates and initializes the specified {@link AuthenticationHandler}.
+ * <p/>
+ *
+ * @param filterConfig filter configuration.
+ * @throws ServletException thrown if the filter or the authentication handler could not be initialized properly.
+ */
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ String configPrefix = filterConfig.getInitParameter(CONFIG_PREFIX);
+ configPrefix = (configPrefix != null) ? configPrefix + "." : "";
+ Properties config = getConfiguration(configPrefix, filterConfig);
+ String authHandlerName = config.getProperty(AUTH_TYPE, null);
+ String authHandlerClassName;
+ if (authHandlerName == null) {
+ throw new ServletException("Authentication type must be specified: simple|kerberos|<class>");
+ }
+ if (authHandlerName.equals("simple")) {
+ authHandlerClassName = PseudoAuthenticationHandler.class.getName();
+ }
+ else if (authHandlerName.equals("kerberos")) {
+ authHandlerClassName = KerberosAuthenticationHandler.class.getName();
+ }
+ else {
+ authHandlerClassName = authHandlerName;
+ }
+
+ try {
+ Class klass = Thread.currentThread().getContextClassLoader().loadClass(authHandlerClassName);
+ authHandler = (AuthenticationHandler) klass.newInstance();
+ authHandler.init(config);
+ }
+ catch (ClassNotFoundException ex) {
+ throw new ServletException(ex);
+ }
+ catch (InstantiationException ex) {
+ throw new ServletException(ex);
+ }
+ catch (IllegalAccessException ex) {
+ throw new ServletException(ex);
+ }
+ String signatureSecret = config.getProperty(configPrefix + SIGNATURE_SECRET);
+ if (signatureSecret == null) {
+ signatureSecret = Long.toString(new Random(System.currentTimeMillis()).nextLong());
+ randomSecret = true;
+ LOG.warn("'signature.secret' configuration not set, using a random value as secret");
+ }
+ signer = new Signer(signatureSecret.getBytes());
+ validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, "3600")) * 1000; //10 hours
+ }
+
+ /**
+ * Returns the authentication handler being used.
+ *
+ * @return the authentication handler being used.
+ */
+ protected AuthenticationHandler getAuthenticationHandler() {
+ return authHandler;
+ }
+
+ /**
+ * Returns if a random secret is being used.
+ *
+ * @return if a random secret is being used.
+ */
+ protected boolean isRandomSecret() {
+ return randomSecret;
+ }
+
+ /**
+ * Returns the validity of the generated tokens.
+ *
+ * @return the validity of the generated tokens, in seconds.
+ */
+ protected long getValidity() {
+ return validity / 1000;
+ }
+
+ /**
+ * Destroys the filter.
+ * <p/>
+ * It invokes the {@link AuthenticationHandler#destroy()} method to release any resources it may hold.
+ */
+ @Override
+ public void destroy
+ () {
+ if (authHandler != null) {
+ authHandler.destroy();
+ authHandler = null;
+ }
+ }
+
+ /**
+ * Returns the filtered configuration (only properties starting with the specified prefix). The property keys
+ * are also trimmed from the prefix. The returned <code>Properties</code> object is used to initialized the
+ * {@link AuthenticationHandler}.
+ * <p/>
+ * This method can be overriden by subclasses to obtain the configuration from other configuration source than
+ * the web.xml file.
+ *
+ * @param configPrefix configuration prefix to use for extracting configuration properties.
+ * @param filterConfig filter configuration object
+ * @return the configuration to be used with the {@link AuthenticationHandler} instance.
+ *
+ * @throws ServletException thrown if the configuration could not be created.
+ */
+ protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException {
+ Properties props = new Properties();
+ Enumeration names = filterConfig.getInitParameterNames();
+ while (names.hasMoreElements()) {
+ String name = (String) names.nextElement();
+ if (name.startsWith(configPrefix)) {
+ String value = filterConfig.getInitParameter(name);
+ props.put(name.substring(configPrefix.length()), value);
+ }
+ }
+ return props;
+ }
+
+ /**
+ * Returns the full URL of the request including the query string.
+ * <p/>
+ * Used as a convenience method for logging purposes.
+ *
+ * @param request the request object.
+ * @return the full URL of the request including the query string.
+ */
+ protected String getRequestURL(HttpServletRequest request) {
+ StringBuffer sb = request.getRequestURL();
+ if (request.getQueryString() != null) {
+ sb.append("?").append(request.getQueryString());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns the {@link AuthenticationToken} for the request.
+ * <p/>
+ * It looks a the received HTTP Cookies and extracts the value of the {@link AuthenticatedURL#AUTH_COOKIE}
+ * if present. It verifies the signature and if correct it creates the {@link AuthenticationToken} and returns
+ * it.
+ * <p/>
+ * If this method returns <code>null</code> the filter will invoke the configured {@link AuthenticationHandler}
+ * to perform user authentication.
+ *
+ * @param request request object.
+ * @return the Authentication token if the request is authentiated, <code>null</code> otherwise.
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if the token is invalid/tampered or if it has expired.
+ */
+ protected AuthenticationToken getToken(HttpServletRequest request) throws IOException, AuthenticationException {
+ AuthenticationToken token = null;
+ String tokenStr = null;
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(AuthenticatedURL.AUTH_COOKIE)) {
+ tokenStr = cookie.getValue();
+ try {
+ tokenStr = signer.verifyAndExtract(tokenStr);
+ }
+ catch (SignerException ex) {
+ throw new AuthenticationException(ex);
+ }
+ break;
+ }
+ }
+ }
+ if (tokenStr != null) {
+ token = AuthenticationToken.parse(tokenStr);
+ if (token.isExpired()) {
+ throw new AuthenticationException("AuthenticationToken expired");
+ }
+ }
+ return token;
+ }
+
+ /**
+ * If the request has a valid authentication token it allows the request to continue to the target resource,
+ * otherwise it triggers an authentication sequence using the configured {@link AuthenticationHandler}.
+ *
+ * @param request the request object.
+ * @param response the response object.
+ * @param filterChain the filter chain object.
+ * @throws IOException thrown if an IO error occurred.
+ * @throws ServletException thrown if a processing error occurred.
+ */
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
+ throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ try {
+ boolean newToken = false;
+ AuthenticationToken token = getToken(httpRequest);
+ if (token == null) {
+ LOG.debug("Request [{}] triggering authentication", getRequestURL(httpRequest));
+ token = authHandler.authenticate(httpRequest, httpResponse);
+ if (token != null && token != AuthenticationToken.ANNONYMOUS) {
+ token.setExpires(System.currentTimeMillis() + validity);
+ }
+ newToken = true;
+ }
+ if (token != null) {
+ LOG.debug("Request [{}] user [{}] authenticated", getRequestURL(httpRequest), token.getUserName());
+ final AuthenticationToken authToken = token;
+ httpRequest = new HttpServletRequestWrapper(httpRequest) {
+
+ @Override
+ public String getAuthType() {
+ return authToken.getType();
+ }
+
+ @Override
+ public String getRemoteUser() {
+ return authToken.getUserName();
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return (authToken != AuthenticationToken.ANNONYMOUS) ? authToken : null;
+ }
+ };
+ if (newToken && token != AuthenticationToken.ANNONYMOUS) {
+ String signedToken = signer.sign(token.toString());
+ httpResponse.addCookie(new Cookie(AuthenticatedURL.AUTH_COOKIE, signedToken));
+ }
+ filterChain.doFilter(httpRequest, httpResponse);
+ }
+ }
+ catch (AuthenticationException ex) {
+ if (!httpResponse.isCommitted()) {
+ Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, "");
+ cookie.setMaxAge(0);
+ httpResponse.addCookie(cookie);
+ httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage());
+ }
+ LOG.warn("Authentication exception: " + ex.getMessage());
+ }
+ }
+
+}
View
81 alfredo/src/main/java/com/cloudera/alfredo/server/AuthenticationHandler.java
@@ -0,0 +1,81 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.server;
+
+import com.cloudera.alfredo.client.AuthenticationException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Interface for server authentication mechanisms.
+ * <p/>
+ * The {@link AuthenticationFilter} manages the lifecycle of the authentication handler.
+ * <p/>
+ * Implementations must be thread-safe as once instance is initialized and used for all requests.
+ */
+public interface AuthenticationHandler {
+
+ /**
+ * Initializes the authentication handler instance.
+ * <p/>
+ * This method is invoked by the {@link AuthenticationFilter#init} method.
+ *
+ * @param config configuration properties to initialize the handler.
+ *
+ * @throws ServletException thrown if the handler could not be initialized.
+ */
+ public void init(Properties config) throws ServletException;
+
+ /**
+ * Destroys the authentication handler instance.
+ * <p/>
+ * This method is invoked by the {@link AuthenticationFilter#destroy} method.
+ */
+ public void destroy();
+
+ /**
+ * Performs an authentication step for the given HTTP client request.
+ * <p/>
+ * This method is invoked by the {@link AuthenticationFilter} only if the HTTP client request is
+ * not yet authenticated.
+ * <p/>
+ * Depending on on the authentication mechanism being implemented, a particular HTTP client may
+ * end up making a sequence of invocations before authentication is successfully established (this is
+ * the case of Kerberos SPNEGO).
+ * <p/>
+ * This method must return an {@link AuthenticationToken} only if the the HTTP client request has
+ * been successfully and fully authenticated.
+ * <p/>
+ * If the HTTP client request has not been completly authenticated, this method must take over
+ * the corresponding HTTP response and it must return <code>null</code>.
+ *
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ * @return an {@link AuthenticationToken} if the HTTP client request has been authenticated,
+ * <code>null</code> otherwise (in this case it must take care of the response).
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if an Authentication error occurred.
+ */
+ public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, AuthenticationException;
+
+}
View
220 alfredo/src/main/java/com/cloudera/alfredo/server/AuthenticationToken.java
@@ -0,0 +1,220 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.server;
+
+import com.cloudera.alfredo.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;
+
+/**
+ * The <code>AuthenticationToken</code> contains information about an authenticated HTTP client and doubles
+ * as the <code>Principal</code> to be returned by authenticated <code>HttpServletRequest</code>s
+ * <p/>
+ * The token can be serialized/deserialized to and from string and it is send and received in HTTP client
+ * responses and requests as a HTTP cookie (this is done by the {@link AuthenticationFilter}).
+ */
+public class AuthenticationToken implements Principal {
+
+ /**
+ * Constant that identifies an annonymous request.
+ */
+ public static final AuthenticationToken ANNONYMOUS = 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<String> ATTRIBUTES =
+ new HashSet<String>(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 = "ANNONYMOUS";
+ generateToken();
+ }
+
+ private static final String ILLEGAL_ARG_MSG = " 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.
+ * (<code>System.currentTimeMillis() + validityPeriod</code>).
+ */
+ public AuthenticationToken(String userName, String principal, String type) {
+ if (userName == null || userName.length() == 0 || userName.contains(ATTR_SEPARATOR)) {
+ throw new IllegalArgumentException("userName" + ILLEGAL_ARG_MSG);
+ }
+ if (principal == null || principal.length() == 0 || principal.contains(ATTR_SEPARATOR)) {
+ throw new IllegalArgumentException("principal" + ILLEGAL_ARG_MSG);
+ }
+ if (type == null || type.length() == 0 || type.contains(ATTR_SEPARATOR)) {
+ throw new IllegalArgumentException("type" + ILLEGAL_ARG_MSG);
+ }
+ this.userName = userName;
+ this.principal = principal;
+ this.type = type;
+ this.expires = -1;
+ }
+
+ /**
+ * Sets the expiration of the token.
+ *
+ * @param expires expiration time of the token in milliseconds since Epoc.
+ */
+ public void setExpires(long expires) {
+ if (this != AuthenticationToken.ANNONYMOUS) {
+ this.expires = expires;
+ generateToken();
+ }
+ }
+
+ /**
+ * Generates the token.
+ */
+ private void generateToken() {
+ StringBuffer sb = new StringBuffer();
+ sb.append(USER_NAME).append("=").append(userName).append(ATTR_SEPARATOR);
+ sb.append(PRINCIPAL).append("=").append(principal).append(ATTR_SEPARATOR);
+ sb.append(TYPE).append("=").append(type).append(ATTR_SEPARATOR);
+ sb.append(EXPIRES).append("=").append(expires);
+ token = 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 <code>Principal</code> 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.
+ */
+ public boolean isExpired() {
+ return expires != -1 && System.currentTimeMillis() > expires;
+ }
+
+ /**
+ * Returns the string representation of the token.
+ * <p/>
+ * This string representation is parseable by the {@link #parse} method.
+ *
+ * @return the string representation of the token.
+ */
+ @Override
+ public String toString() {
+ return token;
+ }
+
+ /**
+ * Parses a string into an authentication token.
+ *
+ * @param tokenStr string representation of a token.
+ * @return the parsed authentication token.
+ * @throws AuthenticationException thrown if the string representation could not be parsed into
+ * an authentication token.
+ */
+ public static AuthenticationToken parse(String tokenStr) throws AuthenticationException {
+ Map<String, String> 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;
+ }
+
+ /**
+ * 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<String, String> split(String tokenStr) throws AuthenticationException {
+ Map<String, String> map = new HashMap<String, String>();
+ 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;
+ }
+
+}
View
287 alfredo/src/main/java/com/cloudera/alfredo/server/KerberosAuthenticationHandler.java
@@ -0,0 +1,287 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.server;
+
+import com.cloudera.alfredo.client.AuthenticationException;
+import com.cloudera.alfredo.client.KerberosAuthenticator;
+import com.sun.security.auth.module.Krb5LoginModule;
+import org.apache.commons.codec.binary.Base64;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * The <code>KerberosAuthenticationHandler</code> implements the Kerberos SPNEGO authentication mechanims for HTTP.
+ * <p/>
+ * The supported configuration properties are:
+ * <ul>
+ * <li>kerberos.principal: the Kerberos principal to used by the server. As stated by the Kerberos SPNEGO
+ * specification, it should be <code>HTTP/${HOSTNAME}@{REALM}</code>. The realm can be ommitted from the
+ * principal as the JDK GSS libraries will use the realm name of the configured KDC.
+ * It does no have default value.</li>
+ * <li>kerberos.keytab: the keytab file containing the credentials for the kerberos principal.
+ * It does not have default value.</li>
+ * </ul>
+ */
+public class KerberosAuthenticationHandler implements AuthenticationHandler {
+ private static Logger LOG = LoggerFactory.getLogger(KerberosAuthenticationHandler.class);
+
+ /**
+ * Kerberos context configuration for the JDK GSS library.
+ */
+ private static class KerberosConfiguration extends Configuration {
+ private String keytab;
+ private String principal;
+
+ public KerberosConfiguration(String keytab, String principal) {
+ this.keytab = keytab;
+ this.principal = principal;
+ }
+
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+ Map<String, String> options = new HashMap<String, String>();
+ options.put("keyTab", keytab);
+ options.put("principal", principal);
+ options.put("useKeyTab", "true");
+ options.put("storeKey", "true");
+ options.put("doNotPrompt", "true");
+ options.put("useTicketCache", "true");
+ options.put("renewTGT", "true");
+ options.put("refreshKrb5Config", "true");
+ options.put("isInitiator", "false");
+ String ticketCache = System.getenv("KRB5CCNAME");
+ if (ticketCache != null) {
+ options.put("ticketCache", ticketCache);
+ }
+ if (LOG.isDebugEnabled()) {
+ options.put("debug", "true");
+ }
+
+ return new AppConfigurationEntry[]{
+ new AppConfigurationEntry(Krb5LoginModule.class.getName(),
+ AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+ options),};
+ }
+ }
+
+ /**
+ * Constant that identifies the authentication mechanism.
+ */
+ public static final String TYPE = "kerberos";
+
+ /**
+ * Constant for the configuration property that indicates the kerberos principal.
+ */
+ public static final String PRINCIPAL = "kerberos.principal";
+
+ /**
+ * Constant for the configuration property that indicates the keytab file path.
+ */
+ public static final String KEYTAB = "kerberos.keytab";
+
+ private String principal;
+ private String keytab;
+ private LoginContext loginContext;
+
+ /**
+ * Initializes the authentication handler instance.
+ * <p/>
+ * It creates a Kerberos context using the principal and keytab specified in the configuration.
+ * <p/>
+ * This method is invoked by the {@link AuthenticationFilter#init} method.
+ *
+ * @param config configuration properties to initialize the handler.
+ *
+ * @throws ServletException thrown if the handler could not be initialized.
+ */
+ @Override
+ public void init(Properties config) throws ServletException {
+ try {
+ principal = config.getProperty(PRINCIPAL, principal);
+ if (principal == null || principal.trim().length() == 0) {
+ throw new ServletException("Principal not defined in configuration");
+ }
+ keytab = config.getProperty(KEYTAB, keytab);
+ if (keytab == null || keytab.trim().length() == 0) {
+ throw new ServletException("Keytab not defined in configuration");
+ }
+ if (!new File(keytab).exists()) {
+ throw new ServletException("Keytab does not exist: " + keytab);
+ }
+
+ Set<Principal> principals = new HashSet<Principal>();
+ principals.add(new KerberosPrincipal(principal));
+ Subject subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>());
+
+ KerberosConfiguration kerberosConfiguration = new KerberosConfiguration(keytab, principal);
+
+ loginContext = new LoginContext("", subject, null, kerberosConfiguration);
+ loginContext.login();
+
+ LOG.info("Initialized, principal [{}] from keytab [{}]", principal, keytab);
+ }
+ catch (Exception ex) {
+ throw new ServletException(ex);
+ }
+ }
+
+ /**
+ * Releases any resources initialized by the authentication handler.
+ * <p/>
+ * It destroys the Kerberos context.
+ */
+ @Override
+ public void destroy() {
+ try {
+ if (loginContext != null) {
+ loginContext.logout();
+ }
+ }
+ catch (LoginException ex) {
+ LOG.warn(ex.getMessage(), ex);
+ }
+ }
+
+ /**
+ * Returns the Kerberos principal used by the authentication handler.
+ *
+ * @return the Kerberos principal used by the authentication handler.
+ */
+ protected String getPrincipal() {
+ return principal;
+ }
+
+ /**
+ * Returns the keytab used by the authentication handler.
+ *
+ * @return the keytab used by the authentication handler.
+ */
+ protected String getKeytab() {
+ return keytab;
+ }
+
+ /**
+ * It enforces the the Kerberos SPNEGO authentication sequence returning an {@link AuthenticationToken} only
+ * after the Kerberos SPNEGO sequence completed successfully.
+ * <p/>
+ *
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ * @return an authentication token if the Kerberos SPNEGO sequence is complete and valid,
+ * <code>null</code> if is in progress (in this case the handler handles the response to the client).
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if Kerberos SPNEGO sequence failed.
+ */
+ @Override
+ public AuthenticationToken authenticate(HttpServletRequest request, final HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ AuthenticationToken token = null;
+ String authorization = request.getHeader(KerberosAuthenticator.AUTHORIZATION);
+
+ if (authorization == null) {
+ response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE);
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ LOG.trace("SPNEGO starts");
+ }
+ else if (!authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) {
+ response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE);
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ LOG.warn("'" + KerberosAuthenticator.AUTHORIZATION + "' does not start with '" +
+ KerberosAuthenticator.NEGOTIATE + "' : {}", authorization);
+ }
+ else {
+ authorization = authorization.substring(KerberosAuthenticator.NEGOTIATE.length()).trim();
+ final Base64 base64 = new Base64(0);
+ final byte[] clientToken = base64.decode(authorization);
+ Subject serverSubject = loginContext.getSubject();
+ try {
+ token = Subject.doAs(serverSubject, new PrivilegedExceptionAction<AuthenticationToken>() {
+
+ @Override
+ public AuthenticationToken run() throws Exception {
+ AuthenticationToken token = null;
+ GSSContext gssContext = null;
+ try {
+ gssContext = GSSManager.getInstance().createContext((GSSCredential) null);
+ byte[] serverToken = gssContext.acceptSecContext(clientToken, 0, clientToken.length);
+ if (serverToken != null && serverToken.length > 0) {
+ String authenticate = base64.encodeToString(serverToken);
+ response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE,
+ KerberosAuthenticator.NEGOTIATE + " " + authenticate);
+ }
+ if (!gssContext.isEstablished()) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ LOG.trace("SPNEGO in progress");
+ }
+ else {
+ String clientPrincipal = gssContext.getSrcName().toString();
+ int index = clientPrincipal.indexOf("/");
+ if (index == -1) {
+ index = clientPrincipal.indexOf("@");
+ }
+ String userName = (index == -1) ? clientPrincipal : clientPrincipal.substring(0, index);
+ token = new AuthenticationToken(userName, clientPrincipal, TYPE);
+ response.setStatus(HttpServletResponse.SC_OK);
+ LOG.trace("SPNEGO completed for principal [{}]", clientPrincipal);
+ }
+ }
+ finally {
+ if (gssContext != null) {
+ gssContext.dispose();
+ }
+ }
+ return token;
+ }
+ });
+ }
+ catch (PrivilegedActionException ex) {
+ if (ex.getException() instanceof IOException) {
+ throw (IOException) ex.getException();
+ }
+ else {
+ throw new AuthenticationException(ex.getException());
+ }
+ }
+ }
+ return token;
+ }
+
+}
View
127 alfredo/src/main/java/com/cloudera/alfredo/server/PseudoAuthenticationHandler.java
@@ -0,0 +1,127 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.server;
+
+import com.cloudera.alfredo.client.AuthenticationException;
+import com.cloudera.alfredo.client.PseudoAuthenticator;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * The <code>PseudoAuthenticationHandler</code> provides a pseudo authentication mechanism that accepts
+ * the user name specified as a query string parameter.
+ * <p/>
+ * This mimics the model of Hadoop Simple authentication which trust the 'user.name' property provided in
+ * the configuration object.
+ * <p/>
+ * This handler can be configured to support anonymous users.
+ * <p/>
+ * The supported configuration property is:
+ * <ul>
+ * <li>annonymous.allowed: <code>true|false</code>, default value is <code>false</code></li>
+ * </ul>
+ */
+public class PseudoAuthenticationHandler implements AuthenticationHandler {
+
+ /**
+ * Constant that identifies the authentication mechanism.
+ */
+ public static final String TYPE = "simple";
+
+ /**
+ * Constant for the configuration property that indicates if annonymous users are allowed.
+ */
+ public static final String ANNONYMOUS_ALLOWED = "annonymous.allowed";
+
+ private boolean acceptAnnonymous;
+
+ /**
+ * Initializes the authentication handler instance.
+ * <p/>
+ * This method is invoked by the {@link AuthenticationFilter#init} method.
+ *
+ * @param config configuration properties to initialize the handler.
+ *
+ * @throws ServletException thrown if the handler could not be initialized.
+ */
+ @Override
+ public void init(Properties config) throws ServletException {
+ acceptAnnonymous = Boolean.parseBoolean(config.getProperty(ANNONYMOUS_ALLOWED, "false"));
+ }
+
+ /**
+ * Returns if the handler is configured to support annonymous users.
+ *
+ * @return if the handler is configured to support annonymous users.
+ */
+ protected boolean getAcceptAnnonymous() {
+ return acceptAnnonymous;
+ }
+
+ /**
+ * Releases any resources initialized by the authentication handler.
+ * <p/>
+ * This implementation does a NOP.
+ */
+ @Override
+ public void destroy() {
+ }
+
+ /**
+ * Authenticates an HTTP client request.
+ * <p/>
+ * It extract the {@link PseudoAuthenticator#USER_NAME} parameter from the query string and creates
+ * and {@link AuthenticationToken} with it.
+ * <p/>
+ * If the HTTP client request does not contain the {@link PseudoAuthenticator#USER_NAME} parameter and
+ * the handler is configured to allow anonymous users it returns the {@link AuthenticationToken#ANNONYMOUS}
+ * token.
+ * <p/>
+ * If the HTTP client request does not contain the {@link PseudoAuthenticator#USER_NAME} parameter and
+ * the handler is configured to disallow anonymous users it thows an {@link AuthenticationException}.
+ *
+ * @param request the HTTP client request.
+ * @param response the HTTP client response.
+ * @return an authentication token if the HTTP client request is accepted and credentials are valid.
+ * @throws IOException thrown if an IO error occurred.
+ * @throws AuthenticationException thrown if HTTP client request was not accepted as an authentication request.
+ */
+ @Override
+ public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, AuthenticationException {
+ AuthenticationToken token;
+ String userName = request.getParameter(PseudoAuthenticator.USER_NAME);
+ if (userName == null) {
+ if (getAcceptAnnonymous()) {
+ token = AuthenticationToken.ANNONYMOUS;
+ }
+ else {
+ throw new AuthenticationException("Annonymous requests are disallowed");
+ }
+ }
+ else {
+ token = new AuthenticationToken(userName, userName, TYPE);
+ }
+ return token;
+ }
+
+}
View
102 alfredo/src/main/java/com/cloudera/alfredo/util/Signer.java
@@ -0,0 +1,102 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.util;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Signs strings and verifies signed strings using a SHA digest.
+ */
+public class Signer {
+ private static final String SIGNATURE = ",s=";
+
+ private byte[] secret;
+
+ /**
+ * Creates a Signer instance using the specified secret.
+ *
+ * @param secret secret to use for creating the digest.
+ */
+ public Signer(byte[] secret) {
+ if (secret == null) {
+ throw new IllegalArgumentException("secret cannot be NULL");
+ }
+ this.secret = secret.clone();
+ }
+
+ /**
+ * Returns a signed string.
+ * <p/>
+ * The signature ',s=SIGNATURE' is appended at the end of the string.
+ *
+ * @param str string to sign.
+ * @return the signed string.
+ */
+ public String sign(String str) {
+ if (str == null || str.length() == 0) {
+ throw new IllegalArgumentException("NULL or empty string to sign");
+ }
+ String signature = computeSignature(str);
+ return str + SIGNATURE + signature;
+ }
+
+ /**
+ * Verifies a signed string and extracts the original string.
+ *
+ * @param signedStr the signed string to verify and extract.
+ * @return the extracted original string.
+ * @throws SignerException thrown if the given string is not a signed string or if the signature is invalid.
+ */
+ public String verifyAndExtract(String signedStr) throws SignerException {
+ int index = signedStr.lastIndexOf(SIGNATURE);
+ if (index == -1) {
+ throw new SignerException("Invalid signed text: " + signedStr);
+ }
+ String originalSignature = signedStr.substring(index + SIGNATURE.length());
+ String rawValue = signedStr.substring(0, index);
+ String currentSignature = computeSignature(rawValue);
+ if (!originalSignature.equals(currentSignature)) {
+ throw new SignerException("Invalid signature");
+ }
+ return rawValue;
+ }
+
+ /**
+ * Returns then signature of a string.
+ *
+ * @param str string to sign.
+ * @return the signature for the string.
+ */
+ protected String computeSignature(String str) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA");
+ md.update(str.getBytes());
+ md.update(secret);
+ byte[] digest = md.digest();
+ Base64 base64 = new Base64(0);
+ return base64.encodeToString(digest);
+ }
+ catch (NoSuchAlgorithmException ex) {
+ throw new RuntimeException("It should not happen, " + ex.getMessage(), ex);
+ }
+ }
+
+}
View
33 alfredo/src/main/java/com/cloudera/alfredo/util/SignerException.java
@@ -0,0 +1,33 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Cloudera, Inc. licenses this file
+ * to you 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.
+ */
+package com.cloudera.alfredo.util;
+
+/**
+ * Exception thrown by {@link Signer} when a string signature is invalid.
+ */
+public class SignerException extends Exception {
+
+ /**
+ * Creates an exception instance.
+ *
+ * @param msg message for the exception.
+ */
+ public SignerException(String msg) {
+ super(msg);
+ }
+}
View
148 alfredo/src/site/apt/Configuration.apt
@@ -0,0 +1,148 @@
+~~ Licensed to Cloudera, Inc. under one
+~~ or more contributor license agreements. See the NOTICE file
+~~ distributed with this work for additional information
+~~ regarding copyright ownership. Cloudera, Inc. licenses this file
+~~ to you 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.
+
+ ---
+ Alfredo, Java HTTP SPNEGO - Configuration
+ ---
+ Alejandro Abdelnur
+ ---
+ 17 January 2011
+
+Alfredo, Java HTTP SPNEGO - Configuration
+
+* Server Side Configuration Setup
+
+ The {{{apidocs/com/cloudera/alfredo/server/AuthenticationFilter.html}AuthenticationFilter filter}} is
+ Alfredo's server side component.
+
+ This filter must be configured in front of all the web application resources that required authenticated requests.
+ For example:
+
+ The Alfredo and dependent JAR files must be in the web application classpath (commonly the <<<WEB-INF/lib>>>
+ directory).
+
+ Alfredo uses SLF4J-API for logging. Alfredo Maven POM dependencies define the SLF4J API dependency but it
+ does not define the dependency on a concrete logging implementation, this must be addded explicitly to the
+ web application. For example, if the web applicationan uses Log4j, the SLF4J-LOG4J12 and LOG4J jar files must
+ be part part of the web application classpath as well as the Log4j configuration file.
+
+** Common Configuration parameters
+
+ * <<<config.prefix>>>: If specified, all other configuration parameter names must start with the prefix.
+ The default value is no prefix.
+
+ * <<<[PREFIX.]authentication.type>>>: the authentication type keyword (<<<simple>> or <<<kerberos>>>) or a
+ {{{apidocs/com/cloudera/alfredo/server/AuthenticationHandler.html}Authentication handler implementation}}.
+
+ * <<<[PREFIX.]signature.secret>>>: The secret to SHA-sign the generated authentication tokens. If a secret is
+ not provided a random secret is generated at start up time. If using multiple web application instances
+ behind a load-balancer a secret must be set for the application to work properly.
+
+ * <<<[PREFIX.]auth.token.validity>>>: The validity -in seconds- of the generated authentication token. The
+ default value is <<<3600>>> seconds.
+
+** Kerberos Configuration
+
+ <<IMPORTANT>>: A KDC must be configured and running.
+
+ To use Kerberos SPNEGO as the authentication mechanism, the authentication filter must be configured with the
+ following init parameters:
+
+ * <<<[PREFIX.]authentication.type>>>: the keyword <<<kerberos>>>.
+
+ * <<<[PREFIX.]kerberos.principal>>>: The web-application Kerberos principal name. The Kerberos principal name
+ must start with <<<HTTP/...>>>. For example: <<<HTTP/localhost@LOCALHOST>>>. There is no default value.
+
+ * <<<[PREFIX.]kerberos.keytab>>>: The path to the keytab file containing the credentials for the kerberos
+ principal. For example: <<</Users/tucu/alfredo.keytab>>>. There is no default value.
+
+ *<<Example>>:
+
++---+
+<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee">
+ ...
+
+ <filter>
+ <filter-name>kerberosFilter</filter-name>
+ <filter-class>com.cloudera.alfredo.server.AuthenticationFilter</filter-class>
+ <init-param>
+ <param-name>authentication.type</param-name>
+ <param-value>kerberos</param-value>
+ </init-param>
+ <init-param>
+ <param-name>kerberos.principal</param-name>
+ <param-value>HTTP/localhost@LOCALHOST</param-value>
+ </init-param>
+ <init-param>
+ <param-name>kerberos.keytab</param-name>
+ <param-value>/tmp/alfredo.keytab</param-value>
+ </init-param>
+ <init-param>
+ <param-name>auth.token.validity</param-name>
+ <param-value>30</param-value>
+ </init-param>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>kerberosFilter</filter-name>
+ <url-pattern>/kerberos/*</url-pattern>
+ </filter-mapping>
+
+ ...
+</web-app>
++---+
+
+** Pseudo/Simple Configuration
+
+ To use Pseudo/Simple as the authentication mechanism (trusting the value of the query string parameter
+ 'user.name'), the authentication filter must be configured with the following init parameters:
+
+ * <<<[PREFIX.]authentication.type>>>: the keyword <<<simple>>>.