This is an automated email from the ASF dual-hosted git repository.

michaelsmith pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git

commit 34c084cebb2f52a6ee11d3d93609b3e4e238816f
Author: gaurav1086 <[email protected]>
AuthorDate: Tue Apr 23 08:34:17 2024 -0700

    IMPALA-12559: Support x5c Parameter for RSA JSON
    Web Keys
    
    This enables the jwt verification using the x5c
    certificate(s) in the RSA jwks keys. The x5c claim can be
    part of the jwks either as a string or an array.
    This patch only supports a single x5c certificate per
    jwk.
    
    If the "x5c" is present and "alg" is not present,
    then "alg" is extracted from the "x5c" certificate using the
    signature algorithm. However, if "x5c" is not preseent, then
    "alg" is a mandatory field on jwk.
    
    Current mapping of signature algorithm string => algorithm:
    
    sha256WithRSAEncryption => rs256
    sha384WithRSAEncryption => rs384
    sha512WithRSAEncryption => rs512
    
    If "x5c" is present, then it is given priority over other
    mandatory fields like "n", "e" to construct the public key.
    
    Testing:
    * added unit test VerifyJwtTokenWithx5cCertificate to
    verify jwt with x5c certificate.
    * added unit test VerifyJwtTokenWithx5cCertificateWithoutAlg
    to verify jwt with x5c certificate without "alg".
    * added e2e test testJwtAuthWithJwksX5cHttpUrl to verify
    jwt with x5c certificate.
    
    Change-Id: I70be6f9f54190544aa005b2644e2ed8db6f6bb74
    Reviewed-on: http://gerrit.cloudera.org:8080/21382
    Reviewed-by: Jason Fehr <[email protected]>
    Reviewed-by: Wenzhe Zhou <[email protected]>
    Tested-by: Impala Public Jenkins <[email protected]>
---
 be/src/util/jwt-util-test.cc                       | 164 +++++++++++++++++++++
 be/src/util/jwt-util.cc                            | 128 ++++++++++++++--
 .../apache/impala/customcluster/JwtHttpTest.java   |  55 +++++++
 testdata/jwt/jwks_x5c_rs256.json                   |  14 ++
 4 files changed, 346 insertions(+), 15 deletions(-)

diff --git a/be/src/util/jwt-util-test.cc b/be/src/util/jwt-util-test.cc
index 25745c1a6..cbf09808f 100644
--- a/be/src/util/jwt-util-test.cc
+++ b/be/src/util/jwt-util-test.cc
@@ -17,6 +17,7 @@
 
 #include <cstdio> // file stuff
 #include <gutil/strings/substitute.h>
+#include <openssl/rand.h>
 
 #include "jwt-util-internal.h"
 #include "jwt-util.h"
@@ -318,6 +319,94 @@ std::string ecdsa256_pub_key_jwk_y = 
"fgazwzugi-g_2lv8jzm115u0qWaIJkcBkTnDgN8lJX
 
 std::string kid_1 = "public:c424b67b-fe28-45d7-b015-f79da50b5b21";
 std::string kid_2 = "public:9b9d0b47-b9ed-4ba6-9180-52fc5b161a3a";
