You are viewing a plain text version of this content. The canonical link for it is here.
Posted to common-commits@hadoop.apache.org by zj...@apache.org on 2015/04/10 06:25:02 UTC

[06/47] hadoop git commit: HADOOP-11717. Support JWT tokens for web single sign on to the Hadoop servers. (Larry McCay via omalley)

HADOOP-11717. Support JWT tokens for web single sign on to the Hadoop
servers. (Larry McCay via omalley)


Project: http://git-wip-us.apache.org/repos/asf/hadoop/repo
Commit: http://git-wip-us.apache.org/repos/asf/hadoop/commit/7b1be9e2
Tree: http://git-wip-us.apache.org/repos/asf/hadoop/tree/7b1be9e2
Diff: http://git-wip-us.apache.org/repos/asf/hadoop/diff/7b1be9e2

Branch: refs/heads/YARN-2928
Commit: 7b1be9e2e55a817c02cb80ba544882e0486215bb
Parents: 2073197
Author: Owen O'Malley <om...@apache.org>
Authored: Tue Apr 7 08:09:41 2015 -0700
Committer: Zhijie Shen <zj...@apache.org>
Committed: Thu Apr 9 20:55:56 2015 -0700

----------------------------------------------------------------------
 hadoop-common-project/hadoop-auth/pom.xml       |  11 +
 .../JWTRedirectAuthenticationHandler.java       | 363 ++++++++++++++++
 .../authentication/util/CertificateUtil.java    |  65 +++
 .../TestJWTRedirectAuthentictionHandler.java    | 418 +++++++++++++++++++
 .../util/TestCertificateUtil.java               |  96 +++++
 hadoop-common-project/hadoop-common/CHANGES.txt |   3 +
 hadoop-project/pom.xml                          |  13 +
 7 files changed, 969 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/hadoop/blob/7b1be9e2/hadoop-common-project/hadoop-auth/pom.xml
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-auth/pom.xml b/hadoop-common-project/hadoop-auth/pom.xml
index 5f7d774..3999d5a 100644
--- a/hadoop-common-project/hadoop-auth/pom.xml
+++ b/hadoop-common-project/hadoop-auth/pom.xml
@@ -108,6 +108,17 @@
       <scope>compile</scope>
     </dependency>
     <dependency>
