This is an automated email from the ASF dual-hosted git repository.

showuon pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new ed9898d7d33 KAFKA-20831: Cache SASL principal per authentication 
(#22880)
ed9898d7d33 is described below

commit ed9898d7d33480fdaa1087a98527cb09f179f422
Author: Nandini Singhal <[email protected]>
AuthorDate: Thu Jul 30 04:37:51 2026 +0200

    KAFKA-20831: Cache SASL principal per authentication (#22880)
    
    ## Summary
    
    - Cache the `KafkaPrincipal` built by `SaslServerAuthenticator` for the
    lifetime of an authenticated connection.
    - Continue to build a new principal after successful SASL
    re-authentication, which installs a new authenticator.
    - Add regression coverage for repeated principal access and
    re-authentication.
    
    ## Motivation
    
    `KafkaChannel.principal()` is consulted while request contexts are
    constructed. The current SASL implementation creates a new
    `SaslAuthenticationContext` and invokes the configured
    `KafkaPrincipalBuilder` on every call, even though the authenticated
    identity is connection-scoped.
    
    Custom principal builders may perform non-trivial work or have side
    effects. Reusing the principal produced for the authenticated session
    avoids redundant work and ensures repeated access observes the same
    principal instance.
    
    ## Compatibility
    
    This change introduces no public API or configuration changes. Initial
    authentication behavior is unchanged. During re-authentication Kafka
    installs a new `SaslServerAuthenticator`, so the new authenticated
    session receives a fresh principal.
    
    ## Validation
    
    ```text
    ./gradlew :clients:test --tests
    'org.apache.kafka.common.security.authenticator.SaslAuthenticatorTest'
    ./gradlew spotlessCheck :clients:checkstyleMain :clients:checkstyleTest
    :clients:spotbugsMain -x test
    ```
    
    The test suite covers PLAIN, SCRAM, OAuth bearer, SSL, token
    authentication, and SASL re-authentication paths.
    
    Reviewers: Gaurav Narula <[email protected]>, Luke Chen
     <[email protected]>
    
    ---------
    
    Co-authored-by: Nandini Singhal 
<[email protected]>
---
 .../authenticator/SaslServerAuthenticator.java     | 12 +++++-
 .../authenticator/SaslAuthenticatorTest.java       | 47 ++++++++++++++++++++++
 2 files changed, 57 insertions(+), 2 deletions(-)

diff --git 
a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java
 
b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java
index b84b5dc2abc..bb8fddc671d 100644
--- 
a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java
+++ 
b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java
@@ -139,6 +139,10 @@ public class SaslServerAuthenticator implements 
Authenticator {
     private AuthenticationException pendingException = null;
     private SaslServer saslServer;
     private String saslMechanism;
+    // The authenticated principal is connection-scoped. Cache it after the 
first build since principal() is invoked
+    // for every request. It is only accessed by the Processor thread, so no 
synchronization is needed.
+    // Re-authentication installs a new SaslServerAuthenticator, and therefore 
a fresh cache.
+    private KafkaPrincipal principal;
 
     // buffers used in `authenticate`
     private Integer saslAuthRequestMaxReceiveSize;
@@ -305,14 +309,18 @@ public class SaslServerAuthenticator implements 
Authenticator {
 
     @Override
     public KafkaPrincipal principal() {
+        if (principal != null)
+            return principal;
+
         Optional<SSLSession> sslSession = transportLayer instanceof 
SslTransportLayer ?
                 Optional.of(((SslTransportLayer) transportLayer).sslSession()) 
: Optional.empty();
         SaslAuthenticationContext context = new 
SaslAuthenticationContext(saslServer, securityProtocol,
                 clientAddress(), listenerName.value(), sslSession);
-        KafkaPrincipal principal = principalBuilder.build(context);
+        KafkaPrincipal builtPrincipal = principalBuilder.build(context);
         if (ScramMechanism.isScram(saslMechanism) && 
Boolean.parseBoolean((String) 
saslServer.getNegotiatedProperty(ScramLoginModule.TOKEN_AUTH_CONFIG))) {
-            principal.tokenAuthenticated(true);
+            builtPrincipal.tokenAuthenticated(true);
         }
+        principal = builtPrincipal;
         return principal;
     }
 
diff --git 
a/clients/src/test/java/org/apache/kafka/common/security/authenticator/SaslAuthenticatorTest.java
 
b/clients/src/test/java/org/apache/kafka/common/security/authenticator/SaslAuthenticatorTest.java
index 07f1d3ca690..fbb4f378c89 100644
--- 
a/clients/src/test/java/org/apache/kafka/common/security/authenticator/SaslAuthenticatorTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/common/security/authenticator/SaslAuthenticatorTest.java
@@ -178,6 +178,7 @@ public class SaslAuthenticatorTest {
         saslClientConfigs = 
clientCertStores.getTrustingConfig(serverCertStores);
         credentialCache = new CredentialCache();
         TestLogin.loginCount.set(0);
+        CountingKafkaPrincipalBuilder.buildCount = 0;
     }
 
     @AfterEach
@@ -217,6 +218,31 @@ public class SaslAuthenticatorTest {
         checkAuthenticationAndReauthentication(securityProtocol, node);
     }
 
+    @Test
+    public void testPrincipalBuiltOncePerAuthentication() throws Exception {
+        String node = "0";
+        time = new MockTime();
+        SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
+        configureMechanisms("PLAIN", Collections.singletonList("PLAIN"));
+        
saslServerConfigs.put(BrokerSecurityConfigs.PRINCIPAL_BUILDER_CLASS_CONFIG,
+                CountingKafkaPrincipalBuilder.class);
+
+        server = createEchoServer(securityProtocol);
+        createClientConnection(securityProtocol, node);
+        checkClientConnection(node);
+        server.selector().channels().get(0).principal();
+        server.selector().channels().get(0).principal();
+        assertEquals(1, CountingKafkaPrincipalBuilder.buildCount,
+                "Principal should be built only once for repeated access on a 
connection");
+
+        time.sleep((long) (CONNECTIONS_MAX_REAUTH_MS_VALUE * 1.1));
+        checkClientConnection(node);
+        server.verifyReauthenticationMetrics(1, 0);
+        server.selector().channels().get(0).principal();
+        assertEquals(2, CountingKafkaPrincipalBuilder.buildCount,
+                "A successful re-authentication should build and cache a new 
principal");
+    }
+
     /**
      * Test SASL/PLAIN with sasl.authentication.max.receive.size config
      */
@@ -2773,4 +2799,25 @@ public class SaslAuthenticatorTest {
             return null;
         }
     }
+
+    public static class CountingKafkaPrincipalBuilder implements 
KafkaPrincipalBuilder {
+        private static int buildCount;
+
+        @Override
+        public KafkaPrincipal build(AuthenticationContext context) {
+            SaslAuthenticationContext saslContext = 
(SaslAuthenticationContext) context;
+            buildCount++;
+            return new KafkaPrincipal(KafkaPrincipal.USER_TYPE, 
saslContext.server().getAuthorizationID());
+        }
+
+        @Override
+        public byte[] serialize(KafkaPrincipal principal) {
+            return new byte[0];
+        }
+
+        @Override
+        public KafkaPrincipal deserialize(byte[] bytes) {
+            return null;
+        }
+    }
 }

Reply via email to