This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new cfc2e40efd5 branch-4.1: [fix](fe) Handle padded OIDC JDBC
authentication payloads (#69714)
cfc2e40efd5 is described below
commit cfc2e40efd5d97b04af711009066185a22deeddb
Author: Calvin Kirs <[email protected]>
AuthorDate: Fri Oct 30 15:39:23 2026 +0800
branch-4.1: [fix](fe) Handle padded OIDC JDBC authentication payloads
(#69714)
### What problem does this PR solve?
Related PR: #63068
MySQL Connector/J can send an `authentication_openid_connect_client`
payload containing the capability byte, a length-encoded JWT, and
trailing zero padding. On branch-4.1,
`MysqlAuthPacketCredentialExtractor` requires that no bytes remain after
the token. It therefore passes the entire encoded payload to the
authentication plugin and a valid token fails with `OIDC access token
signature validation failed`.
Extract the padding fix already included in master by #63068 into a
standalone branch-4.1 change. Accept trailing zero bytes while
preserving the existing handling of non-zero trailing data. Add coverage
for a token longer than 255 bytes with the six padding bytes produced by
Connector/J, and for a non-zero byte following zero padding.
### Release note
Fix OIDC JDBC login failures caused by trailing zero padding in MySQL
Connector/J authentication payloads.
### Check List (For Author)
- Test:
- [x] Unit Test: 57 MySQL protocol/authentication tests passed, with
zero failures/errors/skips. The new padding test fails before the fix.
FE core Checkstyle: zero violations.
- [ ] Regression test
- [x] Manual test: 12/12 checks passed against the FE built from this
branch, a 4.1.3 BE, and the existing Keycloak fixture with the stock
OIDC plugin.
- Unmodified Connector/J 9.4.0 and 9.6.0: OIDC login over TLS 1.3 and
two consecutive queries on the same connection succeeded.
- A token file ending in a newline also succeeded with Connector/J
9.6.0.
- Invalid signatures (both drivers), a mismatched username, disabled
TLS, and a missing token were rejected.
- Native password login with Connector/J 8.4.0/9.6.0 and MySQL Shell 9.5
valid/invalid token checks passed.
- FE compiled and packaged using `build.sh --fe` with UI disabled. For
local packaging only, the BE/Cloud native dependency bootstrap was
scoped to BE/Cloud because this host lacks Paimon native artifacts; the
build script was restored and is not part of this PR.
- Behavior changed:
- [x] Yes. OIDC JDBC payloads with zero padding now yield the original
token; non-zero trailing data is not discarded.
- Does this need documentation?
- [x] No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../MysqlAuthPacketCredentialExtractor.java | 12 +++++-
.../MysqlAuthPacketCredentialExtractorTest.java | 45 ++++++++++++++++++++++
2 files changed, 56 insertions(+), 1 deletion(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/MysqlAuthPacketCredentialExtractor.java
b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/MysqlAuthPacketCredentialExtractor.java
index 443515702ed..263d42de8c3 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/MysqlAuthPacketCredentialExtractor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/MysqlAuthPacketCredentialExtractor.java
@@ -75,12 +75,22 @@ public class MysqlAuthPacketCredentialExtractor {
} catch (RuntimeException e) {
return authResponse;
}
- if (oidcToken == null || oidcToken.length == 0 || payload.remaining()
!= 0 || !looksLikeJwt(oidcToken)) {
+ if (oidcToken == null || oidcToken.length == 0 ||
!remainingBytesArePadding(payload)
+ || !looksLikeJwt(oidcToken)) {
return authResponse;
}
return oidcToken;
}
+ private static boolean remainingBytesArePadding(ByteBuffer payload) {
+ while (payload.hasRemaining()) {
+ if (payload.get() != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private static boolean looksLikeJwt(byte[] bytes) {
return bytes.length >= 3
&& bytes[0] == JWT_PREFIX_FIRST_CHAR
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/MysqlAuthPacketCredentialExtractorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/MysqlAuthPacketCredentialExtractorTest.java
index acb6c7790a1..df282b3ed89 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/MysqlAuthPacketCredentialExtractorTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/MysqlAuthPacketCredentialExtractorTest.java
@@ -23,11 +23,13 @@ import org.apache.doris.mysql.MysqlChannel;
import org.apache.doris.mysql.MysqlSerializer;
import org.apache.doris.mysql.authenticate.password.ClearPassword;
+import com.google.common.base.Strings;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
class MysqlAuthPacketCredentialExtractorTest {
private static final String USER_NAME = "alice";
@@ -81,6 +83,49 @@ class MysqlAuthPacketCredentialExtractorTest {
Assertions.assertEquals(token, ((ClearPassword)
request.getPassword()).getPassword());
}
+ @Test
+ void testExtractAuthenticateRequestForOidcClientPayloadWithPadding() {
+ MysqlAuthPacketCredentialExtractor extractor = new
MysqlAuthPacketCredentialExtractor();
+ MysqlChannel channel = Mockito.mock(MysqlChannel.class);
+ MysqlAuthPacket authPacket = Mockito.mock(MysqlAuthPacket.class);
+ String token = "eyJhbGciOiJSUzI1NiJ9." + Strings.repeat("p", 1024) +
".signature";
+ byte[] payload = oidcClientPayload(token);
+ // Connector/J reserves nine bytes for the length and leaves six zero
bytes after this token.
+ byte[] paddedPayload = Arrays.copyOf(payload, payload.length + 6);
+ Mockito.when(channel.getRemoteIp()).thenReturn(REMOTE_IP);
+
Mockito.when(authPacket.getPluginName()).thenReturn(OIDC_CLIENT_PLUGIN);
+ Mockito.when(authPacket.getAuthResponse()).thenReturn(paddedPayload);
+
+ AuthenticateRequest request =
extractor.extractAuthenticateRequest(USER_NAME, channel, authPacket)
+ .orElseThrow(() -> new AssertionError("request is required"));
+
+ Assertions.assertEquals(CredentialType.OAUTH_TOKEN,
request.getCredentialType());
+ Assertions.assertArrayEquals(token.getBytes(StandardCharsets.UTF_8),
request.getCredential());
+ Assertions.assertInstanceOf(ClearPassword.class,
request.getPassword());
+ Assertions.assertEquals(token, ((ClearPassword)
request.getPassword()).getPassword());
+ }
+
+ @Test
+ void
testExtractAuthenticateRequestPreservesOidcClientPayloadWithNonZeroTrailingByte()
{
+ MysqlAuthPacketCredentialExtractor extractor = new
MysqlAuthPacketCredentialExtractor();
+ MysqlChannel channel = Mockito.mock(MysqlChannel.class);
+ MysqlAuthPacket authPacket = Mockito.mock(MysqlAuthPacket.class);
+ String token = "eyJhbGciOiJSUzI1NiJ9.payload.signature";
+ byte[] payload = oidcClientPayload(token);
+ byte[] invalidPayload = Arrays.copyOf(payload, payload.length + 6);
+ invalidPayload[invalidPayload.length - 1] = 1;
+ Mockito.when(channel.getRemoteIp()).thenReturn(REMOTE_IP);
+
Mockito.when(authPacket.getPluginName()).thenReturn(OIDC_CLIENT_PLUGIN);
+ Mockito.when(authPacket.getAuthResponse()).thenReturn(invalidPayload);
+
+ AuthenticateRequest request =
extractor.extractAuthenticateRequest(USER_NAME, channel, authPacket)
+ .orElseThrow(() -> new AssertionError("request is required"));
+
+ Assertions.assertArrayEquals(invalidPayload, request.getCredential());
+ Assertions.assertEquals(new String(invalidPayload,
StandardCharsets.UTF_8),
+ ((ClearPassword) request.getPassword()).getPassword());
+ }
+
private static byte[] oidcClientPayload(String token) {
MysqlSerializer serializer = MysqlSerializer.newInstance();
serializer.writeInt1(1);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]