You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hive.apache.org by ng...@apache.org on 2022/03/24 17:38:31 UTC
[hive] branch master updated: HIVE-25575: Add support for JWT authentication in HTTP mode (Yu-Wen Lai reviewed by Sourabh Goyal)
This is an automated email from the ASF dual-hosted git repository.
ngangam pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new c3fa88a HIVE-25575: Add support for JWT authentication in HTTP mode (Yu-Wen Lai reviewed by Sourabh Goyal)
c3fa88a is described below
commit c3fa88a1b7d1475f44383fca913aecf9c664bab0
Author: Yu-Wen Lai <yu...@cloudera.com>
AuthorDate: Fri Aug 27 01:32:54 2021 -0700
HIVE-25575: Add support for JWT authentication in HTTP mode (Yu-Wen Lai reviewed by Sourabh Goyal)
HS2 side change:
* Fetches JWKS from a URL into memory while HS2 starts up.
* Accepts JWT in Authorization: Bearer header and verifies it with JWKS
JDBC driver side change:
* JDBC client can pick up JWT from env variable
* JDBC client can accept JWT in JDBC url
* sends JWT in authorization header
Test:
mvn test -Dtest=org.apache.hive.service.auth.jwt.TestHttpJwtAuthentication
Co-authored-by: Shubham Chaurasia <sc...@cloudera.com>
---
.../java/org/apache/hadoop/hive/conf/HiveConf.java | 11 +-
itests/hive-unit/pom.xml | 6 +
.../auth/jwt/TestHttpJwtAuthentication.java | 223 +++++++++++++++++++++
.../resources/auth.jwt/jwt-authorized-key.json | 12 ++
.../resources/auth.jwt/jwt-unauthorized-key.json | 12 ++
.../resources/auth.jwt/jwt-verification-jwks.json | 20 ++
.../java/org/apache/hive/jdbc/HiveConnection.java | 45 +++++
jdbc/src/java/org/apache/hive/jdbc/Utils.java | 3 +
.../jdbc/jwt/HttpJwtAuthRequestInterceptor.java | 49 +++++
.../jdbc/saml/HttpSamlAuthRequestInterceptor.java | 4 +-
service/pom.xml | 6 +
.../hive/service/auth/HiveAuthConstants.java | 3 +-
.../apache/hive/service/auth/HttpAuthUtils.java | 1 +
.../apache/hive/service/auth/jwt/JWTValidator.java | 111 ++++++++++
.../service/auth/jwt/URLBasedJWKSProvider.java | 81 ++++++++
.../hive/service/cli/thrift/ThriftHttpServlet.java | 62 ++++--
16 files changed, 632 insertions(+), 17 deletions(-)
diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
index ce8ca7b..3b42210 100644
--- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
+++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
@@ -4122,7 +4122,7 @@ public class HiveConf extends Configuration {
// HiveServer2 auth configuration
HIVE_SERVER2_AUTHENTICATION("hive.server2.authentication", "NONE",
- new StringSet("NOSASL", "NONE", "LDAP", "KERBEROS", "PAM", "CUSTOM", "SAML"),
+ new StringSet("NOSASL", "NONE", "LDAP", "KERBEROS", "PAM", "CUSTOM", "SAML", "JWT"),
"Client authentication types.\n" +
" NONE: no authentication check\n" +
" LDAP: LDAP/AD based authentication\n" +
@@ -4131,7 +4131,9 @@ public class HiveConf extends Configuration {
" (Use with property hive.server2.custom.authentication.class)\n" +
" PAM: Pluggable authentication module\n" +
" NOSASL: Raw transport\n" +
- " SAML: SAML 2.0 compliant authentication. This is only supported in http transport mode."),
+ " SAML: SAML 2.0 compliant authentication. This is only supported in http transport mode.\n" +
+ " JWT: JWT based authentication. HS2 expects JWT contains the user name as subject and was signed by an\n" +
+ " asymmetric key. This is only supported in http transport mode."),
HIVE_SERVER2_TRUSTED_DOMAIN("hive.server2.trusted.domain", "",
"Specifies the host or a domain to trust connections from. Authentication is skipped " +
"for any connection coming from a host whose hostname ends with the value of this" +
@@ -4236,6 +4238,10 @@ public class HiveConf extends Configuration {
HIVE_SERVER2_PAM_SERVICES("hive.server2.authentication.pam.services", null,
"List of the underlying pam services that should be used when auth type is PAM\n" +
"A file with the same name must exist in /etc/pam.d"),
+ // JWT Auth configs
+ HIVE_SERVER2_AUTHENTICATION_JWT_JWKS_URL("hive.server2.authentication.jwt.jwks.url", "",
+ "URL of the file from where URLBasedJWKSProvider will try to load JWKS if JWT is enabled for the\n" +
+ "authentication mode."),
// HS2 SAML2.0 configuration
HIVE_SERVER2_SAML_KEYSTORE_PATH("hive.server2.saml2.keystore.path", "",
@@ -4301,6 +4307,7 @@ public class HiveConf extends Configuration {
HIVE_SERVER2_SAML_GROUP_FILTER("hive.server2.saml2.group.filter", "",
"Comma separated list of group names which will be allowed when SAML\n"
+ " authentication is enabled."),
+
HIVE_SERVER2_ENABLE_DOAS("hive.server2.enable.doAs", true,
"Setting this property to true will have HiveServer2 execute\n" +
"Hive operations as the user making the calls to it."),
diff --git a/itests/hive-unit/pom.xml b/itests/hive-unit/pom.xml
index 92d4b4f..042924c 100644
--- a/itests/hive-unit/pom.xml
+++ b/itests/hive-unit/pom.xml
@@ -185,6 +185,12 @@
</dependency>
<!-- test inter-project -->
<dependency>
+ <groupId>com.github.tomakehurst</groupId>
+ <artifactId>wiremock-jre8-standalone</artifactId>
+ <version>2.32.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
diff --git a/itests/hive-unit/src/test/java/org/apache/hive/service/auth/jwt/TestHttpJwtAuthentication.java b/itests/hive-unit/src/test/java/org/apache/hive/service/auth/jwt/TestHttpJwtAuthentication.java
new file mode 100644
index 0000000..202ff0d
--- /dev/null
+++ b/itests/hive-unit/src/test/java/org/apache/hive/service/auth/jwt/TestHttpJwtAuthentication.java
@@ -0,0 +1,223 @@
+/*
+ * 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.hive.service.auth.jwt;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.apache.hive.jdbc.HiveConnection;
+import org.apache.hive.jdbc.Utils;
+import org.apache.hive.jdbc.miniHS2.MiniHS2;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+public class TestHttpJwtAuthentication {
+ private static final Map<String, String> DEFAULTS = new HashMap<>(System.getenv());
+ private static Map<String, String> envMap;
+
+ private static final File jwtAuthorizedKeyFile =
+ new File("src/test/resources/auth.jwt/jwt-authorized-key.json");
+ private static final File jwtUnauthorizedKeyFile =
+ new File("src/test/resources/auth.jwt/jwt-unauthorized-key.json");
+ private static final File jwtVerificationJWKSFile =
+ new File("src/test/resources/auth.jwt/jwt-verification-jwks.json");
+
+ public static final String USER_1 = "USER_1";
+
+ private static MiniHS2 miniHS2;
+
+ private static final int MOCK_JWKS_SERVER_PORT = 8089;
+ @ClassRule
+ public static final WireMockRule MOCK_JWKS_SERVER = new WireMockRule(MOCK_JWKS_SERVER_PORT);
+
+ /**
+ * This is a hack to make environment variables modifiable.
+ * Ref: https://stackoverflow.com/questions/318239/how-do-i-set-environment-variables-from-java.
+ */
+ @BeforeClass
+ public static void makeEnvModifiable() throws Exception {
+ envMap = new HashMap<>();
+ Class<?> envClass = Class.forName("java.lang.ProcessEnvironment");
+ Field theEnvironmentField = envClass.getDeclaredField("theEnvironment");
+ Field theUnmodifiableEnvironmentField = envClass.getDeclaredField("theUnmodifiableEnvironment");
+ removeStaticFinalAndSetValue(theEnvironmentField, envMap);
+ removeStaticFinalAndSetValue(theUnmodifiableEnvironmentField, envMap);
+ }
+
+ private static void removeStaticFinalAndSetValue(Field field, Object value) throws Exception {
+ field.setAccessible(true);
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+ field.set(null, value);
+ }
+
+ @Before
+ public void initEnvMap() {
+ envMap.clear();
+ envMap.putAll(DEFAULTS);
+ }
+
+ @BeforeClass
+ public static void setupHS2() throws Exception {
+ MOCK_JWKS_SERVER.stubFor(get("/jwks")
+ .willReturn(ok()
+ .withBody(Files.readAllBytes(jwtVerificationJWKSFile.toPath()))));
+
+ HiveConf conf = new HiveConf();
+ conf.setBoolVar(ConfVars.HIVE_SUPPORT_CONCURRENCY, false);
+ conf.setBoolVar(ConfVars.HIVE_SERVER2_LOGGING_OPERATION_ENABLED, false);
+ conf.setBoolVar(ConfVars.HIVESTATSCOLAUTOGATHER, false);
+ conf.setVar(ConfVars.HIVE_SERVER2_AUTHENTICATION, "JWT");
+ // the content of the URL below is the same as jwtVerificationJWKSFile
+ conf.setVar(ConfVars.HIVE_SERVER2_AUTHENTICATION_JWT_JWKS_URL, "http://localhost:" + MOCK_JWKS_SERVER_PORT +
+ "/jwks");
+ miniHS2 = new MiniHS2.Builder().withConf(conf).withHTTPTransport().build();
+
+ miniHS2.start(new HashMap<>());
+ }
+
+ @AfterClass
+ public static void stopServices() throws Exception {
+ if (miniHS2 != null && miniHS2.isStarted()) {
+ miniHS2.stop();
+ miniHS2.cleanup();
+ miniHS2 = null;
+ MiniHS2.cleanupLocalDir();
+ }
+ }
+
+ @Test
+ public void testAuthorizedUser() throws Exception {
+ String jwt = generateJWT(USER_1, jwtAuthorizedKeyFile.toPath(), TimeUnit.MINUTES.toMillis(5));
+ HiveConnection connection = getConnection(jwt, true);
+ assertLoggedInUser(connection, USER_1);
+ connection.close();
+
+ connection = getConnection(jwt, false);
+ assertLoggedInUser(connection, USER_1);
+ connection.close();
+ }
+
+ @Test(expected = SQLException.class)
+ public void testExpiredJwt() throws Exception {
+ String jwt = generateJWT(USER_1, jwtAuthorizedKeyFile.toPath(), 1);
+ Thread.sleep(1);
+ HiveConnection connection = getConnection(jwt, true);
+ }
+
+ @Test(expected = SQLException.class)
+ public void testUnauthorizedUser() throws Exception {
+ String unauthorizedJwt = generateJWT(USER_1, jwtUnauthorizedKeyFile.toPath(), TimeUnit.MINUTES.toMillis(5));
+ HiveConnection connection = getConnection(unauthorizedJwt, true);
+ }
+
+ @Test(expected = SQLException.class)
+ public void testWithoutJwtProvided() throws Exception {
+ HiveConnection connection = getConnection(null, true);
+ }
+
+ private HiveConnection getConnection(String jwt, Boolean putJwtInEnv) throws Exception {
+ String url = getJwtJdbcConnectionUrl();
+ if (jwt != null && putJwtInEnv) {
+ System.getenv().put(Utils.JdbcConnectionParams.AUTH_JWT_ENV, jwt);
+ } else if (jwt != null) {
+ url += "jwt=" + jwt;
+ }
+ Class.forName("org.apache.hive.jdbc.HiveDriver");
+ Connection connection = DriverManager.getConnection(url, null, null);
+ return (HiveConnection) connection;
+ }
+
+ private String generateJWT(String user, Path keyFile, long lifeTimeMillis) throws Exception {
+ RSAKey rsaKeyPair = RSAKey.parse(new String(java.nio.file.Files.readAllBytes(keyFile), StandardCharsets.UTF_8));
+
+ // Create RSA-signer with the private key
+ JWSSigner signer = new RSASSASigner(rsaKeyPair);
+
+ JWSHeader header = new JWSHeader
+ .Builder(JWSAlgorithm.RS256)
+ .keyID(rsaKeyPair.getKeyID())
+ .build();
+
+ Date now = new Date();
+ Date expirationTime = new Date(now.getTime() + lifeTimeMillis);
+ JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ .jwtID(UUID.randomUUID().toString())
+ .issueTime(now)
+ .issuer("auth-server")
+ .subject(user)
+ .expirationTime(expirationTime)
+ .claim("custom-claim-or-payload", "custom-claim-or-payload")
+ .build();
+
+ SignedJWT signedJWT = new SignedJWT(header, claimsSet);
+
+ // Compute the RSA signature
+ signedJWT.sign(signer);
+
+ return signedJWT.serialize();
+ }
+
+ private String getJwtJdbcConnectionUrl() throws Exception {
+ return miniHS2.getHttpJdbcURL() + "auth=jwt;";
+ }
+
+ private void assertLoggedInUser(HiveConnection connection, String expectedUser)
+ throws SQLException {
+ Statement stmt = connection.createStatement();
+ ResultSet resultSet = stmt.executeQuery("select logged_in_user()");
+ assertTrue(resultSet.next());
+ String loggedInUser = resultSet.getString(1);
+ assertEquals(expectedUser, loggedInUser);
+ }
+}
diff --git a/itests/hive-unit/src/test/resources/auth.jwt/jwt-authorized-key.json b/itests/hive-unit/src/test/resources/auth.jwt/jwt-authorized-key.json
new file mode 100644
index 0000000..b5b4fb4
--- /dev/null
+++ b/itests/hive-unit/src/test/resources/auth.jwt/jwt-authorized-key.json
@@ -0,0 +1,12 @@
+{
+ "p": "-8lxjB9JZA44XBLLVGnY20x28uT8NQ1BlbqI0Tlr96An4B_PzgPL5_bFFB7SWs8ehSWn9z2SJfClhQpBLfy-2mXvJek_xgibESIlPXqY9Qrg7-PhRmPs3whyiIsnn8tpPMm2XJ_4n0Y-Yfx4nwErGdy84LiKFMDXPEk2a7ndYWs",
+ "kty": "RSA",
+ "q": "0YAcTLBnTrSUiciE0lliIkAidW0TnHP48v-vJitLEz0d8mlTZ_aeOQJm6CUOqF7BqQv3Z8OK_HYKXfOr7xzUlfROONybUXRFE0LvT5Fjvrq-56QGB6GeFq5i6HKlRcC_8TD6WwUJWIzeYuPqhp_FYIpT4ds131d5VYPKDCdY_dM",
+ "d": "VsxW72idEAtoZQDphvxJ0t54EyRfcIJVB9BZuqnyNTfH-VsaUO3st86w_PMU_i0lmyIc8dkCmwOb8R2pRXDo6UxEYUe5YfBnvn9iYF3Ll2QfPOKfZhDBOfqSjEb1po20is7mXTQORBv3bhSo664pasHItTwDz-KKI-FiIu_PYq0lYihuaedUUMp3MQTvDFulpFWEKzqseBDat07BholvxjzlnBK-Ez3KI9qGH8VIIk5TGW5pVu3cQe1WC8NJOe3xR9vu7XX6xvhVLPP7fvKiXJWJ_I_SagAhR1JW0uDJl_b0CrYYeVUnt_pzvW1BeJGz7ysCXcHlLBUh72XrpW-O7Q",
+ "e": "AQAB",
+ "kid": "123",
+ "qi": "9yk0mg4LY48YS8cvG51wMVfKfEjSbt2ygKxqabdsP-qSVpz-KVJtCmbKa57jm2BaMV_mRBQFodxu4XN58VGsj5MzXC5Jb_CkLeQfkp6ZKvehZhiJn3HF0Kb19u9xPvKDclHpKl-UMM1Pcu8Ww52DOyOYcHa1_SLZ05CcOWvMkS8",
+ "dp": "HYtToYeCSxVIE7W42hzZb1IXmwS3e1ok2fbbWwGL47CNPUU-UwQrBvrzwRqkwDcRc7opbV9yKLWGFohPgZ_onSPc3evyqcAUwfvptr8N96LhJgTtSB8tijYpilAZxCxQGuvoVBIJUFcjtsezN6Uhc5VtLEk7GphOKSrGEfnrOiU",
+ "dq": "tF2uf5v0JT-1DnazW4IWydQblqtlEfKKp3LX8W2egh7BNJ3XcA9UI1LdFAord2u1IXwq8YvZkgdyX3bVVNSmdb_SxIOxuMv4WF_tNry-eku-5iFCC7nqKC7U-rkRb19GIToAoPJSHImTQOJmXKcbQEV3eGDJHdLqpGQFRLdvl38",
+ "n": "zg12QaFTsez1EijOYRFzNZdowOt79ePqxCMQ-EEHynUhEZ6TIDnXfjWfuWocS1qRRglUUbHerEtmACUKPQShaG8uL0ZXiLqDr2QSuqrTtr2VUGesxZc6GiqkZlnWFNu5kSUvtemcKxWl8OLFf-5kNnGW4_4xM6BIwosYZnddfFqQT5IP6iTMZIUIKXxY4s1dadYRIiMteNutro67fhOLKabHkyC6ILE6f6VZsYbb_NXC5yC--7DiC2GYKzy7TKmaczuDfQZVgVY-nL9kTPIdhf334EYHQfYmLdvLc56g8-cxY3xh2GnwAj1JcT2u3hsS4KS05bUFHFnveO5uxIYKMQ"
+}
\ No newline at end of file
diff --git a/itests/hive-unit/src/test/resources/auth.jwt/jwt-unauthorized-key.json b/itests/hive-unit/src/test/resources/auth.jwt/jwt-unauthorized-key.json
new file mode 100644
index 0000000..f4845de
--- /dev/null
+++ b/itests/hive-unit/src/test/resources/auth.jwt/jwt-unauthorized-key.json
@@ -0,0 +1,12 @@
+{
+ "p": "wvzuDSY6dIsIJB0UM5BIncN6ui5ee-KHpCmBhh_ia2iX3DluQODEgITw7gDATTDdQsBD-nJLjrqUs5g5Gmt0UgZucXQ5PCt1CK6dLEZCaLivw2fsHYvOKeTkdA49wqLkTc8pkfQs09N-b6NspDDqVJPFffBvFpR_IBFay-xKa5k",
+ "kty": "RSA",
+ "q": "sQzza69VkEmgUm50pEGjgu-OxugOrjcHrjQ42A23YVwAAJ90qPNQa62O7dv5oWmSX2PJ7TgjkzbvtTycLfT_vUeapwfCcJe4WoDg54xF3E35yBvBIwReRiavxf5nWsHEtd5kBg6wRIndGwGUBE91xaLg21spjH7nQKtG9vKeNM8",
+ "d": "UbiPIpr7agQqpM3ERfaXsKNMETyBrIYr3yoggHQ7XQkSPepCgRhE86puRmjf76FtZ3RwpJwjLfO6Ap0fIE9LXXE8otTF9sMnC9fe7odHkEu61Wr3aQM-53dgZoJL7XU53LOo0cNO44SBbw11d2cYlAR3KuCEK7bCLMBOkK1gdxVpgDC7DgxVgnP39bUlf4fA5gQeT5nNGnCWTV4jMVWCyEb0Ck5CvGJp1cCKaMSEvV4j6AM72EkAn8PogTSOJpurRJaTky0De7-ncT2Sv5DCuOIkMhsHqayLbm7a84ORHqsnWpZV85WVW-xxiivkVpqtSDRKCI94pMa9DWszjNJW8Q",
+ "e": "AQAB",
+ "kid": "sig-1642039368",
+ "qi": "CXP_tewCHyXk6PNDcbI0wtXsaWJryOJfMsc7roBCoOwDbTekUFXhOfRmFX5ZTNetRNDpw9nNiQDXt8pyw7UZ-0EhD1cLst1slS__hBi5QEAGo9cUxl3RGeMAFtY9O8B1gjFyKkG5BzdddGBKGQT3Tg23Eyzn6EA_NCw4XAKnkwQ",
+ "dp": "aAdzphZQN595n3LYNU50P59sWeqlRCkuvvnZ_coDDdUGuFr3pKuGix7iP8is0EISuitD2VmjUCnhbhP3202bCKwfvm4Inz58OT6X4mg1xBNMys8mHPla6-UPsY9rie1IKu8suY7xX65FlaA2NT9XtfoE8tUVH5HoZR59N7EAX3k",
+ "dq": "mTkZDO-fgBCH4-7dmS2JIY7KpI897T2IsxVUwH4WXvastd1Jq9FuntGEKYu_HRbtawpEPbzg5M2dY97BVvB5xshKKhWIC8Lx87knapw19XOyIKEMY46rO9DNO-9waNXatH5zV96sY5RgOrgB7j0KMnFEYfIiIgnNfmT8NElB63c",
+ "n": "htq92ltGQrZv19TlhluoqmXjjRXw_NWEd0nPZsWrbLnr8lZ-gOxsjIsDMjb5HNDNmuAS7pg2d_o5ZZAY1sSjKf_EuUPZN-MOej8ZBOtrMxEH7e_t37kYIbbJSuzt55poZdRli6BE8CVDesS4W-wsFZ0MvUazAUADh3onARN7Arf3jwknm5CLafE_JzKrNKZadBElEFEAEu5y9n_SuTlemw3P81lOVmZmjGjfqtPx01O5aV_truMjrQa3NUivu1ihrjvJl0xc3rwJe7qDrfEqgvpBQ-vrAsvg3Jiz5Idj6cU3J0hNtV4ixYxcDQecNlgR7gBeIp3E8BXL1kGOOHYUtw"
+}
\ No newline at end of file
diff --git a/itests/hive-unit/src/test/resources/auth.jwt/jwt-verification-jwks.json b/itests/hive-unit/src/test/resources/auth.jwt/jwt-verification-jwks.json
new file mode 100644
index 0000000..a6fd935
--- /dev/null
+++ b/itests/hive-unit/src/test/resources/auth.jwt/jwt-verification-jwks.json
@@ -0,0 +1,20 @@
+{
+ "keys": [
+ {
+ "kty": "RSA",
+ "e": "AQAB",
+ "alg": "RS256",
+ "kid": "819d1e61429dd3d3caef129c0ac2bae8c6d46fbc",
+ "use": "sig",
+ "n": "qfR12Bcs_hSL0Y1fN5TYZeUQIFmuVRYa210na81BFj91xxwtICJY6ckZCI3Jf0v2tPLOT_iKVk4WBCZ7AVJVvZqHuttkyrdFROMVTe6DwmcjbbkgACMVildTnHy9xy2KuX-OZsEYzgHuRgfe_Y-JN6LoxBYZx6VoBLpgK-F0Q-0O_bRgZhHifVG4ZzARjhgz0PvBb700GtOTHS6mQIfToPErbgqcowKN9k-mJqJr8xpXSHils-Yw97LHjICZmvA5B8EPNW28DwFOE5JrsPcyrFKOAYl4NcSYQgjl-17TWE5_tFdZ8Lz-srjiPMoHlBjZD1C7aO03LI-_9u8lVsktMw"
+ },
+ {
+ "kty": "RSA",
+ "e": "AQAB",
+ "alg": "RS256",
+ "kid": "123",
+ "use": "sig",
+ "n": "zg12QaFTsez1EijOYRFzNZdowOt79ePqxCMQ-EEHynUhEZ6TIDnXfjWfuWocS1qRRglUUbHerEtmACUKPQShaG8uL0ZXiLqDr2QSuqrTtr2VUGesxZc6GiqkZlnWFNu5kSUvtemcKxWl8OLFf-5kNnGW4_4xM6BIwosYZnddfFqQT5IP6iTMZIUIKXxY4s1dadYRIiMteNutro67fhOLKabHkyC6ILE6f6VZsYbb_NXC5yC--7DiC2GYKzy7TKmaczuDfQZVgVY-nL9kTPIdhf334EYHQfYmLdvLc56g8-cxY3xh2GnwAj1JcT2u3hsS4KS05bUFHFnveO5uxIYKMQ"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java b/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
index 5fcad06..abc5438 100644
--- a/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
+++ b/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
@@ -65,6 +65,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
@@ -88,6 +89,7 @@ import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier;
+import org.apache.hive.jdbc.jwt.HttpJwtAuthRequestInterceptor;
import org.apache.hive.jdbc.saml.HiveJdbcBrowserClientFactory;
import org.apache.hive.jdbc.saml.HiveJdbcSamlRedirectStrategy;
import org.apache.hive.jdbc.saml.HttpSamlAuthRequestInterceptor;
@@ -594,6 +596,12 @@ public class HiveConnection implements java.sql.Connection {
host, getServerHttpUrl(useSsl), loggedInSubject, cookieStore, cookieName,
useSsl, additionalHttpHeaders,
customCookies);
+ } else if (isJwtAuthMode()) {
+ final String signedJwt = getJWT();
+ Preconditions.checkArgument(signedJwt != null && !signedJwt.isEmpty(), "For jwt auth mode," +
+ " a signed jwt must be provided");
+ requestInterceptor = new HttpJwtAuthRequestInterceptor(signedJwt, cookieStore,
+ cookieName, useSsl, additionalHttpHeaders, customCookies);
} else if (isBrowserAuthMode()) {
requestInterceptor = new HttpSamlAuthRequestInterceptor(browserClient, cookieStore,
cookieName, useSsl, additionalHttpHeaders, customCookies);
@@ -804,6 +812,38 @@ public class HiveConnection implements java.sql.Connection {
return httpClientBuilder.build();
}
+ private String getJWT() {
+ String jwtCredential = getJWTStringFromSession();
+ if (jwtCredential == null || jwtCredential.isEmpty()) {
+ jwtCredential = getJWTStringFromEnv();
+ }
+ return jwtCredential;
+ }
+
+ private String getJWTStringFromEnv() {
+ String jwtCredential = System.getenv(JdbcConnectionParams.AUTH_JWT_ENV);
+ if (jwtCredential == null || jwtCredential.isEmpty()) {
+ LOG.debug("No JWT is specified in env variable {}", JdbcConnectionParams.AUTH_JWT_ENV);
+ } else {
+ int startIndex = Math.max(0, jwtCredential.length() - 7);
+ String lastSevenChars = jwtCredential.substring(startIndex);
+ LOG.debug("Fetched JWT (ends with {}) from the env.", lastSevenChars);
+ }
+ return jwtCredential;
+ }
+
+ private String getJWTStringFromSession() {
+ String jwtCredential = sessConfMap.get(JdbcConnectionParams.AUTH_TYPE_JWT_KEY);
+ if (jwtCredential == null || jwtCredential.isEmpty()) {
+ LOG.debug("No JWT is specified in connection string.");
+ } else {
+ int startIndex = Math.max(0, jwtCredential.length() - 7);
+ String lastSevenChars = jwtCredential.substring(startIndex);
+ LOG.debug("Fetched JWT (ends with {}) from the session.", lastSevenChars);
+ }
+ return jwtCredential;
+ }
+
/**
* Create underlying SSL or non-SSL transport
*
@@ -1245,6 +1285,11 @@ public class HiveConnection implements java.sql.Connection {
.equals(sessConfMap.get(JdbcConnectionParams.AUTH_TYPE));
}
+ private boolean isJwtAuthMode() {
+ return JdbcConnectionParams.AUTH_TYPE_JWT.equalsIgnoreCase(sessConfMap.get(JdbcConnectionParams.AUTH_TYPE))
+ || sessConfMap.containsKey(JdbcConnectionParams.AUTH_TYPE_JWT_KEY);
+ }
+
/**
* This checks for {@code JdbcConnectionParams.AUTH_BROWSER_DISABLE_SSL_VALIDATION}
* on the connection url and returns the boolean value of it. Returns false if the
diff --git a/jdbc/src/java/org/apache/hive/jdbc/Utils.java b/jdbc/src/java/org/apache/hive/jdbc/Utils.java
index 0fd4820..764ae11 100644
--- a/jdbc/src/java/org/apache/hive/jdbc/Utils.java
+++ b/jdbc/src/java/org/apache/hive/jdbc/Utils.java
@@ -99,6 +99,9 @@ public class Utils {
public static final String AUTH_PASSWD = "password";
public static final String AUTH_KERBEROS_AUTH_TYPE = "kerberosAuthType";
public static final String AUTH_KERBEROS_AUTH_TYPE_FROM_SUBJECT = "fromSubject";
+ public static final String AUTH_TYPE_JWT = "jwt";
+ public static final String AUTH_TYPE_JWT_KEY = "jwt";
+ public static final String AUTH_JWT_ENV = "JWT";
// JdbcConnection param which specifies if we need to use a browser to do
// authentication.
// JdbcConnectionParam which specifies if the authMode is done via a browser
diff --git a/jdbc/src/java/org/apache/hive/jdbc/jwt/HttpJwtAuthRequestInterceptor.java b/jdbc/src/java/org/apache/hive/jdbc/jwt/HttpJwtAuthRequestInterceptor.java
new file mode 100644
index 0000000..51390c6
--- /dev/null
+++ b/jdbc/src/java/org/apache/hive/jdbc/jwt/HttpJwtAuthRequestInterceptor.java
@@ -0,0 +1,49 @@
+/*
+ * 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.hive.jdbc.jwt;
+
+import org.apache.hive.jdbc.HttpRequestInterceptorBase;
+import org.apache.hive.service.auth.HttpAuthUtils;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpRequest;
+import org.apache.http.client.CookieStore;
+import org.apache.http.protocol.HttpContext;
+
+import java.util.Map;
+
+/**
+ * This implements the logic to intercept the HTTP requests from the Hive Jdbc connection
+ * and adds JWT auth header.
+ */
+public class HttpJwtAuthRequestInterceptor extends HttpRequestInterceptorBase {
+
+ private final String signedJwt;
+
+ public HttpJwtAuthRequestInterceptor(String signedJwt, CookieStore cookieStore, String cn,
+ boolean isSSL, Map<String, String> additionalHeaders,
+ Map<String, String> customCookies) {
+ super(cookieStore, cn, isSSL, additionalHeaders, customCookies);
+ this.signedJwt = signedJwt;
+ }
+
+ @Override
+ protected void addHttpAuthHeader(HttpRequest httpRequest, HttpContext httpContext) {
+ httpRequest.addHeader(HttpHeaders.AUTHORIZATION, HttpAuthUtils.BEARER + " " + signedJwt);
+ }
+}
diff --git a/jdbc/src/java/org/apache/hive/jdbc/saml/HttpSamlAuthRequestInterceptor.java b/jdbc/src/java/org/apache/hive/jdbc/saml/HttpSamlAuthRequestInterceptor.java
index ad3d89d..430dc8d 100644
--- a/jdbc/src/java/org/apache/hive/jdbc/saml/HttpSamlAuthRequestInterceptor.java
+++ b/jdbc/src/java/org/apache/hive/jdbc/saml/HttpSamlAuthRequestInterceptor.java
@@ -21,6 +21,7 @@ package org.apache.hive.jdbc.saml;
import com.google.common.base.Preconditions;
import java.util.Map;
import org.apache.hive.jdbc.HttpRequestInterceptorBase;
+import org.apache.hive.service.auth.HttpAuthUtils;
import org.apache.hive.service.auth.saml.HiveSamlUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
@@ -37,7 +38,6 @@ import org.slf4j.LoggerFactory;
public class HttpSamlAuthRequestInterceptor extends HttpRequestInterceptorBase {
private final IJdbcBrowserClient browserClient;
- private static final String BEARER = "Bearer ";
private static final Logger LOG = LoggerFactory
.getLogger(HttpSamlAuthRequestInterceptor.class);
@@ -56,7 +56,7 @@ public class HttpSamlAuthRequestInterceptor extends HttpRequestInterceptorBase {
: browserClient.getServerResponse().getToken();
String clientIdentifier = browserClient.getClientIdentifier();
if (token != null && !token.isEmpty()) {
- httpRequest.addHeader(HttpHeaders.AUTHORIZATION, BEARER + token);
+ httpRequest.addHeader(HttpHeaders.AUTHORIZATION, HttpAuthUtils.BEARER + " " + token);
httpRequest.addHeader(HiveSamlUtils.SSO_CLIENT_IDENTIFIER, clientIdentifier);
httpRequest.removeHeaders(HiveSamlUtils.SSO_TOKEN_RESPONSE_PORT);
} else {
diff --git a/service/pom.xml b/service/pom.xml
index 6aca7e2..c2f2ccb 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -25,6 +25,7 @@
<name>Hive Service</name>
<properties>
<hive.path.to.root>..</hive.path.to.root>
+ <nimbus-jose-jwt.version>9.20</nimbus-jose-jwt.version>
</properties>
<dependencies>
<!-- dependencies are always listed in sorted order by groupId, artifactId -->
@@ -78,6 +79,11 @@
</dependency>
<!-- inter-project -->
<dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <version>${nimbus-jose-jwt.version}</version>
+ </dependency>
+ <dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
diff --git a/service/src/java/org/apache/hive/service/auth/HiveAuthConstants.java b/service/src/java/org/apache/hive/service/auth/HiveAuthConstants.java
index 0d71df0..f1a5114 100644
--- a/service/src/java/org/apache/hive/service/auth/HiveAuthConstants.java
+++ b/service/src/java/org/apache/hive/service/auth/HiveAuthConstants.java
@@ -26,7 +26,8 @@ public class HiveAuthConstants {
KERBEROS("KERBEROS"),
CUSTOM("CUSTOM"),
PAM("PAM"),
- SAML("SAML");
+ SAML("SAML"),
+ JWT("JWT");
private final String authType;
diff --git a/service/src/java/org/apache/hive/service/auth/HttpAuthUtils.java b/service/src/java/org/apache/hive/service/auth/HttpAuthUtils.java
index 31985d9..efaec8a 100644
--- a/service/src/java/org/apache/hive/service/auth/HttpAuthUtils.java
+++ b/service/src/java/org/apache/hive/service/auth/HttpAuthUtils.java
@@ -48,6 +48,7 @@ public final class HttpAuthUtils {
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
public static final String AUTHORIZATION = "Authorization";
public static final String BASIC = "Basic";
+ public static final String BEARER = "Bearer";
public static final String NEGOTIATE = "Negotiate";
private static final Logger LOG = LoggerFactory.getLogger(HttpAuthUtils.class);
private static final String COOKIE_ATTR_SEPARATOR = "&";
diff --git a/service/src/java/org/apache/hive/service/auth/jwt/JWTValidator.java b/service/src/java/org/apache/hive/service/auth/jwt/JWTValidator.java
new file mode 100644
index 0000000..a1b934f
--- /dev/null
+++ b/service/src/java/org/apache/hive/service/auth/jwt/JWTValidator.java
@@ -0,0 +1,111 @@
+/*
+ * 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.hive.service.auth.jwt;
+
+import com.google.common.base.Preconditions;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
+import com.nimbusds.jose.jwk.AsymmetricJWK;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.sasl.AuthenticationException;
+import java.io.IOException;
+import java.security.Key;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * This class is used to validate JWT. JWKS is fetched during instantiation and kept in the memory.
+ * We disallow JWT signature verification with symmetric key, because that means anyone can get the same key
+ * and use it to sign a JWT.
+ */
+public class JWTValidator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JWTValidator.class.getName());
+ private static final DefaultJWSVerifierFactory JWS_VERIFIER_FACTORY = new DefaultJWSVerifierFactory();
+
+ private final URLBasedJWKSProvider jwksProvider;
+
+ public JWTValidator(HiveConf conf) throws IOException, ParseException {
+ this.jwksProvider = new URLBasedJWKSProvider(conf);
+ }
+
+ public String validateJWTAndExtractUser(String signedJwt) throws ParseException, AuthenticationException {
+ Preconditions.checkNotNull(jwksProvider);
+ Preconditions.checkNotNull(signedJwt, "No token found");
+ final SignedJWT parsedJwt = SignedJWT.parse(signedJwt);
+ List<JWK> matchedJWKS = jwksProvider.getJWKs(parsedJwt.getHeader());
+ if (matchedJWKS.isEmpty()) {
+ throw new AuthenticationException("Failed to find matched JWKs with the JWT header: " + parsedJwt.getHeader());
+ }
+
+ // verify signature
+ Exception lastException = null;
+ for (JWK matchedJWK : matchedJWKS) {
+ String keyID = matchedJWK.getKeyID() == null ? "null" : matchedJWK.getKeyID();
+ try {
+ JWSVerifier verifier = getVerifier(parsedJwt.getHeader(), matchedJWK);
+ if (parsedJwt.verify(verifier)) {
+ LOG.debug("Verified JWT {} by JWK {}", parsedJwt.getPayload(), keyID);
+ break;
+ }
+ } catch (Exception e) {
+ lastException = e;
+ LOG.warn("Failed to verify JWT {} by JWK {}", parsedJwt.getPayload(), keyID, e);
+ }
+ }
+ // We use only the last seven characters to let a user can differentiate exceptions for different JWT
+ int startIndex = Math.max(0, signedJwt.length() - 7);
+ String lastSevenChars = signedJwt.substring(startIndex);
+ if (parsedJwt.getState() != JWSObject.State.VERIFIED) {
+ throw new AuthenticationException("Failed to verify the JWT signature (ends with " + lastSevenChars + ")",
+ lastException);
+ }
+
+ // verify claims
+ JWTClaimsSet claimsSet = parsedJwt.getJWTClaimsSet();
+ Date expirationTime = claimsSet.getExpirationTime();
+ if (expirationTime != null) {
+ Date now = new Date();
+ if (now.after(expirationTime)) {
+ LOG.warn("Rejecting an expired JWT: {}", parsedJwt.getPayload());
+ throw new AuthenticationException("JWT (ends with " + lastSevenChars + ") has been expired");
+ }
+ }
+
+ // We assume the subject of claims is the query user
+ return claimsSet.getSubject();
+ }
+
+ private static JWSVerifier getVerifier(JWSHeader header, JWK jwk) throws JOSEException {
+ Preconditions.checkArgument(jwk instanceof AsymmetricJWK,
+ "JWT signature verification with symmetric key is not allowed.");
+ Key key = ((AsymmetricJWK) jwk).toPublicKey();
+ return JWS_VERIFIER_FACTORY.createJWSVerifier(header, key);
+ }
+}
diff --git a/service/src/java/org/apache/hive/service/auth/jwt/URLBasedJWKSProvider.java b/service/src/java/org/apache/hive/service/auth/jwt/URLBasedJWKSProvider.java
new file mode 100644
index 0000000..ebf99e3
--- /dev/null
+++ b/service/src/java/org/apache/hive/service/auth/jwt/URLBasedJWKSProvider.java
@@ -0,0 +1,81 @@
+/*
+ * 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.hive.service.auth.jwt;
+
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKMatcher;
+import com.nimbusds.jose.jwk.JWKSelector;
+import com.nimbusds.jose.jwk.JWKSet;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.sasl.AuthenticationException;
+import java.io.IOException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides a way to get JWKS json. Hive will use this to verify the incoming JWTs.
+ */
+public class URLBasedJWKSProvider {
+
+ private static final Logger LOG = LoggerFactory.getLogger(URLBasedJWKSProvider.class.getName());
+ private final HiveConf conf;
+ private List<JWKSet> jwkSets = new ArrayList<>();
+
+ public URLBasedJWKSProvider(HiveConf conf) throws IOException, ParseException {
+ this.conf = conf;
+ loadJWKSets();
+ }
+
+ /**
+ * Fetches the JWKS and stores into memory. The JWKS are expected to be in the standard form as defined here -
+ * https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.
+ */
+ private void loadJWKSets() throws IOException, ParseException {
+ String jwksURL = HiveConf.getVar(conf, HiveConf.ConfVars.HIVE_SERVER2_AUTHENTICATION_JWT_JWKS_URL);
+ String[] jwksURLs = jwksURL.split(",");
+ for (String urlString : jwksURLs) {
+ URL url = new URL(urlString);
+ jwkSets.add(JWKSet.load(url));
+ LOG.info("Loaded JWKS from " + urlString);
+ }
+ }
+
+ /**
+ * Returns filtered JWKS by one or more criteria, such as kid, typ, alg.
+ */
+ public List<JWK> getJWKs(JWSHeader header) throws AuthenticationException {
+ JWKMatcher matcher = JWKMatcher.forJWSHeader(header);
+ if (matcher == null) {
+ throw new AuthenticationException("Unsupported algorithm: " + header.getAlgorithm());
+ }
+
+ List<JWK> jwks = new ArrayList<>();
+ JWKSelector selector = new JWKSelector(matcher);
+ for (JWKSet jwkSet : jwkSets) {
+ jwks.addAll(selector.select(jwkSet));
+ }
+ return jwks;
+ }
+}
diff --git a/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java b/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java
index 244bd3a..bbb74e0 100644
--- a/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java
+++ b/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java
@@ -24,6 +24,7 @@ import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.security.PrivilegedExceptionAction;
import java.security.SecureRandom;
+import java.text.ParseException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
@@ -41,6 +42,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.NewCookie;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import org.apache.hadoop.hive.conf.HiveConf;
@@ -59,6 +61,7 @@ import org.apache.hive.service.auth.HttpAuthUtils;
import org.apache.hive.service.auth.HttpAuthenticationException;
import org.apache.hive.service.auth.PasswdAuthenticationProvider;
import org.apache.hive.service.auth.PlainSaslHelper;
+import org.apache.hive.service.auth.jwt.JWTValidator;
import org.apache.hive.service.auth.ldap.HttpEmptyAuthenticationException;
import org.apache.hive.service.auth.saml.HiveSaml2Client;
import org.apache.hive.service.auth.saml.HiveSamlRelayStateStore;
@@ -110,6 +113,8 @@ public class ThriftHttpServlet extends TServlet {
private static final String HIVE_DELEGATION_TOKEN_HEADER = "X-Hive-Delegation-Token";
private static final String X_FORWARDED_FOR = "X-Forwarded-For";
+ private JWTValidator jwtValidator;
+
public ThriftHttpServlet(TProcessor processor, TProtocolFactory protocolFactory,
String authType, UserGroupInformation serviceUGI, UserGroupInformation httpUGI,
HiveAuthFactory hiveAuthFactory, HiveConf hiveConf) throws Exception {
@@ -136,6 +141,9 @@ public class ThriftHttpServlet extends TServlet {
this.isHttpOnlyCookie = hiveConf.getBoolVar(
ConfVars.HIVE_SERVER2_THRIFT_HTTP_COOKIE_IS_HTTPONLY);
}
+ if (this.authType.isEnabled(HiveAuthConstants.AuthTypes.JWT)) {
+ this.jwtValidator = new JWTValidator(hiveConf);
+ }
}
@Override
@@ -213,6 +221,8 @@ public class ThriftHttpServlet extends TServlet {
} else {
clientUserName = doKerberosAuth(request);
}
+ } else if (authType.isEnabled(HiveAuthConstants.AuthTypes.JWT) && hasJWT(request)) {
+ clientUserName = validateJWT(request, response);
} else if (authType.isEnabled(HiveAuthConstants.AuthTypes.SAML)) {
// check if this request needs a SAML redirect
String authHeader = request.getHeader(HttpAuthUtils.AUTHORIZATION);
@@ -264,8 +274,7 @@ public class ThriftHttpServlet extends TServlet {
LOG.info("Cookie added for clientUserName " + clientUserName);
}
super.doPost(request, response);
- }
- catch (HttpAuthenticationException e) {
+ } catch (HttpAuthenticationException e) {
// Ignore HttpEmptyAuthenticationException, it is normal for knox
// to send a request with empty header
if (!(e instanceof HttpEmptyAuthenticationException)) {
@@ -292,8 +301,7 @@ public class ThriftHttpServlet extends TServlet {
}
}
response.getWriter().println("Authentication Error: " + e.getMessage());
- }
- finally {
+ } finally {
// Clear the thread locals
SessionManager.clearUserName();
SessionManager.clearIpAddress();
@@ -302,6 +310,23 @@ public class ThriftHttpServlet extends TServlet {
}
}
+ private String validateJWT(HttpServletRequest request, HttpServletResponse response)
+ throws HttpAuthenticationException {
+ Preconditions.checkState(jwtValidator != null, "JWT validator should have been set");
+ String signedJwt = extractBearerToken(request, response);
+ String user = null;
+ try {
+ user = jwtValidator.validateJWTAndExtractUser(signedJwt);
+ Preconditions.checkNotNull(user, "JWT needs to contain the user name as subject");
+ Preconditions.checkState(!user.isEmpty(), "User name should not be empty");
+ LOG.info("JWT verification successful for user {}", user);
+ } catch (Exception e) {
+ LOG.error("JWT verification failed", e);
+ throw new HttpAuthenticationException(e);
+ }
+ return user;
+ }
+
/**
* A request needs redirect if it does not have a bearer token and it contains a valid
* response port in its header.
@@ -672,7 +697,8 @@ public class ThriftHttpServlet extends TServlet {
private String getUsername(HttpServletRequest request)
throws HttpAuthenticationException {
- String creds[] = getAuthHeaderTokens(request);
+ String authHeaderDecodedString = getAuthHeaderDecodedString(request);
+ String[] creds = authHeaderDecodedString.split(":", 2);
// Username must be present
if (creds[0] == null || creds[0].isEmpty()) {
throw new HttpAuthenticationException("Authorization header received " +
@@ -683,23 +709,23 @@ public class ThriftHttpServlet extends TServlet {
private String getPassword(HttpServletRequest request)
throws HttpAuthenticationException {
- String[] creds = getAuthHeaderTokens(request);
+ String authHeaderDecodedString = getAuthHeaderDecodedString(request);
+ String[] creds = authHeaderDecodedString.split(":", 2);
// Password must be present
- if (creds[1] == null || creds[1].isEmpty()) {
+ if (creds.length < 2 || creds[1] == null || creds[1].isEmpty()) {
throw new HttpAuthenticationException("Authorization header received " +
- "from the client does not contain username.");
+ "from the client does not contain password.");
}
return creds[1];
}
- private String[] getAuthHeaderTokens(HttpServletRequest request) throws HttpAuthenticationException {
+ private String getAuthHeaderDecodedString(HttpServletRequest request) throws HttpAuthenticationException {
String authHeaderBase64Str = getAuthHeader(request);
- String authHeaderString = new String(Base64.getDecoder().decode(authHeaderBase64Str), StandardCharsets.UTF_8);
- return authHeaderString.split(":");
+ return new String(Base64.getDecoder().decode(authHeaderBase64Str), StandardCharsets.UTF_8);
}
/**
- * Returns the base64 encoded auth header payload
+ * Returns the base64 encoded auth header payload.
* @param request request to interrogate
* @return base64 encoded auth header payload
* @throws HttpAuthenticationException exception if header is missing or empty
@@ -732,6 +758,18 @@ public class ThriftHttpServlet extends TServlet {
return authType.isEnabled(HiveAuthConstants.AuthTypes.KERBEROS);
}
+ private boolean hasJWT(HttpServletRequest request) {
+ String authHeaderString;
+ try {
+ authHeaderString = getAuthHeader(request);
+ } catch (HttpAuthenticationException e) {
+ return false;
+ }
+ // Assume JWT consists of three parts separated by dots
+ String[] jwt = authHeaderString.split("\\.");
+ return jwt.length == 3;
+ }
+
private static String getDoAsQueryParam(String queryString) {
if (LOG.isDebugEnabled()) {
LOG.debug("URL query string:" + queryString);