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-wen....@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 <schaura...@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);