+std::string kid_x5c = "kid_x5c";
+
+std::string rsa_pub_key_jwk_n_1 = 
"5dIMi_SgqaF7CZbwWgVLCUwILxYW4LAY6cU-ptsb9H4LRgw"
+"cIGoj77jJwpU1P5GJCm_HNRk5DHnSqfWHDOex1k5Pcqhk8ukAZzDMWwCWDcFkOA26-Kikgugtys2MLPwa"
+"sr_DgvTQDsqiW7XaeIjm0Y8mnrfjy018sLrtNsbckYNwftWgDjYFFQ8kubuezUg-KxGfq8N9DXtXaEgpV"
+"pjA6hHe9svHI8d3gKp9B3AMUkOjDTJjZO_zPBUA9w0zNRH9BuaB8iSMO1pmPoMbg_N_Oq_wpLMCDc2nTM"
+"Dmz5U0nQDfAUc3nba6oG_g_yuKYts4QoriFboxV-jP4bBr4-4NjPRPTEfIhLh1gmPX60CiEfiUx9w9bJ6"
+"CaetKiqGudagc57BK_UT9rrRp4jwqt_iWPmV9CSvL5ebYkmacujdMkW0ZmN1y3QOXykc4XLAd3lK5k7a_"
+"csI2V-y5ekDL1MonLmxk6I4aiRUG77r76KbPT6AjFxRN8enCdkIT6IvPgb1HWIrK7YwxXvmIK4ELzzGvw"
+"qTqQySQxLNklUXGrgmTlHaiwsGcpTbltAoCI1j_JffT-5dcxnk_FST4ZgAWMjzPkbTWA2pgJVDgqkaoM_"
+"4D4xHjHrpUE7x9ZQKgEwAF9aH7ZauqOFaKkTrNjN3gF6j4b7CwXk5gqG_uXGvPOzJHD-s";
+
+std::string rsa_pub_key_jwk_e_1 = "AQAB";
+
+std::string rsa_pub_key_jwk_x5c = 
"MIIE2jCCAsICAQEwDQYJKoZIhvcNAQELBQAwMzELMAkGA1U"
+"EBhMCVVMxEDAOBgNVBAoMB0pXVC1DUFAxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzEyMjIxMzIzNTda"
+"Fw0zMzEyMTkxMzIzNTdaMDMxCzAJBgNVBAYTAlVTMRAwDgYDVQQKDAdKV1QtQ1BQMRIwEAYDVQQDDAlsb"
+"2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDl0gyL9KCpoXsJlvBaBUsJTAgvFh"
+"bgsBjpxT6m2xv0fgtGDBwgaiPvuMnClTU/kYkKb8c1GTkMedKp9YcM57HWTk9yqGTy6QBnMMxbAJYNwWQ"
+"4Dbr4qKSC6C3KzYws/Bqyv8OC9NAOyqJbtdp4iObRjyaet+PLTXywuu02xtyRg3B+1aAONgUVDyS5u57N"
+"SD4rEZ+rw30Ne1doSClWmMDqEd72y8cjx3eAqn0HcAxSQ6MNMmNk7/M8FQD3DTM1Ef0G5oHyJIw7WmY+g"
+"xuD8386r/CkswINzadMwObPlTSdAN8BRzedtrqgb+D/K4pi2zhCiuIVujFX6M/hsGvj7g2M9E9MR8iEuH"
+"WCY9frQKIR+JTH3D1snoJp60qKoa51qBznsEr9RP2utGniPCq3+JY+ZX0JK8vl5tiSZpy6N0yRbRmY3XL"
+"dA5fKRzhcsB3eUrmTtr9ywjZX7Ll6QMvUyicubGTojhqJFQbvuvvops9PoCMXFE3x6cJ2QhPoi8+BvUdY"
+"isrtjDFe+YgrgQvPMa/CpOpDJJDEs2SVRcauCZOUdqLCwZylNuW0CgIjWP8l99P7l1zGeT8VJPhmABYyP"
+"M+RtNYDamAlUOCqRqgz/gPjEeMeulQTvH1lAqATAAX1oftlq6o4VoqROs2M3eAXqPhvsLBeTmCob+5ca8"
+"87MkcP6wIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQBW2kREK4hlzxCDqykxrwfbQpiPwrbFmn+3RDJla+p"
+"I4L3wrvYT1nU96guFIU3zKnbMzqwPMRUCUjadr2jKxAmMWxCd/ThHQB+ne5xTvx7/6RVQfGjyMCG/SZtS"
+"H8/aO7ILNRtPT+SL5ZZwezaqv6gD89tSXB/w/0pYXy70wDuU17KCrTsKSISWGJ1cKi5l2R/m/ZaGjcV8U"
+"8NcFepF2bX3u/i0zhaqOqjiwrSEt7fWGDLabPs6n7GtfibZROEDZ/h0JrDINC+6mSfTOYAMJvGjeHA3H/"
+"NvzqR+CJgpXGCqElqVuBF0HdxPmwRRBoZC/BLIEcz0VHmB4rcpfaV47TZT+J+04fHYp4Y1S0u112CDrDe"
+"+61cDrnbDHC7aGX0G93pYSBKAB1e3LLc9rXQgf2F0pRtFB3rgZA9MtJ+TL7DUvY4VXJNq3v7UolIdldYR"
+"dk21YqAS2Hp0fivvFoEk2P/WbwDEErxR0FkZ/JQoI9FMJ9AvDxa4MsFFtlQVInfD2HUu+nhnuEAA8R6L+"
+"F2XqhfLY/H7H31iFBK6UCuqptED71VwWHqfBsAPRhLXAqGco7Ln2dzioyj0QdwJqQQIqigltSYtXxfIML"
+"W0BekQ5yln7QTxnZlobkPHUW9s3NK+OMLuKCzVREzjic/aioQP3cRBMXkG2deMwrk3aX8yJuz4gA==";
+
+std::string pem_priv_key = R"(-----BEGIN PRIVATE KEY-----
+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
++grQSrtFfSnickR6i3XrDlspd/khcQ==
+-----END PRIVATE KEY-----)";
 
 std::string jwks_hs_file_format = R"(
 {
@@ -341,6 +430,42 @@ std::string jwks_ec_file_format = R"(
   ]
 })";
 
