Skip to content

Commit 6ca06ca

Browse files
HIVE-29653: Fix SAML bearer token authentication bypass in HiveServer2 (#6534)
* HIVE-29653: Fix SAML bearer token authentication bypass in HiveServer2 * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Address review comments * Address issue caused by applying github copilot suggestion --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 4c4a618 commit 6ca06ca

3 files changed

Lines changed: 99 additions & 7 deletions

File tree

itests/hive-unit/src/test/java/org/apache/hive/service/auth/saml/TestHttpSamlAuthentication.java

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.net.InetAddress;
3636
import java.net.ServerSocket;
3737
import java.nio.charset.StandardCharsets;
38+
import java.util.Base64;
3839
import java.sql.ResultSet;
3940
import java.sql.SQLException;
4041
import java.sql.Statement;
@@ -137,9 +138,17 @@ public void cleanUpIdpEnv() {
137138
idpContainer.stop();
138139
idpContainer = null;
139140
}
140-
if (miniHS2 != null) {
141+
if (miniHS2 != null && miniHS2.isStarted()) {
141142
miniHS2.stop();
142143
}
144+
HiveSamlAuthTokenGenerator.shutdown();
145+
}
146+
147+
private static ISAMLAuthTokenGenerator createTokenGenerator(String tokenTtl) {
148+
HiveSamlAuthTokenGenerator.shutdown();
149+
HiveConf conf = new HiveConf();
150+
conf.setVar(ConfVars.HIVE_SERVER2_SAML_CALLBACK_TOKEN_TTL, tokenTtl);
151+
return HiveSamlAuthTokenGenerator.get(conf);
143152
}
144153

145154
private void setupIDP(boolean useSignedAssertions, String authMode) throws Exception {
@@ -554,6 +563,86 @@ public void testTokenReuse() throws Exception {
554563
}
555564
}
556565

566+
@Test
567+
public void testValidTokenRoundTrip() throws Exception {
568+
ISAMLAuthTokenGenerator tokenGenerator = createTokenGenerator("30s");
569+
String token = tokenGenerator.get("alice", "relay-state-1");
570+
assertEquals("alice", tokenGenerator.validate(token));
571+
}
572+
573+
@Test
574+
public void testForgedSignatureRejected() throws Exception {
575+
ISAMLAuthTokenGenerator tokenGenerator = createTokenGenerator("30s");
576+
String forgedPayload = "u=alice;id=1337;time=" + System.currentTimeMillis()
577+
+ ";rs=deadbeef;sg=bogus";
578+
try {
579+
String forgedToken = Base64.getEncoder().encodeToString(forgedPayload.getBytes(StandardCharsets.UTF_8));
580+
tokenGenerator.validate(forgedToken);
581+
fail("Expected forged token to be rejected");
582+
} catch (HttpSamlAuthenticationException e) {
583+
assertEquals("Token could not be verified", e.getMessage());
584+
}
585+
}
586+
587+
@Test
588+
public void testInvalidTokenRejected() throws Exception {
589+
ISAMLAuthTokenGenerator tokenGenerator = createTokenGenerator("30s");
590+
try {
591+
tokenGenerator.validate("notAValidToken");
592+
fail("Expected malformed base64 token to be rejected");
593+
} catch (HttpSamlAuthenticationException e) {
594+
assertEquals("Invalid token", e.getMessage());
595+
}
596+
String invalidStructure = Base64.getEncoder().encodeToString("foo".getBytes());
597+
try {
598+
tokenGenerator.validate(invalidStructure);
599+
fail("Expected invalid token structure to be rejected");
600+
} catch (HttpSamlAuthenticationException e) {
601+
assertEquals("Invalid token", e.getMessage());
602+
}
603+
}
604+
605+
@Test
606+
public void testExpiredTokenRejected() throws Exception {
607+
ISAMLAuthTokenGenerator tokenGenerator = createTokenGenerator("1s");
608+
String token = tokenGenerator.get("alice", "relay-state-1");
609+
Thread.sleep(1100);
610+
try {
611+
tokenGenerator.validate(token);
612+
fail("Expected expired token to be rejected");
613+
} catch (HttpSamlAuthenticationException e) {
614+
assertEquals("Token is expired", e.getMessage());
615+
}
616+
}
617+
618+
@Test
619+
public void testParseHandlesBase64PaddingInSignature() {
620+
Map<String, String> kv = new HashMap<>();
621+
String token = "u=alice;id=1;time=1000;rs=rs1;sg=YWJjZA==";
622+
assertTrue(HiveSamlAuthTokenGenerator.parse(token, kv));
623+
assertEquals("alice", kv.get("u"));
624+
assertEquals("YWJjZA==", kv.get("sg"));
625+
}
626+
627+
@Test
628+
public void testParseRejectsEncodedBearerToken() {
629+
Map<String, String> kv = new HashMap<>();
630+
String encoded = Base64.getEncoder().encodeToString(
631+
"u=alice;id=1;time=1000;rs=rs1;sg=abc".getBytes());
632+
assertFalse(HiveSamlAuthTokenGenerator.parse(encoded, kv));
633+
}
634+
635+
@Test
636+
public void testParseDecodedTokenFromGenerator() throws Exception {
637+
ISAMLAuthTokenGenerator tokenGenerator = createTokenGenerator("30s");
638+
String encoded = tokenGenerator.get("bob", "relay-42");
639+
String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
640+
Map<String, String> kv = new HashMap<>();
641+
assertTrue(HiveSamlAuthTokenGenerator.parse(decoded, kv));
642+
assertEquals("bob", kv.get("u"));
643+
assertEquals("relay-42", kv.get(HiveSamlAuthTokenGenerator.RELAY_STATE));
644+
}
645+
557646
private static void assertLoggedInUser(HiveConnection connection, String expectedUser)
558647
throws SQLException {
559648
Statement stmt = connection.createStatement();

service/src/java/org/apache/hive/service/auth/saml/HiveSamlAuthTokenGenerator.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.hive.service.auth.saml;
2020

2121
import com.google.common.annotations.VisibleForTesting;
22+
import java.nio.charset.StandardCharsets;
2223
import java.security.MessageDigest;
2324
import java.security.NoSuchAlgorithmException;
2425
import java.security.SecureRandom;
@@ -78,11 +79,11 @@ public String get(String username, String relayStateKey) {
7879
}
7980

8081
private String encode(String token) {
81-
return Base64.getEncoder().encodeToString(token.getBytes());
82+
return Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8));
8283
}
8384