+      <groupId>com.nimbusds</groupId>
+      <artifactId>nimbus-jose-jwt</artifactId>
+      <scope>compile</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk15on</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
       <groupId>org.apache.directory.server</groupId>
       <artifactId>apacheds-kerberos-codec</artifactId>
       <scope>compile</scope>

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7b1be9e2/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/JWTRedirectAuthenticationHandler.java
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/JWTRedirectAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/JWTRedirectAuthenticationHandler.java
new file mode 100644
index 0000000..42df6a0
--- /dev/null
+++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/JWTRedirectAuthenticationHandler.java
@@ -0,0 +1,363 @@
+/**
+ * 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 javax.servlet.http.Cookie;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Properties;
+import java.text.ParseException;
+
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.PublicKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.cert.CertificateException;
+import java.security.interfaces.RSAPublicKey;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.security.authentication.server.AltKerberosAuthenticationHandler;
+import org.apache.hadoop.security.authentication.server.AuthenticationToken;
+import org.apache.hadoop.security.authentication.util.CertificateUtil;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+
+/**
+ * The {@link JWTRedirectAuthenticationHandler} extends
+ * AltKerberosAuthenticationHandler to add WebSSO behavior for UIs. The expected
+ * SSO token is a JsonWebToken (JWT). The supported algorithm is RS256 which
+ * uses PKI between the token issuer and consumer. The flow requires a redirect
+ * to a configured authentication server URL and a subsequent request with the
+ * expected JWT token. This token is cryptographically verified and validated.
+ * The user identity is then extracted from the token and used to create an
+ * AuthenticationToken - as expected by the AuthenticationFilter.
+ *
+ * <p/>
+ * The supported configuration properties are:
+ * <ul>
+ * <li>authentication.provider.url: the full URL to the authentication server.
+ * This is the URL that the handler will redirect the browser to in order to
+ * authenticate the user. It does not have a default value.</li>
+ * <li>public.key.pem: This is the PEM formatted public key of the issuer of the
+ * JWT token. It is required for verifying that the issuer is a trusted party.
+ * DO NOT include the PEM header and footer portions of the PEM encoded
+ * certificate. It does not have a default value.</li>
+ * <li>expected.jwt.audiences: This is a list of strings that identify
+ * acceptable audiences for the JWT token. The audience is a way for the issuer
+ * to indicate what entity/s that the token is intended for. Default value is
+ * null which indicates that all audiences will be accepted.</li>
+ * <li>jwt.cookie.name: the name of the cookie that contains the JWT token.
+ * Default value is "hadoop-jwt".</li>
+ * </ul>
+ */
+public class JWTRedirectAuthenticationHandler extends
+    AltKerberosAuthenticationHandler {
+  private static Logger LOG = LoggerFactory
+      .getLogger(JWTRedirectAuthenticationHandler.class);
+
+  public static final String AUTHENTICATION_PROVIDER_URL = "authentication.provider.url";
+  public static final String PUBLIC_KEY_PEM = "public.key.pem";
+  public static final String EXPECTED_JWT_AUDIENCES = "expected.jwt.audiences";
+  public static final String JWT_COOKIE_NAME = "jwt.cookie.name";
+  private static final String ORIGINAL_URL_QUERY_PARAM = "originalUrl=";
+  private String authenticationProviderUrl = null;
+  private RSAPublicKey publicKey = null;
+  private List<String> audiences = null;
+  private String cookieName = "hadoop-jwt";
+
+  /**
+   * Primarily for testing, this provides a way to set the publicKey for
+   * signature verification without needing to get a PEM encoded value.
+   *
+   * @param pk
+   */
+  public void setPublicKey(RSAPublicKey pk) {
+    publicKey = pk;
+  }
+
+  /**
+   * 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 {
+    super.init(config);
+    // setup the URL to redirect to for authentication
+    authenticationProviderUrl = config
+        .getProperty(AUTHENTICATION_PROVIDER_URL);
+    if (authenticationProviderUrl == null) {
+      throw new ServletException(
+          "Authentication provider URL must not be null - configure: "
+              + AUTHENTICATION_PROVIDER_URL);
+    }
+
+    // setup the public key of the token issuer for verification
+    if (publicKey == null) {
+      String pemPublicKey = config.getProperty(PUBLIC_KEY_PEM);
+      if (pemPublicKey == null) {
+        throw new ServletException(
+            "Public key for signature validation must be provisioned.");
+      }
+      publicKey = CertificateUtil.parseRSAPublicKey(pemPublicKey);
+    }
+    // setup the list of valid audiences for token validation
+    String auds = config.getProperty(EXPECTED_JWT_AUDIENCES);
+    if (auds != null) {
+      // parse into the list
+      String[] audArray = auds.split(",");
+      audiences = new ArrayList<String>();
+      for (String a : audArray) {
+        audiences.add(a);
+      }
+    }
+
+    // setup custom cookie name if configured
+    String customCookieName = config.getProperty(JWT_COOKIE_NAME);
+    if (customCookieName != null) {
+      cookieName = customCookieName;
+    }
+  }
+
+  @Override
+  public AuthenticationToken alternateAuthenticate(HttpServletRequest request,
+      HttpServletResponse response) throws IOException,
+      AuthenticationException {
+    AuthenticationToken token = null;
+
+    String serializedJWT = null;
+    HttpServletRequest req = (HttpServletRequest) request;
+    serializedJWT = getJWTFromCookie(req);
+    if (serializedJWT == null) {
+      String loginURL = constructLoginURL(request, response);
+      LOG.info("sending redirect to: " + loginURL);
+      ((HttpServletResponse) response).sendRedirect(loginURL);
+    } else {
+      String userName = null;
+      SignedJWT jwtToken = null;
+      boolean valid = false;
+      try {
+        jwtToken = SignedJWT.parse(serializedJWT);
+        valid = validateToken(jwtToken);
+        if (valid) {
+          userName = jwtToken.getJWTClaimsSet().getSubject();
+          LOG.info("USERNAME: " + userName);
+        } else {
+          LOG.warn("jwtToken failed validation: " + jwtToken.serialize());
+        }
+      } catch(ParseException pe) {
+        // unable to parse the token let's try and get another one
+        LOG.warn("Unable to parse the JWT token", pe);
+      }
+      if (valid) {
+        LOG.debug("Issuing AuthenticationToken for user.");
+        token = new AuthenticationToken(userName, userName, getType());
+      } else {
+        String loginURL = constructLoginURL(request, response);
+        LOG.info("token validation failed - sending redirect to: " + loginURL);
+        ((HttpServletResponse) response).sendRedirect(loginURL);
+      }
+    }
+    return token;
+  }
+
+  /**
+   * Encapsulate the acquisition of the JWT token from HTTP cookies within the
+   * request.
+   *
+   * @param serializedJWT
+   * @param req
+   * @return serialized JWT token
+   */
+  protected String getJWTFromCookie(HttpServletRequest req) {
+    String serializedJWT = null;
+    Cookie[] cookies = req.getCookies();
+    String userName = null;
+    if (cookies != null) {
+      for (Cookie cookie : cookies) {
+        if (cookieName.equals(cookie.getName())) {
+          LOG.info(cookieName
+              + " cookie has been found and is being processed");
+          serializedJWT = cookie.getValue();
+          break;
+        }
+      }
+    }
+    return serializedJWT;
+  }
+
+  /**
+   * Create the URL to be used for authentication of the user in the absence of
+   * a JWT token within the incoming request.
+   *
+   * @param request
+   * @param response
+   * @return url to use as login url for redirect
+   */
+  protected String constructLoginURL(HttpServletRequest request,
+      HttpServletResponse response) {
+    String delimiter = "?";
+    if (authenticationProviderUrl.contains("?")) {
+      delimiter = "&";
+    }
+    String loginURL = authenticationProviderUrl + delimiter
+        + ORIGINAL_URL_QUERY_PARAM
+        + request.getRequestURL().toString();
+    return loginURL;
+  }
+
+  /**
+   * This method provides a single method for validating the JWT for use in
+   * request processing. It provides for the override of specific aspects of
+   * this implementation through submethods used within but also allows for the
+   * override of the entire token validation algorithm.
+   *
+   * @param jwtToken
+   * @return true if valid
+   * @throws AuthenticationException
+   */
+  protected boolean validateToken(SignedJWT jwtToken) {
+    boolean sigValid = validateSignature(jwtToken);
+    if (!sigValid) {
+      LOG.warn("Signature could not be verified");
+    }
+    boolean audValid = validateAudiences(jwtToken);
+    if (!audValid) {
+      LOG.warn("Audience validation failed.");
+    }
+    boolean expValid = validateExpiration(jwtToken);
+    if (!expValid) {
+      LOG.info("Expiration validation failed.");
+    }
+
+    return sigValid && audValid && expValid;
+  }
+
+  /**
+   * Verify the signature of the JWT token in this method. This method depends
+   * on the public key that was established during init based upon the
+   * provisioned public key. Override this method in subclasses in order to
+   * customize the signature verification behavior.
+   *
+   * @param jwtToken
+   * @throws AuthenticationException
+   */
+  protected boolean validateSignature(SignedJWT jwtToken) {
+    boolean valid = false;
+    if (JWSObject.State.SIGNED == jwtToken.getState()) {
+      LOG.debug("JWT token is in a SIGNED state");
+      if (jwtToken.getSignature() != null) {
+        LOG.debug("JWT token signature is not null");
+        try {
+          JWSVerifier verifier = new RSASSAVerifier(publicKey);
+          if (jwtToken.verify(verifier)) {
+            valid = true;
+            LOG.debug("JWT token has been successfully verified");
+          } else {
+            LOG.warn("JWT signature verification failed.");
+          }
+        } catch (JOSEException je) {
+          LOG.warn("Error while validating signature", je);
+        }
+      }
+    }
+    return valid;
+  }
+
+  /**
+   * Validate whether any of the accepted audience claims is present in the
+   * issued token claims list for audience. Override this method in subclasses
+   * in order to customize the audience validation behavior.
+   *
+   * @param jwtToken
+   *          the JWT token where the allowed audiences will be found
+   * @return true if an expected audience is present, otherwise false
+   */
+  protected boolean validateAudiences(SignedJWT jwtToken) {
+    boolean valid = false;
+    try {
+      List<String> tokenAudienceList = jwtToken.getJWTClaimsSet()
+          .getAudience();
+      // if there were no expected audiences configured then just
+      // consider any audience acceptable
+      if (audiences == null) {
+        valid = true;
+      } else {
+        // if any of the configured audiences is found then consider it
+        // acceptable
+        boolean found = false;
+        for (String aud : tokenAudienceList) {
+          if (audiences.contains(aud)) {
+            LOG.debug("JWT token audience has been successfully validated");
+            valid = true;
+            break;
+          }
+        }
+        if (!valid) {
+          LOG.warn("JWT audience validation failed.");
+        }
+      }
+    } catch (ParseException pe) {
+      LOG.warn("Unable to parse the JWT token.", pe);
+    }
+    return valid;
+  }
+
+  /**
+   * Validate that the expiration time of the JWT token has not been violated.
+   * If it has then throw an AuthenticationException. Override this method in
+   * subclasses in order to customize the expiration validation behavior.
+   *
+   * @param jwtToken
+   * @throws AuthenticationException
+   */
+  protected boolean validateExpiration(SignedJWT jwtToken) {
+    boolean valid = false;
+    try {
+      Date expires = jwtToken.getJWTClaimsSet().getExpirationTime();
+      if (expires != null && new Date().before(expires)) {
+        LOG.debug("JWT token expiration date has been "
+            + "successfully validated");
+        valid = true;
+      } else {
+        LOG.warn("JWT expiration date validation failed.");
+      }
+    } catch (ParseException pe) {
+      LOG.warn("JWT expiration date validation failed.", pe);
+    }
+    return valid;
+  }
+}

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7b1be9e2/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java
new file mode 100644
index 0000000..77b2530
--- /dev/null
+++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.hadoop.security.authentication.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+
+import javax.servlet.ServletException;
+
+public class CertificateUtil {
+  private static final String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n";
+  private static final String PEM_FOOTER = "\n-----END CERTIFICATE-----";
+
+  /**
+   * Gets an RSAPublicKey from the provided PEM encoding.
+   *
+   * @param pem
+   *          - the pem encoding from config without the header and footer
+   * @return RSAPublicKey
+   */
+  public static RSAPublicKey parseRSAPublicKey(String pem) throws ServletException {
+    String fullPem = PEM_HEADER + pem + PEM_FOOTER;
+    PublicKey key = null;
+    try {
+      CertificateFactory fact = CertificateFactory.getInstance("X.509");
+      ByteArrayInputStream is = new ByteArrayInputStream(
+          fullPem.getBytes("UTF8"));
+
+      X509Certificate cer = (X509Certificate) fact.generateCertificate(is);
+      key = cer.getPublicKey();
+    } catch (CertificateException ce) {
+      String message = null;
+      if (pem.startsWith(PEM_HEADER)) {
+        message = "CertificateException - be sure not to include PEM header "
+            + "and footer in the PEM configuration element.";
+      } else {
+        message = "CertificateException - PEM may be corrupt";
+      }
+      throw new ServletException(message, ce);
+    } catch (UnsupportedEncodingException uee) {
+      throw new ServletException(uee);
+    }
+    return (RSAPublicKey) key;
+  }
+}

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7b1be9e2/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestJWTRedirectAuthentictionHandler.java
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestJWTRedirectAuthentictionHandler.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestJWTRedirectAuthentictionHandler.java
new file mode 100644
index 0000000..4ac9535
--- /dev/null
+++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestJWTRedirectAuthentictionHandler.java
@@ -0,0 +1,418 @@
+/**
+ * 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 static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Properties;
+import java.util.Vector;
+import java.util.Date;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.hadoop.minikdc.KerberosSecurityTestcase;
+import org.apache.hadoop.security.authentication.KerberosTestUtils;
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.nimbusds.jose.*;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.util.Base64URL;
+
+public class TestJWTRedirectAuthentictionHandler extends
+    KerberosSecurityTestcase {
+  private static final String SERVICE_URL = "https://localhost:8888/resource";
+  private static final String REDIRECT_LOCATION =
+      "https://localhost:8443/authserver?originalUrl=" + SERVICE_URL;
+  RSAPublicKey publicKey = null;
+  RSAPrivateKey privateKey = null;
+  JWTRedirectAuthenticationHandler handler = null;
+
+  @Test
+  public void testNoPublicKeyJWT() throws Exception {
+    try {
+      Properties props = getProperties();
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000),
+          privateKey);
+
+      Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      fail("alternateAuthentication should have thrown a ServletException");
+    } catch (ServletException se) {
+      assertTrue(se.getMessage().contains(
+          "Public key for signature validation must be provisioned"));
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown a AuthenticationException");
+    }
+  }
+
+  @Test
+  public void testCustomCookieNameJWT() throws Exception {
+    try {
+      handler.setPublicKey(publicKey);
+
+      Properties props = getProperties();
+      props.put(JWTRedirectAuthenticationHandler.JWT_COOKIE_NAME, "jowt");
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000),
+          privateKey);
+
+      Cookie cookie = new Cookie("jowt", jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      Assert.assertEquals("bob", token.getUserName());
+    } catch (ServletException se) {
+      fail("alternateAuthentication should NOT have thrown a ServletException: "
+          + se.getMessage());
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown a AuthenticationException");
+    }
+  }
+
+  @Test
+  public void testNoProviderURLJWT() throws Exception {
+    try {
+      handler.setPublicKey(publicKey);
+
+      Properties props = getProperties();
+      props
+          .remove(JWTRedirectAuthenticationHandler.AUTHENTICATION_PROVIDER_URL);
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000),
+          privateKey);
+
+      Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      fail("alternateAuthentication should have thrown an AuthenticationException");
+    } catch (ServletException se) {
+      assertTrue(se.getMessage().contains(
+          "Authentication provider URL must not be null"));
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown a AuthenticationException");
+    }
+  }
+
+  @Test
+  public void testUnableToParseJWT() throws Exception {
+    try {
+      KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+      kpg.initialize(2048);
+
+      KeyPair kp = kpg.genKeyPair();
+      RSAPublicKey publicKey = (RSAPublicKey) kp.getPublic();
+
+      handler.setPublicKey(publicKey);
+
+      Properties props = getProperties();
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000),
+          privateKey);
+
+      Cookie cookie = new Cookie("hadoop-jwt", "ljm" + jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      Mockito.verify(response).sendRedirect(REDIRECT_LOCATION);
+    } catch (ServletException se) {
+      fail("alternateAuthentication should NOT have thrown a ServletException");
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown a AuthenticationException");
+    }
+  }
+
+  @Test
+  public void testFailedSignatureValidationJWT() throws Exception {
+    try {
+
+      // Create a public key that doesn't match the one needed to
+      // verify the signature - in order to make it fail verification...
+      KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+      kpg.initialize(2048);
+
+      KeyPair kp = kpg.genKeyPair();
+      RSAPublicKey publicKey = (RSAPublicKey) kp.getPublic();
+
+      handler.setPublicKey(publicKey);
+
+      Properties props = getProperties();
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000),
+          privateKey);
+
+      Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      Mockito.verify(response).sendRedirect(REDIRECT_LOCATION);
+    } catch (ServletException se) {
+      fail("alternateAuthentication should NOT have thrown a ServletException");
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown a AuthenticationException");
+    }
+  }
+
+  @Test
+  public void testExpiredJWT() throws Exception {
+    try {
+      handler.setPublicKey(publicKey);
+
+      Properties props = getProperties();
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() - 1000),
+          privateKey);
+
+      Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      Mockito.verify(response).sendRedirect(REDIRECT_LOCATION);
+    } catch (ServletException se) {
+      fail("alternateAuthentication should NOT have thrown a ServletException");
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown a AuthenticationException");
+    }
+  }
+
+  @Test
+  public void testInvalidAudienceJWT() throws Exception {
+    try {
+      handler.setPublicKey(publicKey);
+
+      Properties props = getProperties();
+      props
+          .put(JWTRedirectAuthenticationHandler.EXPECTED_JWT_AUDIENCES, "foo");
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000),
+          privateKey);
+
+      Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      Mockito.verify(response).sendRedirect(REDIRECT_LOCATION);
+    } catch (ServletException se) {
+      fail("alternateAuthentication should NOT have thrown a ServletException");
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown a AuthenticationException");
+    }
+  }
+
+  @Test
+  public void testValidAudienceJWT() throws Exception {
+    try {
+      handler.setPublicKey(publicKey);
+
+      Properties props = getProperties();
+      props
+          .put(JWTRedirectAuthenticationHandler.EXPECTED_JWT_AUDIENCES, "bar");
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000),
+          privateKey);
+
+      Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      Assert.assertEquals("bob", token.getUserName());
+    } catch (ServletException se) {
+      fail("alternateAuthentication should NOT have thrown a ServletException");
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown an AuthenticationException");
+    }
+  }
+
+  @Test
+  public void testValidJWT() throws Exception {
+    try {
+      handler.setPublicKey(publicKey);
+
+      Properties props = getProperties();
+      handler.init(props);
+
+      SignedJWT jwt = getJWT("alice", new Date(new Date().getTime() + 5000),
+          privateKey);
+
+      Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize());
+      HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+      Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie });
+      Mockito.when(request.getRequestURL()).thenReturn(
+          new StringBuffer(SERVICE_URL));
+      HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+      Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn(
+          SERVICE_URL);
+
+      AuthenticationToken token = handler.alternateAuthenticate(request,
+          response);
+      Assert.assertNotNull("Token should not be null.", token);
+      Assert.assertEquals("alice", token.getUserName());
+    } catch (ServletException se) {
+      fail("alternateAuthentication should NOT have thrown a ServletException.");
+    } catch (AuthenticationException ae) {
+      fail("alternateAuthentication should NOT have thrown an AuthenticationException");
+    }
+  }
+
+  @Before
+  public void setup() throws Exception, NoSuchAlgorithmException {
+    setupKerberosRequirements();
+
+    KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+    kpg.initialize(2048);
+
+    KeyPair kp = kpg.genKeyPair();
+    publicKey = (RSAPublicKey) kp.getPublic();
+    privateKey = (RSAPrivateKey) kp.getPrivate();
+
+    handler = new JWTRedirectAuthenticationHandler();
+  }
+
+  protected void setupKerberosRequirements() throws Exception {
+    String[] keytabUsers = new String[] { "HTTP/host1", "HTTP/host2",
+        "HTTP2/host1", "XHTTP/host" };
+    String keytab = KerberosTestUtils.getKeytabFile();
+    getKdc().createPrincipal(new File(keytab), keytabUsers);
+  }
+
+  @After
+  public void teardown() throws Exception {
+    handler.destroy();
+  }
+
+  protected Properties getProperties() {
+    Properties props = new Properties();
+    props.setProperty(
+        JWTRedirectAuthenticationHandler.AUTHENTICATION_PROVIDER_URL,
+        "https://localhost:8443/authserver");
+    props.setProperty("kerberos.principal",
+        KerberosTestUtils.getServerPrincipal());
+    props.setProperty("kerberos.keytab", KerberosTestUtils.getKeytabFile());
+    return props;
+  }
+
+  protected SignedJWT getJWT(String sub, Date expires, RSAPrivateKey privateKey)
+      throws Exception {
+    JWTClaimsSet claimsSet = new JWTClaimsSet();
+    claimsSet.setSubject(sub);
+    claimsSet.setIssueTime(new Date(new Date().getTime()));
+    claimsSet.setIssuer("https://c2id.com");
+    claimsSet.setCustomClaim("scope", "openid");
+    claimsSet.setExpirationTime(expires);
+    List<String> aud = new ArrayList<String>();
+    aud.add("bar");
+    claimsSet.setAudience("bar");
+
+    JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).build();
+
+    SignedJWT signedJWT = new SignedJWT(header, claimsSet);
+    Base64URL sigInput = Base64URL.encode(signedJWT.getSigningInput());
+    JWSSigner signer = new RSASSASigner(privateKey);
+
+    signedJWT.sign(signer);
+
+    return signedJWT;
+  }
+}

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7b1be9e2/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestCertificateUtil.java
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestCertificateUtil.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestCertificateUtil.java
new file mode 100644
index 0000000..f52b6d2
--- /dev/null
+++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestCertificateUtil.java
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.hadoop.security.authentication.util;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.security.interfaces.RSAPublicKey;
+
+import javax.servlet.ServletException;
+
+import org.junit.Test;
+
+public class TestCertificateUtil {
+
+  @Test
+  public void testInvalidPEMWithHeaderAndFooter() throws Exception {
+    String pem = "-----BEGIN CERTIFICATE-----\n"
+        + "MIICOjCCAaOgAwIBAgIJANXi/oWxvJNzMA0GCSqGSIb3DQEBBQUAMF8xCzAJBgNVBAYTAlVTMQ0w"
+        + "CwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZIYWRvb3AxDTALBgNVBAsTBFRl"
+        + "c3QxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNTAxMDIyMTE5MjRaFw0xNjAxMDIyMTE5MjRaMF8x"
+        + "CzAJBgNVBAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZIYWRv"
+        + "b3AxDTALBgNVBAsTBFRlc3QxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOB"
+        + "jQAwgYkCgYEAwpfpLdi7dWTHNzETt+L7618/dWUQFb/C7o1jIxFgbKOVIB6d5YmvUbJck5PYxFkz"
+        + "C25fmU5H71WGOI1Kle5TFDmIo+hqh5xqu1YNRZz9i6D94g+2AyYr9BpvH4ZfdHs7r9AU7c3kq68V"
+        + "7OPuuaHb25J8isiOyA3RiWuJGQlXTdkCAwEAATANBgkqhkiG9w0BAQUFAAOBgQAdRUyCUqE9sdim"
+        + "Fbll9BuZDKV16WXeWGq+kTd7ETe7l0fqXjq5EnrifOai0L/pXwVvS2jrFkKQRlRxRGUNaeEBZ2Wy"
+        + "9aTyR+HGHCfvwoCegc9rAVw/DLaRriSO/jnEXzYK6XLVKH+hx5UXrJ7Oyc7JjZUc3g9kCWORThCX"
+        + "Mzc1xA==" + "\n-----END CERTIFICATE-----";
+    try {
+      CertificateUtil.parseRSAPublicKey(pem);
+      fail("Should not have thrown ServletException");
+    } catch (ServletException se) {
+      assertTrue(se.getMessage().contains("PEM header"));
+    }
+  }
+
+  @Test
+  public void testCorruptPEM() throws Exception {
+    String pem = "LJMLJMMIICOjCCAaOgAwIBAgIJANXi/oWxvJNzMA0GCSqGSIb3DQEBBQUAMF8xCzAJBgNVBAYTAlVTMQ0w"
+        + "CwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZIYWRvb3AxDTALBgNVBAsTBFRl"
+        + "c3QxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNTAxMDIyMTE5MjRaFw0xNjAxMDIyMTE5MjRaMF8x"
+        + "CzAJBgNVBAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZIYWRv"
+        + "b3AxDTALBgNVBAsTBFRlc3QxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOB"
+        + "jQAwgYkCgYEAwpfpLdi7dWTHNzETt+L7618/dWUQFb/C7o1jIxFgbKOVIB6d5YmvUbJck5PYxFkz"
+        + "C25fmU5H71WGOI1Kle5TFDmIo+hqh5xqu1YNRZz9i6D94g+2AyYr9BpvH4ZfdHs7r9AU7c3kq68V"
+        + "7OPuuaHb25J8isiOyA3RiWuJGQlXTdkCAwEAATANBgkqhkiG9w0BAQUFAAOBgQAdRUyCUqE9sdim"
+        + "Fbll9BuZDKV16WXeWGq+kTd7ETe7l0fqXjq5EnrifOai0L/pXwVvS2jrFkKQRlRxRGUNaeEBZ2Wy"
+        + "9aTyR+HGHCfvwoCegc9rAVw/DLaRriSO/jnEXzYK6XLVKH+hx5UXrJ7Oyc7JjZUc3g9kCWORThCX"
+        + "Mzc1xA==";
+    try {
+      CertificateUtil.parseRSAPublicKey(pem);
+      fail("Should not have thrown ServletException");
+    } catch (ServletException se) {
+      assertTrue(se.getMessage().contains("corrupt"));
+    }
+  }
+
+  @Test
+  public void testValidPEM() throws Exception {
+    String pem = "MIICOjCCAaOgAwIBAgIJANXi/oWxvJNzMA0GCSqGSIb3DQEBBQUAMF8xCzAJBgNVBAYTAlVTMQ0w"
+        + "CwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZIYWRvb3AxDTALBgNVBAsTBFRl"
+        + "c3QxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNTAxMDIyMTE5MjRaFw0xNjAxMDIyMTE5MjRaMF8x"
+        + "CzAJBgNVBAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZIYWRv"
+        + "b3AxDTALBgNVBAsTBFRlc3QxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOB"
+        + "jQAwgYkCgYEAwpfpLdi7dWTHNzETt+L7618/dWUQFb/C7o1jIxFgbKOVIB6d5YmvUbJck5PYxFkz"
+        + "C25fmU5H71WGOI1Kle5TFDmIo+hqh5xqu1YNRZz9i6D94g+2AyYr9BpvH4ZfdHs7r9AU7c3kq68V"
+        + "7OPuuaHb25J8isiOyA3RiWuJGQlXTdkCAwEAATANBgkqhkiG9w0BAQUFAAOBgQAdRUyCUqE9sdim"
+        + "Fbll9BuZDKV16WXeWGq+kTd7ETe7l0fqXjq5EnrifOai0L/pXwVvS2jrFkKQRlRxRGUNaeEBZ2Wy"
+        + "9aTyR+HGHCfvwoCegc9rAVw/DLaRriSO/jnEXzYK6XLVKH+hx5UXrJ7Oyc7JjZUc3g9kCWORThCX"
+        + "Mzc1xA==";
+    try {
+      RSAPublicKey pk = CertificateUtil.parseRSAPublicKey(pem);
+      assertTrue(pk != null);
+      assertTrue(pk.getAlgorithm().equals("RSA"));
+    } catch (ServletException se) {
+      fail("Should not have thrown ServletException");
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7b1be9e2/hadoop-common-project/hadoop-common/CHANGES.txt
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-common/CHANGES.txt b/hadoop-common-project/hadoop-common/CHANGES.txt
index f52e09f..5a8cda4 100644
--- a/hadoop-common-project/hadoop-common/CHANGES.txt
+++ b/hadoop-common-project/hadoop-common/CHANGES.txt
@@ -481,6 +481,9 @@ Release 2.8.0 - UNRELEASED
     HADOOP-9805. Refactor RawLocalFileSystem#rename for improved testability.
     (Jean-Pierre Matsumoto via cnauroth)
 
+    HADOOP-11717. Support JWT tokens for web single sign on to the Hadoop
+    servers. (Larry McCay via omalley)
+
   OPTIMIZATIONS
 
     HADOOP-11785. Reduce the number of listStatus operation in distcp

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7b1be9e2/hadoop-project/pom.xml
----------------------------------------------------------------------
diff --git a/hadoop-project/pom.xml b/hadoop-project/pom.xml
index a2def1a..2c8837c 100644
--- a/hadoop-project/pom.xml
+++ b/hadoop-project/pom.xml
@@ -970,6 +970,19 @@
        <scope>test</scope>
      </dependency>
       
+      <dependency>
+          <groupId>com.nimbusds</groupId>
+          <artifactId>nimbus-jose-jwt</artifactId>
+          <version>3.9</version>
+          <scope>compile</scope>
+          <exclusions>
+          <exclusion>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+
     </dependencies>
   </dependencyManagement>