+std::string jwks_rsa_file_format_x5c = R"(
+{
+  "keys": [
+    { "kty": "RSA", "kid": "$0", "alg": "$1", "n": "$2", "e": "$3", "x5c": [ 
"$4" ] }
+  ]
+})";
+
+std::string jwks_rsa_file_format_x5c_without_alg = R"(
+{
+  "keys": [
+    { "kty": "RSA", "kid": "$0", "n": "$1", "e": "$2", "x5c": [ "$3" ] }
+  ]
+})";
+
+/// Creates a JWT Token with x5c using the pem_priv_key
+static std::string CreateJwtX5cToken() {
+  // Create a JWT token from pem_priv_key
+  unsigned char nonce[24];
+  RAND_bytes(nonce, sizeof(nonce));
+  std::string jti =
+    jwt::base::encode<jwt::alphabet::base64url>(
+        std::string{reinterpret_cast<const char*>(nonce), sizeof(nonce)});
+
+  std::string token = jwt::create()
+    .set_issuer("auth0")
+    .set_type("JWT")
+    .set_id(jti)
+    .set_key_id(kid_x5c)
+    .set_subject("jwt-cpp.example.localhost")
+    .set_issued_at(std::chrono::system_clock::now())
+    .set_expires_at(std::chrono::system_clock::now() + 
std::chrono::seconds{36000})
+    .sign(jwt::algorithm::rs256("", pem_priv_key, "", ""));
+
+  return token;
+}
+
 /// Utility class for creating a file that will be automatically deleted upon 
test
 /// completion.
 class TempTestDataFile {
@@ -1139,4 +1264,43 @@ TEST(JwtUtilTest, VerifyJwtFailExpiredToken) {
       << " Actual error: " << status.GetDetail();
 }
 
+TEST(JwtUtilTest, VerifyJwtTokenWithx5cCertificate) {
+  // Verify JWT token with x5c certificate.
+  TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format_x5c, kid_x5c, 
"RS256",
+      rsa_pub_key_jwk_n_1, rsa_pub_key_jwk_e_1, rsa_pub_key_jwk_x5c));
+  JWTHelper jwt_helper;
+  Status status = jwt_helper.Init(jwks_file.Filename());
+  EXPECT_OK(status);
+
+  // Create JWT Token from the pem
+  auto token = CreateJwtX5cToken();
+  JWTHelper::UniqueJWTDecodedToken decoded_token;
+  status = JWTHelper::Decode(token, decoded_token);
+  EXPECT_OK(status);
+
+  // Verify the token with the public key
+  status = jwt_helper.Verify(decoded_token.get());
+  EXPECT_OK(status);
+}
+
+TEST(JwtUtilTest, VerifyJwtTokenWithx5cCertificateWithoutAlg) {
+  // Verify JWT token with x5c certificate and without "alg".
+  TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format_x5c_without_alg, 
kid_x5c,
+      rsa_pub_key_jwk_n_1, rsa_pub_key_jwk_e_1, rsa_pub_key_jwk_x5c));
+
+  JWTHelper jwt_helper;
+  Status status = jwt_helper.Init(jwks_file.Filename());
+  EXPECT_OK(status);
+
+  // Create JWT Token from the pem
+  auto token = CreateJwtX5cToken();
+  JWTHelper::UniqueJWTDecodedToken decoded_token;
+  status = JWTHelper::Decode(token, decoded_token);
+  EXPECT_OK(status);
+
+  // Verify the token with the public key
+  status = jwt_helper.Verify(decoded_token.get());
+  EXPECT_OK(status);
+}
+
 } // namespace impala