8485
private String decode(String encodedToken) {
85-
return new String(Base64.getDecoder().decode(encodedToken));
86+
return new String(Base64.getDecoder().decode(encodedToken), StandardCharsets.UTF_8);
8687
}
8788

8889
private String getTokenStr(String username, String id, String timestamp,
@@ -100,7 +101,7 @@ private String getTokenStr(String username, String id, String timestamp,
100101
private String getSign(String input) {
101102
try {
102103
MessageDigest md = MessageDigest.getInstance("SHA-256");
103-
md.update(input.getBytes());
104+
md.update(input.getBytes(StandardCharsets.UTF_8));
104105
md.update(signatureSecret);
105106
byte[] digest = md.digest();
106107
return Base64.getEncoder().encodeToString(digest);
@@ -144,7 +145,8 @@ private boolean isExpired(long currentTime, long tokenTime) {
144145
}
145146

146147
private boolean signatureMatches(String origSign, String derivedSign) {
147-
return !MessageDigest.isEqual(origSign.getBytes(), derivedSign.getBytes());
148+
return MessageDigest.isEqual(origSign.getBytes(StandardCharsets.UTF_8),
149+
derivedSign.getBytes(StandardCharsets.UTF_8));
148150
}
149151

150152
public static boolean parse(String token, Map<String, String> kv) {
@@ -153,7 +155,7 @@ public static boolean parse(String token, Map<String, String> kv) {
153155
return false;
154156
}
155157
for (String split : splits) {
156-
String[] pair = split.split(SEPARATOR);
158+
String[] pair = split.split(SEPARATOR, 2);
157159
if (pair.length != 2) {
158160
return false;
159161
}

service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,8 @@ private String doSamlAuth(HttpServletRequest request, HttpServletResponse respon
382382
LOG.info("Successfully validated the token for user {}", user);
383383
// token is valid; now confirm if the client identifier matches with the relay state.
384384
Map<String, String> keyValues = new HashMap<>();
385-
if (HiveSamlAuthTokenGenerator.parse(token, keyValues)) {
385+
String decodedToken = new String(Base64.getDecoder().decode(token), java.nio.charset.StandardCharsets.UTF_8);
386+
if (HiveSamlAuthTokenGenerator.parse(decodedToken, keyValues)) {
386387
String relayStateKey = keyValues.get(HiveSamlAuthTokenGenerator.RELAY_STATE);
387388
if (!HiveSamlRelayStateStore.get()
388389
.validateClientIdentifier(relayStateKey, clientIdentifier)) {

0 commit comments

Comments
 (0)