Wenzhe Zhou has posted comments on this change. ( 
http://gerrit.cloudera.org:8080/17435 )

Change subject: IMPALA-10489: Implement JWT support
......................................................................


Patch Set 9:

(22 comments)

http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/rpc/authentication.cc
File be/src/rpc/authentication.cc:

http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/rpc/authentication.cc@156
PS9, Line 156: DEFINE_string(jwt_custom_claim_username
> Do we have any code that would validate this value? (i.e. it should not be
Add code to validate this value in AuthManager::Init().


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/rpc/authentication.cc@156
PS9, Line 156: "Custom claim 'username'"
> Nit: I would expand on this description to note that this specifies the cus
Updated iy.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/rpc/authentication.cc@1432
PS9, Line 1432:     if (use_saml) {
              :       SecureAuthProvider* sap = NULL;
              :       external_http_auth_provider_.reset(sap = new 
SecureAuthProvider(false));
              :       sap->InitSaml();
              :       LOG(INFO) <<
              :           "External communication is authenticated for hs2-http 
protocol with SAML2 SSO";
              :     } else {
              :       LOG(INFO) << "External communication is not authenticated 
for hs2-http protocol";
              :     }
> I think we want to be able to have JWT as the only authentication method (w
Added code as suggested.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/transport/THttpServer.cpp
File be/src/transport/THttpServer.cpp:

http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/transport/THttpServer.cpp@278
PS9, Line 278:         } else if (use_jwt_token_
             :             && stripped_bearer_auth_token.find('.') != 
string::npos) {
             :           // Since Impala supports multiple auth mechanism in 
parallel, falls back to JWT.
             :           if 
(callbacks_.jwt_token_auth_fn(stripped_bearer_auth_token)) {
             :             authorized = true;
             :             if (metrics_enabled_) {
             :               
http_metrics_->total_jwt_token_auth_success_->Increment(1);
             :             }
             :           }
> The other way to do this is to move the below JWT auth section above this "
Yes, this is more clean. Fixed as suggested.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util-test.cc
File be/src/util/jwt-util-test.cc:

http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util-test.cc@145
PS9, Line 145:   // Delete this temporary file
             :   void Delete();
> If this isn't used directly, then you can make it private.
Fixed.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util-test.cc@149
PS9, Line 149: const char* Filename() const { return name_.c_str(); }
> Since JsonWebKeySet::Init() takes the filename in as a "const std::string&"
fixed.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util-test.cc@169
PS9, Line 169: NULL
> Nit: Use nullptr rather than NULL for new code.
fixed.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util-test.cc@224
PS9, Line 224:   TempTestDataFile* jwks_file = new TempTestDataFile(
             :       "{"
             :       "  \"keys\": ["
             :       "    {"
             :       "      \"use\": \"sig\","
             :       "      \"kty\": \"RSA\","
             :       "      \"alg\": \"RS256\","
             :       "      \"n\": 
\"sttddbg-_yjXzcFpbMJB1fIFam9lQBeXWbTqzJwbuFbspHMsRowa8FaPw\","
             :       "      \"e\": \"AQAB\""
             :       "    }"
             :       "  ]"
             :       "}");
             :   JsonWebKeySet* jwks = new JsonWebKeySet();
> Here and elsewhere in this file, use smart pointers / std::unique_ptr where
Fixed.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util-test.cc@348
PS9, Line 348: TEST(JwtUtilTest, VerifyJwtRS512) {
             :   // Sign JWT token with RS512.
             :   TempTestDataFile jwks_file(Substitute(jwks_file_format, kid_1, 
"RS512", rsa512_pub_key,
             :       kid_2, "RS512", rsa512_pub_key_invalid));
             :   JsonWebKeySet* jwks = new JsonWebKeySet();
             :   Status status = jwks->Init(jwks_file.Filename());
             :   ASSERT_TRUE(status.ok());
             :   ASSERT_EQ(2, jwks->GetKeyNum());
             :
             :   const JsonWebKey* key1 = jwks->LookupKey(kid_1);
             :   ASSERT_TRUE(key1 != nullptr);
             :   ASSERT_EQ(ADD_PUB_KEY_HEADER_FOOTER(rsa512_pub_key), 
key1->key_value_);
             :
             :   // Create a JWT token and sign it with RS512.
             :   string priv_key = 
ADD_RSA_PRIV_KEY_HEADER_FOOTER(rsa512_priv_key);
             :   string pub_key = ADD_PUB_KEY_HEADER_FOOTER(rsa512_pub_key);
             :   auto token = jwt::create()
             :                    .set_issuer("auth0")
             :                    .set_type("JWS")
             :                    .set_algorithm("RS512")
             :                    .set_key_id(kid_1)
             :                    .set_payload_claim("username", 
picojson::value("impala"))
             :                    .sign(jwt::algorithm::rs512(pub_key, 
priv_key, "", ""));
             :
             :   // Verify the JWT token with our wrapper API which use public 
key retrieved from JWKS,
             :   // and read username from the token.
             :   string username;
             :   status = VerifyJWTTokenExtractUsername(token, jwks, 
"username", username);
             :   ASSERT_TRUE(status.ok());
             :   ASSERT_EQ("impala", username);
             :
             :   delete jwks;
             : }
> For completeness, let's also add a similar case with RS384
Added test case for RS384.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util-test.cc@456
PS9, Line 456:   ASSERT_TRUE(status.ok());
> When checking that status is ok, use ASSERT_OK/EXPECT_OK from testutil/gtes
Fixed as suggested


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc
File be/src/util/jwt-util.cc:

http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@103
PS9, Line 103:     if (!keys.IsArray()) {
             :       return Status(TErrorCode::JWKS_PARSE_ERROR,
             :           Substitute(
             :               "'keys' must be of type Array but is a '$0'", 
NameOfTypeOfJsonValue(keys)));
             :     }
> Should we add an additional check that the array has non-zero Size()?
Done


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@131
PS9, Line 131:     for (Value::ConstMemberIterator member = 
json_key.MemberBegin();
             :          member != json_key.MemberEnd(); ++member) {
> For this for loop, you might just insert all the values into an unordered_m
Done


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@133
PS9, Line 133:      if (strcmp("use", member->name.GetString()) == 0) {
             :         RETURN_IF_ERROR(ReadKeyProperty("use", json_key, 
&key_use, /*required*/ false));
> It sounds like this is optional, so maybe we don't need to require a specif
Right, it's optional. We don't need to save it.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@133
PS9, Line 133:      if (strcmp("use", member->name.GetString()) == 0) {
             :         RETURN_IF_ERROR(ReadKeyProperty("use", json_key, 
&key_use, /*required*/ false));
> Do we care about what "use" is set to? Do we want to require any specific v
No, remove it.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@146
PS9, Line 146: else if (strcmp("n", member->name.GetString()) == 0) {
             :         found_kvalue = true;
             :         RETURN_IF_ERROR(ReadKeyProperty("n", json_key, 
&key_value));
             :         if (key_value.empty()) {
             :           return Status("'n' property must be a non-empty 
string");
             :         }
> This works for RSA. Other encryption types like elliptic curve require othe
Right, added comments for other algorithms.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@204
PS9, Line 204: NULL
> Use "nullptr" in new code rather than NULL
fixed


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@249
PS9, Line 249: VerifyJWTTokenExtractUsername
> From a style point of view, I think I would split up this function. I would
Added a class with the suggested member functions.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@282
PS9, Line 282:       if (strncasecmp(algorithm.c_str(), 
jwk->algorithm_.c_str(), algorithm.length())
             :           != 0) {
> To be careful, compare the lengths as well to make sure this isn't just mat
Done


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@290
PS9, Line 290:      if (strncasecmp(algorithm.c_str(), "rs256", 
algorithm.length()) == 0) {
             :         jwt::verify()
             :             
.allow_algorithm(jwt::algorithm::rs256(jwk->key_value_, "", "", ""))
             :             .with_issuer(issuer)
             :             .verify(decoded_token);
             :       } else if (strncasecmp(algorithm.c_str(), "rs384", 
algorithm.length()) == 0) {
             :         jwt::verify()
             :             
.allow_algorithm(jwt::algorithm::rs384(jwk->key_value_, "", "", ""))
             :             .with_issuer(issuer)
             :             .verify(decoded_token);
             :       } else if (strncasecmp(algorithm.c_str(), "rs512", 
algorithm.length()) == 0) {
             :         jwt::verify()
             :             
.allow_algorithm(jwt::algorithm::rs512(jwk->key_value_, "", "", ""))
             :             .with_issuer(issuer)
             :             .verify(decoded_token);
> One performance thing we may want to do is to construct the verifier once p
Changed code to create one verifier.


http://gerrit.cloudera.org:8080/#/c/17435/9/be/src/util/jwt-util.cc@312
PS9, Line 312:     // Get value of custom claim 'username' from the token 
payload.
             :     if 
(decoded_token.has_payload_claim(jwt_custom_claim_username)) {
             :       // Assume the claim data type of 'username' is string.
             :       username.assign(
             :           
decoded_token.get_payload_claim(jwt_custom_claim_username).to_json().to_str());
             :     }
> What happens if the username claim is not present? Should we return an erro
Changed code to return error.


http://gerrit.cloudera.org:8080/#/c/17435/9/fe/src/test/java/org/apache/impala/customcluster/LdapHS2Test.java
File fe/src/test/java/org/apache/impala/customcluster/LdapHS2Test.java:

http://gerrit.cloudera.org:8080/#/c/17435/9/fe/src/test/java/org/apache/impala/customcluster/LdapHS2Test.java@421
PS9, Line 421:   /**
             :    * Tests if sessions are authenticated by verifying the JWT 
token for connections
             :    * to the HTTP hiveserver2 endpoint.
             :    * Since we don't have Java version of JWT library, we use 
pre-calculated JWT token
             :    * and JWKS. The token and JWK set used in this test case were 
generated by using
             :    * BE unit-test function JwtUtilTest::VerifyJwtRS256.
             :    */
             :   @Test
             :   public void testHiveserver2JwtAuth() throws Exception {
             :     String jwksFilename =
             :         new File(System.getenv("IMPALA_HOME"), 
"testdata/jwt/jwks_rs256.json").getPath();
             :     setUp(String.format(
             :         "--jwt_token_auth=true --jwt_validate_signature=true 
--jwks_file_path=%s "
             :             + "--jwt_allow_without_tls=true",
             :         jwksFilename));
             :     verifyMetrics(0, 0);
             :     THttpClient transport = new 
THttpClient("http://localhost:28000";);
             :     Map<String, String> headers = new HashMap<String, String>();
             :
             :     // Case 1: Authenticate with valid JWT Token in HTTP header.
             :     String jwtToken =
             :         
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
             :         + 
"NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
             :         + 
"W1wYWxhIn0.OW5H2SClLlsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI"
             :         + 
"02W69TZNat7DYcC86A_ogLMfNXagHjlMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU55"
             :         + 
"4kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgc"
             :         + 
"bZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO25b5n4fryfKasSgaZfmk0C"
             :         + 
"oOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKsPk0bg";
             :     headers.put("Authorization", "Bearer " + jwtToken);
             :     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 'impala' from JWT 
token and use it as
             :     // login user.
             :     TOpenSessionReq openReq = new TOpenSessionReq();
             :     TOpenSessionResp openResp = client.OpenSession(openReq);
             :     // One successful authentication.
             :     verifyMetrics(0, 0);
             :     verifyJwtAuthMetrics(1, 0);
             :     // Running a query should succeed.
             :     TOperationHandle operationHandle = execAndFetch(
             :         client, openResp.getSessionHandle(), "select 
logged_in_user()", "impala");
             :     // Two more successful authentications - for the Exec() and 
the Fetch().
             :     verifyMetrics(0, 0);
             :     verifyJwtAuthMetrics(3, 0);
             :
             :     // case 2: Authenticate fails with invalid JWT token which 
does not have signature.
             :     String invalidJwtToken =
             :         
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
             :         + 
"NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
             :         + "W1wYWxhIn0.";
             :     headers.put("Authorization", "Bearer " + invalidJwtToken);
             :     headers.put("X-Forwarded-For", "127.0.0.1");
             :     transport.setCustomHeaders(headers);
             :     try {
             :       openResp = client.OpenSession(openReq);
             :       fail("Exception exception.");
             :     } catch (Exception e) {
             :       verifyJwtAuthMetrics(3, 1);
             :       assertEquals(e.getMessage(), "HTTP Response code: 401");
             :     }
             :   }
> These new test cases are testing LDAP + JWT. LdapHS2Test::setUp() includes
Tried to add tests in Python custom cluster tests, but did not work. I will use 
LDAP library and move the JWT test cases as new Java file. Will try Python 
custom cluster later.


http://gerrit.cloudera.org:8080/#/c/17435/9/fe/src/test/java/org/apache/impala/customcluster/LdapWebserverTest.java
File fe/src/test/java/org/apache/impala/customcluster/LdapWebserverTest.java:

http://gerrit.cloudera.org:8080/#/c/17435/9/fe/src/test/java/org/apache/impala/customcluster/LdapWebserverTest.java@276
PS9, Line 276:   /**
             :    * Tests if sessions are authenticated by verifying the JWT 
token for connections
             :    * to the Web Server.
             :    * Since we don't have Java version of JWT library, we use 
pre-calculated JWT token
             :    * and JWKS. The token and JWK set used in this test case were 
generated by using
             :    * BE unit-test function JwtUtilTest::VerifyJwtRS256.
             :    */
             :   @Test
             :   public void testWebserverJwtAuth() throws Exception {
             :     String jwksFilename =
             :         new File(System.getenv("IMPALA_HOME"), 
"testdata/jwt/jwks_rs256.json").getPath();
             :     setUp(String.format(
             :               "--jwt_token_auth=true 
--jwt_validate_signature=true --jwks_file_path=%s "
             :                   + "--jwt_allow_without_tls=true",
             :               jwksFilename),
             :         "");
             :
             :     // Case 1: Authenticate with valid JWT Token in HTTP header.
             :     String jwtToken =
             :         
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
             :         + 
"NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
             :         + 
"W1wYWxhIn0.OW5H2SClLlsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI"
             :         + 
"02W69TZNat7DYcC86A_ogLMfNXagHjlMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU55"
             :         + 
"4kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgc"
             :         + 
"bZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO25b5n4fryfKasSgaZfmk0C"
             :         + 
"oOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKsPk0bg";
             :     attemptConnection("Bearer " + jwtToken, "127.0.0.1");
             :     verifyJwtAuthMetrics(Range.closed(1L, 1L), zero);
             :
             :     // Case 2: Failed with invalid JWT Token.
             :     String invalidJwtToken =
             :         
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
             :         + 
"NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
             :         + "W1wYWxhIn0.";
             :     try {
             :       attemptConnection("Bearer " + invalidJwtToken, 
"127.0.0.1");
             :     } catch (IOException e) {
             :       assertTrue(e.getMessage().contains("Server returned HTTP 
response code: 401"));
             :     }
             :     verifyJwtAuthMetrics(Range.closed(1L, 1L), Range.closed(1L, 
1L));
             :   }
> Same comment as on the other Java LDAP test file. It would be nice to split
Tried to add tests in Python custom cluster tests, but did not work. I will use 
LDAP library and move the JWT test cases as new Java file.



--
To view, visit http://gerrit.cloudera.org:8080/17435
To unsubscribe, visit http://gerrit.cloudera.org:8080/settings

Gerrit-Project: Impala-ASF
Gerrit-Branch: master
Gerrit-MessageType: comment
Gerrit-Change-Id: I6b71fa854c9ddc8ca882878853395e1eb866143c
Gerrit-Change-Number: 17435
Gerrit-PatchSet: 9
Gerrit-Owner: Wenzhe Zhou <wz...@cloudera.com>
Gerrit-Reviewer: Andrew Sherman <asher...@cloudera.com>
Gerrit-Reviewer: Impala Public Jenkins <impala-public-jenk...@cloudera.com>
Gerrit-Reviewer: Joe McDonnell <joemcdonn...@cloudera.com>
Gerrit-Reviewer: Wenzhe Zhou <wz...@cloudera.com>
Gerrit-Comment-Date: Wed, 16 Jun 2021 01:03:06 +0000
Gerrit-HasComments: Yes

Reply via email to