diff --git a/be/src/util/jwt-util.cc b/be/src/util/jwt-util.cc
index 97d0df312..45837276b 100644
--- a/be/src/util/jwt-util.cc
+++ b/be/src/util/jwt-util.cc
@@ -29,7 +29,10 @@
 #include <openssl/ec.h>
 #include <openssl/evp.h>
 #include <openssl/pem.h>
+#include <openssl/rand.h>
 #include <openssl/rsa.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
 #include <rapidjson/document.h>
 #include <rapidjson/error/en.h>
 #include <rapidjson/filereadstream.h>
@@ -47,6 +50,12 @@
 DECLARE_int32(jwks_update_frequency_s);
 DECLARE_int32(jwks_pulling_timeout_s);
 
+// Support only a single x5c certificate.
+// Update MAX_X5C_CERTIFICATES when we can
+// support more than one.
+#define MAX_X5C_CERTIFICATES 1
+static const char* ARRAY_TYPE = "Array";
+
 namespace impala {
 
 using rapidjson::Document;
@@ -136,11 +145,23 @@ class JWKSetParser {
   // Parse a public key and populate JWKS's internal map.
   Status ParseKey(const Value& json_key) {
     std::unordered_map<std::string, std::string> kv_map;
-    string k, v;
     for (Value::ConstMemberIterator member = json_key.MemberBegin();
          member != json_key.MemberEnd(); ++member) {
+      string k, v, values[MAX_X5C_CERTIFICATES];
       k = string(member->name.GetString());
-      RETURN_IF_ERROR(ReadKeyProperty(k.c_str(), json_key, &v, /*required*/ 
false));
+      const Value& json_value = json_key[k.c_str()];
+      if (NameOfTypeOfJsonValue(json_value) == ARRAY_TYPE) {
+        RETURN_IF_ERROR(ReadKeyArrayProperty(k.c_str(), json_key, values,
+            /*required*/ false));
+        // If x5c certificate is present, pick up the first element
+        if (!values[0].empty()) {
+          v = values[0];
+        } else {
+          return Status(Substitute("$0 property must be a non-empty array", 
k));
+        }
+      } else {
+        RETURN_IF_ERROR(ReadKeyProperty(k.c_str(), json_key, &v, /*required*/ 
false));
+      }
       if (kv_map.find(k) == kv_map.end()) {
         kv_map.insert(make_pair(k, v));
       } else {
@@ -193,6 +214,22 @@ class JWKSetParser {
     return ValidateTypeAndExtractValue(name, json_value, value);
   }
 
+  // Reads a key property of type array of the given name and assigns the 
property
+  // value to the out parameter. A true return value indicates success.
+  template <typename T>
+  Status ReadKeyArrayProperty(
+      const string& name, const Value& json_key, T* value, bool required = 
true) {
+    const Value& json_value = json_key[name.c_str()];
+    if (json_value.IsNull()) {
+      if (required) {
+        return Status(Substitute("'$0' property is required and cannot be 
null", name));
+      } else {
+        return Status::OK();
+      }
+    }
+    return ValidateTypeAndExtractArrayValue(name, json_value, value);
+  }
+
 // Extract a value stored in a rapidjson::Value and assign it to the out 
parameter.
 // The type will be validated before extraction. A true return value indicates 
success.
 // The name parameter is only used to generate an error message upon failure.
@@ -208,7 +245,26 @@ class JWKSetParser {
     return Status::OK();                                                       
        \
   }
 
+// Extract a value stored in a rapidjson::Value and assign it to the out 
parameter.
+// The type will be validated before extraction. Upon success, status is 
returned as OK.
+// The name parameter is only used to generate an error message upon failure.
+#define EXTRACT_ARRAY_VALUE(json_type, cpp_type)                               
        \
+  Status ValidateTypeAndExtractArrayValue(                                     
        \
+      const string& name, const Value& json_value, cpp_type* value) {          
        \
+    if (!json_value.Is##json_type()) {                                         
        \
+      return Status(                                                           
        \
+          Substitute("'$0' property must be of type " #json_type " but is a 
$1", name, \
+              NameOfTypeOfJsonValue(json_value)));                             
        \
+    }                                                                          
        \
+                                                                               
        \
+    for (size_t i = 0; i < json_value.Size() && i < MAX_X5C_CERTIFICATES; i++) 
 {      \
+      value[i] = json_value[i].GetString();                                    
        \
+    }                                                                          
        \
+    return Status::OK();                                                       
        \
+  }                                                                            
        \
+
   EXTRACT_VALUE(String, string)
+  EXTRACT_ARRAY_VALUE(Array, string)
   // EXTRACT_VALUE(Bool, bool)
 };
 
@@ -296,13 +352,7 @@ Status RSAJWTPublicKeyBuilder::CreateJWKPublicKey(
   //   "e":"AQAB",
   //   "kid":"Id that can be uniquely Identified"
   // }
-  auto it_alg = kv_map.find("alg");
-  if (it_alg == kv_map.end()) return Status("'alg' property is required");
-  string algorithm = boost::algorithm::to_lower_copy(it_alg->second);
-  if (algorithm.empty()) {
-    return Status(Substitute("'alg' property must be a non-empty string"));
-  }
-
+  string pub_key_str, algorithm;
   auto it_n = kv_map.find("n");
   auto it_e = kv_map.find("e");
   if (it_n == kv_map.end() || it_e == kv_map.end()) {
@@ -317,21 +367,69 @@ Status RSAJWTPublicKeyBuilder::CreateJWKPublicKey(
         Substitute("Invalid public key 'n':'$0', 'e':'$1'", it_n->second, 
it_e->second));
   }
 
+  // if jwk contains "x5c", then use it instead of "n" and "e"
+  // to construct the public key
+  auto it_x5c = kv_map.find("x5c");
+  if (it_x5c != kv_map.end()) {
+    pub_key_str = jwt::helper::convert_base64_der_to_pem(it_x5c->second);
+    // if "x5c" is present but "alg" is not present, extract it from x5c 
certificate.
+    auto it_alg = kv_map.find("alg");
+    if (it_alg == kv_map.end()) {
+      const char *data = pub_key_str.c_str();
+      BIO *bio = BIO_new(BIO_s_mem());
+      if (!bio) {
+        return Status(Substitute("Failed to allocate memory for BIO"));
+      }
+      BIO_puts(bio, data);
+      X509 *cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
+      if (!cert) {
+        BIO_free(bio);
+        return Status(Substitute("Invalid x5c certificate"));
+      }
+      auto alg = X509_get0_tbs_sigalg(cert);
+      int pkey_nid = OBJ_obj2nid(alg->algorithm);
+      std::string sigalg(OBJ_nid2ln(pkey_nid));
+      if (sigalg == "sha256WithRSAEncryption") {
+        algorithm = "rs256";
+      } else if (sigalg == "sha384WithRSAEncryption") {
+        algorithm = "rs384";
+      } else if (sigalg == "sha512WithRSAEncryption") {
+        algorithm = "rs512";
+      } else {
+        BIO_free(bio);
+        return Status(Substitute("Unsupported alg $0 in signature", sigalg));
+      }
+      BIO_free(bio);
+      X509_free(cert);
+    } else {
+      algorithm = boost::algorithm::to_lower_copy(it_alg->second);
+    }
+  } else {
+    // if "x5c" is not present "alg" must be present.
+    pub_key_str = pub_key;
+    auto it_alg = kv_map.find("alg");
+    if (it_alg == kv_map.end()) return Status("'alg' property is required");
+    algorithm = boost::algorithm::to_lower_copy(it_alg->second);
+  }
+
+  if (algorithm.empty()) {
+    return Status(Substitute("'alg' property must be a non-empty string"));
+  }
   Status status;
   JWTPublicKey* jwt_pub_key = nullptr;
   try {
     if (algorithm == "rs256") {
-      jwt_pub_key = new RS256JWTPublicKey(algorithm, pub_key);
+      jwt_pub_key = new RS256JWTPublicKey(algorithm, pub_key_str);
     } else if (algorithm == "rs384") {
-      jwt_pub_key = new RS384JWTPublicKey(algorithm, pub_key);
+      jwt_pub_key = new RS384JWTPublicKey(algorithm, pub_key_str);
     } else if (algorithm == "rs512") {
-      jwt_pub_key = new RS512JWTPublicKey(algorithm, pub_key);
+      jwt_pub_key = new RS512JWTPublicKey(algorithm, pub_key_str);
     } else if (algorithm == "ps256") {
-      jwt_pub_key = new PS256JWTPublicKey(algorithm, pub_key);
+      jwt_pub_key = new PS256JWTPublicKey(algorithm, pub_key_str);
     } else if (algorithm == "ps384") {
-      jwt_pub_key = new PS384JWTPublicKey(algorithm, pub_key);
+      jwt_pub_key = new PS384JWTPublicKey(algorithm, pub_key_str);
     } else if (algorithm == "ps512") {
-      jwt_pub_key = new PS512JWTPublicKey(algorithm, pub_key);
+      jwt_pub_key = new PS512JWTPublicKey(algorithm, pub_key_str);
     } else {
       return Status(Substitute("Invalid 'alg' property value: '$0'", 
algorithm));
     }
diff --git a/fe/src/test/java/org/apache/impala/customcluster/JwtHttpTest.java 
b/fe/src/test/java/org/apache/impala/customcluster/JwtHttpTest.java
index 82fba0618..87fe47033 100644
--- a/fe/src/test/java/org/apache/impala/customcluster/JwtHttpTest.java
+++ b/fe/src/test/java/org/apache/impala/customcluster/JwtHttpTest.java
@@ -69,6 +69,20 @@ public class JwtHttpTest {
       + 
"bZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO25b5n4fryfKasSgaZfmk0C"
       + "oOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKsPk0bg";
 
+  String jwtTokenX5c_ = 
"eyJhbGciOiJSUzI1NiIsImtpZCI6ImtpZF94NWMiLCJ0eXAiOiJKV1Q"
+      + 
"ifQ.eyJleHAiOjI1ODk4MzQ2NzUsImlhdCI6MTcxNTYzMjM1MSwiaXNzIjoiYXV0aDAiLCJ"
+      + 
"qdGkiOiJ4OEpoSDZVd21XQUZfSGZ2cnR0aEREbWYzREwzTVRzWSIsInN1YiI6Imp3dC1jcH"
+      + 
"AuZXhhbXBsZS5sb2NhbGhvc3QifQ.JZxb9GPuhtlbxWT6_XWX0uZ9EN7PP4frfHNxjquDIl"
+      + 
"gv7At8sEFw21mVWKoafDHKRPzt35zhRlRO9saXQOyVFxSzHHv23RSS47bgUqcpkHQQltP6P"
+      + 
"9SRbJDT7GB13Kusx5Pzl-kJosNR-ZpiQY_nkJEUPHj9vIYAc6B5eGudGEoyWAvjtNE2uBCr"
+      + 
"t5UodEO1RqcZOwjZivTjIIjCOgu3ibz2lmJfhEGwSHOm5uld7sdjjnAviVvVSRASoHP4e2Y"
+      + 
"u4aFevvNaW-CNNlNLXq1QlLE9ClB-IgecZUFOexGZaLSNGiKuAvAqzN6ks_gUJKgtqBeQGe"
+      + 
"DVqCNPE8JuNTfIG0W7Ywb3U9zFBeZ3CtI4RwQsKOYncuy54AC841iGAWkAChsWtBkgTjupS"
+      + 
"ExjvUsKTu3MK5ffbh4LARrj3fTOZlmOqRCM884WG2KoN695dqxcmQKf5QiYMTrFXEVUCM-Y"
+      + 
"4smZqHsTQI3PxfcU6neYnwDDMS-FUNePX8yLX_3E2FpBTIuBitwInO1Bk6SsbZvZRmG-2Cw"
+      + 
"EWlclIr9hMxSeProDSamS5mI89VuShDLYUXaDISSKXoKriFdOxICL2uTdbQLsb_6Z5GbYW4"
+      + 
"2CcUSgG9lJmImtMKH9bS2wwSOmUBi03XBLrraODdI69_EFWFgCG9WjcWJo0R74GJbHAi3cyLM";
+
   /*
    * Create JWKS file in the root directory of WebServer if it's set as true.
    */
@@ -541,6 +555,47 @@ public class JwtHttpTest {
     return certDir.toString();
   }
 
+  /**
+   * Tests if sessions are authenticated by verifying the JWT token for 
connections
+   * to the HTTP hiveserver2 endpoint. The JWKS contains x5c certificate which 
is used
+   * for for JWT verification. The JWKS is specified as HTTP URL to the 
statestore Web
+   * server.
+   */
+  @Test
+  public void testJwtAuthWithJwksX5cHttpUrl() throws Exception {
+    createJWKSForWebServer_ = false;
+    String jwksFilename =
+        new File(System.getenv("IMPALA_HOME"), 
"testdata/jwt/jwks_x5c_rs256.json").getPath();
+    String impaladJwtArgs = String.format("--jwt_token_auth=true "
+            + "--jwt_validate_signature=true --jwks_file_path=%s "
+            + "--jwt_custom_claim_username=sub "
+            + "--jwt_allow_without_tls=true", jwksFilename);
+    setUp(impaladJwtArgs);
+
+    THttpClient transport = new THttpClient("http://localhost:28000";);
+    Map<String, String> headers = new HashMap<String, String>();
+
+    // Authenticate with valid JWT Token in HTTP header.
+    headers.put("Authorization", "Bearer " + jwtTokenX5c_);
+    headers.put("X-Forwarded-For", "127.0.0.1");
+    transport.setCustomHeaders(headers);
+    transport.open();
+    TCLIService.Iface client = new TCLIService.Client(new 
TBinaryProtocol(transport));
+
+    // Open a session which will get username 'jwt-cpp.example.localhost' from 
JWT token
+    // and use it as login user.
+    TOpenSessionReq openReq = new TOpenSessionReq();
+    TOpenSessionResp openResp = client.OpenSession(openReq);
+    // One successful authentication.
+    verifyJwtAuthMetrics(1, 0);
+    // Running a query should succeed.
+    TOperationHandle operationHandle = execAndFetch(
+        client, openResp.getSessionHandle(), "select logged_in_user()",
+          "jwt-cpp.example.localhost");
+    // Two more successful authentications - for the Exec() and the Fetch().
+     verifyJwtAuthMetrics(3, 0);
+  }
+
   /**
    * Asserts that the specified string is present in the impalad.ERROR file 
within the
    * specified log directory.
diff --git a/testdata/jwt/jwks_x5c_rs256.json b/testdata/jwt/jwks_x5c_rs256.json
new file mode 100644
index 000000000..89eb0aacd
--- /dev/null
+++ b/testdata/jwt/jwks_x5c_rs256.json
@@ -0,0 +1,14 @@
+{
+    "keys": [
+        {
+            "kty": "RSA",
+            "kid": "kid_x5c",
+            "alg": "RS256",
+            "n": 
"5dIMi_SgqaF7CZbwWgVLCUwILxYW4LAY6cU-ptsb9H4LRgwcIGoj77jJwpU1P5GJCm_HNRk5DHnSqfWHDOex1k5Pcqhk8ukAZzDMWwCWDcFkOA26-Kikgugtys2MLPwasr_DgvTQDsqiW7XaeIjm0Y8mnrfjy018sLrtNsbckYNwftWgDjYFFQ8kubuezUg-KxGfq8N9DXtXaEgpVpjA6hHe9svHI8d3gKp9B3AMUkOjDTJjZO_zPBUA9w0zNRH9BuaB8iSMO1pmPoMbg_N_Oq_wpLMCDc2nTMDmz5U0nQDfAUc3nba6oG_g_yuKYts4QoriFboxV-jP4bBr4-4NjPRPTEfIhLh1gmPX60CiEfiUx9w9bJ6CaetKiqGudagc57BK_UT9rrRp4jwqt_iWPmV9CSvL5ebYkmacujdMkW0ZmN1y3QOXykc4XLAd3lK5k7a_csI2V-y5ekDL1MonLmxk6I
 [...]
+            "e": "AQAB",
+            "x5c": [
+                
"MIIE2jCCAsICAQEwDQYJKoZIhvcNAQELBQAwMzELMAkGA1UEBhMCVVMxEDAOBgNVBAoMB0pXVC1DUFAxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzEyMjIxMzIzNTdaFw0zMzEyMTkxMzIzNTdaMDMxCzAJBgNVBAYTAlVTMRAwDgYDVQQKDAdKV1QtQ1BQMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDl0gyL9KCpoXsJlvBaBUsJTAgvFhbgsBjpxT6m2xv0fgtGDBwgaiPvuMnClTU/kYkKb8c1GTkMedKp9YcM57HWTk9yqGTy6QBnMMxbAJYNwWQ4Dbr4qKSC6C3KzYws/Bqyv8OC9NAOyqJbtdp4iObRjyaet+PLTXywuu02xtyRg3B+1aAONgUVDyS5u57NSD4rEZ+rw30Ne1doSClWmMD
 [...]
+            ]
+        }
+    ]
+}

Reply via email to