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]

Reply via email to