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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 113088da6cda CAMEL-23995: Fix saslAuthType overriding explicit 
securityProtocol and generating non-working configs
113088da6cda is described below

commit 113088da6cdad653f12eec85e151e460aafeb01e
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 15 09:02:41 2026 +0200

    CAMEL-23995: Fix saslAuthType overriding explicit securityProtocol and 
generating non-working configs
    
    Fix three bugs in the saslAuthType convenience layer (CAMEL-22864) that made
    3 of 5 SASL auth types non-functional and silently overrode explicit user
    configuration:
    
    - SSL tautology: saslAuthType != NONE was always true inside isSasl(), 
forcing
      SASL_SSL even when user set securityProtocol=SASL_PLAINTEXT. Now relies on
      the configurer default and only downgrades on explicit SASL_PLAINTEXT.
    - Kerberos always throws: buildJaasConfig() called validateKerberos() before
      checking if principal was set. Now returns null early for external JAAS 
config.
    - Missing callback handlers: OAuth and AWS MSK IAM never set required 
callback
      handler classes. Added getSaslCallbackProperties() helper with putIfAbsent
      semantics. Also fixed OAuth scope key and moved token endpoint URL to 
top-level
      Kafka property.
    
    Closes #24690
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/component/kafka/KafkaConfiguration.java  | 18 ++---
 .../kafka/security/KafkaSecurityConfigurer.java    | 40 ++++++++++-
 .../camel/component/kafka/KafkaComponentTest.java  | 62 ++++++++++++++++
 .../security/KafkaSecurityConfigurerTest.java      | 83 +++++++++++++++++++++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    | 16 +++++
 5 files changed, 207 insertions(+), 12 deletions(-)

diff --git 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/KafkaConfiguration.java
 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/KafkaConfiguration.java
index 57372b08af62..1e0ec5d2d957 100755
--- 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/KafkaConfiguration.java
+++ 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/KafkaConfiguration.java
@@ -449,15 +449,17 @@ public class KafkaConfiguration implements Cloneable, 
HeaderFilterStrategyAware
                 // The saslAuthType just helps set the mechanism and protocol
             }
 
-            // Determine if we should use SSL (check if any SSL properties are 
set)
-            boolean hasSslConfig = 
ObjectHelper.isNotEmpty(sslTruststoreLocation)
-                    || ObjectHelper.isNotEmpty(sslKeystoreLocation)
-                    || sslContextParameters != null;
-
-            // For SASL types, default to SSL unless explicitly using 
SASL_PLAINTEXT
+            // For SASL types, the configurer defaults to useSsl=true 
(SASL_SSL).
+            // Only downgrade to SASL_PLAINTEXT if the user explicitly 
requested it.
             if (saslAuthType.isSasl()) {
-                boolean useSsl = !securityProtocol.equals("SASL_PLAINTEXT") && 
!securityProtocol.equals("PLAINTEXT");
-                configurer.withSsl(useSsl || hasSslConfig || saslAuthType != 
KafkaAuthType.NONE);
+                if (securityProtocol.equals("SASL_PLAINTEXT")) {
+                    configurer.withSsl(false);
+                }
+            } else if (saslAuthType == KafkaAuthType.NONE) {
+                boolean hasSslConfig = 
ObjectHelper.isNotEmpty(sslTruststoreLocation)
+                        || ObjectHelper.isNotEmpty(sslKeystoreLocation)
+                        || sslContextParameters != null;
+                configurer.withSsl(hasSslConfig || 
securityProtocol.equals("SSL"));
             }
 
             // Apply the configuration
diff --git 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/security/KafkaSecurityConfigurer.java
 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/security/KafkaSecurityConfigurer.java
index 7538bd3d4fc3..b445b8c0639e 100644
--- 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/security/KafkaSecurityConfigurer.java
+++ 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/security/KafkaSecurityConfigurer.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.component.kafka.security;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
 
@@ -23,6 +25,8 @@ import org.apache.camel.component.kafka.KafkaConfiguration;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.kafka.clients.CommonClientConfigs;
 import org.apache.kafka.common.config.SaslConfigs;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Builder for Kafka security configuration.
@@ -67,6 +71,8 @@ import org.apache.kafka.common.config.SaslConfigs;
  */
 public class KafkaSecurityConfigurer {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(KafkaSecurityConfigurer.class);
+
     private final KafkaAuthType authType;
     private String username;
     private String password;
@@ -336,9 +342,8 @@ public class KafkaSecurityConfigurer {
                 validateOAuth();
                 sb.append(" 
clientId=\"").append(escapeJaasValue(oauthClientId)).append("\"");
                 sb.append(" 
clientSecret=\"").append(escapeJaasValue(oauthClientSecret)).append("\"");
-                sb.append(" 
oauth.token.endpoint.uri=\"").append(oauthTokenEndpointUri).append("\"");
                 if (ObjectHelper.isNotEmpty(oauthScope)) {
-                    sb.append(" 
oauth.scope=\"").append(oauthScope).append("\"");
+                    sb.append(" 
scope=\"").append(escapeJaasValue(oauthScope)).append("\"");
                 }
                 break;
 
@@ -347,6 +352,11 @@ public class KafkaSecurityConfigurer {
                 break;
 
             case KERBEROS:
+                if (ObjectHelper.isEmpty(kerberosPrincipal)) {
+                    LOG.debug(
+                            "Kerberos principal not set; assuming external 
JAAS configuration (e.g. java.security.auth.login.config)");
+                    return null;
+                }
                 validateKerberos();
                 sb.append(" useKeyTab=").append(kerberosUseKeyTab);
                 sb.append(" storeKey=").append(kerberosStoreKey);
@@ -405,7 +415,32 @@ public class KafkaSecurityConfigurer {
             if (jaasConfig != null) {
                 configuration.setSaslJaasConfig(jaasConfig);
             }
+            applySaslProperties(configuration.getAdditionalProperties());
+        }
+    }
+
+    private void applySaslProperties(Map<String, Object> additionalProperties) 
{
+        getSaslCallbackProperties().forEach(additionalProperties::putIfAbsent);
+    }
+
+    private Map<String, String> getSaslCallbackProperties() {
+        Map<String, String> props = new LinkedHashMap<>();
+        switch (authType) {
+            case OAUTH:
+                props.put(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS,
+                        
"org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler");
+                if (ObjectHelper.isNotEmpty(oauthTokenEndpointUri)) {
+                    props.put(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL, 
oauthTokenEndpointUri);
+                }
+                break;
+            case AWS_MSK_IAM:
+                props.put(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS,
+                        
"software.amazon.msk.auth.iam.IAMClientCallbackHandler");
+                break;
+            default:
+                break;
         }
+        return props;
     }
 
     /**
@@ -426,6 +461,7 @@ public class KafkaSecurityConfigurer {
             if (jaasConfig != null) {
                 props.setProperty(SaslConfigs.SASL_JAAS_CONFIG, jaasConfig);
             }
+            getSaslCallbackProperties().forEach(props::setProperty);
         }
 
         return props;
diff --git 
a/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/KafkaComponentTest.java
 
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/KafkaComponentTest.java
index f2622e32c552..15154285dbac 100644
--- 
a/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/KafkaComponentTest.java
+++ 
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/KafkaComponentTest.java
@@ -36,6 +36,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -420,6 +421,8 @@ public class KafkaComponentTest {
         assertEquals("SASL_SSL", 
props.getProperty(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
         assertEquals("AWS_MSK_IAM", 
props.getProperty(SaslConfigs.SASL_MECHANISM));
         
assertTrue(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG).contains("IAMLoginModule"));
+        assertEquals("software.amazon.msk.auth.iam.IAMClientCallbackHandler",
+                
props.getProperty(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS));
     }
 
     @Test
@@ -446,6 +449,65 @@ public class KafkaComponentTest {
         assertNull(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG));
     }
 
+    @Test
+    public void testSaslAuthTypePlainWithExplicitSaslPlaintext() {
+        String uri = "kafka:mytopic?brokers=broker1:12345&saslAuthType=PLAIN"
+                     + 
"&saslUsername=user&saslPassword=pass&securityProtocol=SASL_PLAINTEXT";
+
+        KafkaEndpoint endpoint = context.getEndpoint(uri, KafkaEndpoint.class);
+
+        Properties props = 
endpoint.getConfiguration().createProducerProperties();
+        assertEquals("SASL_PLAINTEXT", 
props.getProperty(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+        assertEquals("PLAIN", props.getProperty(SaslConfigs.SASL_MECHANISM));
+    }
+
+    @Test
+    public void testSaslAuthTypeKerberosDoesNotThrow() {
+        String uri = 
"kafka:mytopic?brokers=broker1:12345&saslAuthType=KERBEROS";
+
+        KafkaEndpoint endpoint = context.getEndpoint(uri, KafkaEndpoint.class);
+        assertEquals(KafkaAuthType.KERBEROS, 
endpoint.getConfiguration().getSaslAuthType());
+
+        Properties props = 
endpoint.getConfiguration().createProducerProperties();
+        assertEquals("SASL_SSL", 
props.getProperty(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+        assertEquals("GSSAPI", props.getProperty(SaslConfigs.SASL_MECHANISM));
+        assertNull(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG));
+    }
+
+    @Test
+    public void testSaslAuthTypeOAuth() {
+        String uri = "kafka:mytopic?brokers=broker1:12345&saslAuthType=OAUTH"
+                     + "&oauthClientId=myClient&oauthClientSecret=mySecret"
+                     + 
"&oauthTokenEndpointUri=https://auth.example.com/token&oauthScope=kafka";;
+
+        KafkaEndpoint endpoint = context.getEndpoint(uri, KafkaEndpoint.class);
+        assertEquals(KafkaAuthType.OAUTH, 
endpoint.getConfiguration().getSaslAuthType());
+
+        Properties props = 
endpoint.getConfiguration().createProducerProperties();
+        assertEquals("SASL_SSL", 
props.getProperty(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+        assertEquals("OAUTHBEARER", 
props.getProperty(SaslConfigs.SASL_MECHANISM));
+        assertNotNull(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG));
+        
assertTrue(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG).contains("OAuthBearerLoginModule"));
+        
assertTrue(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG).contains("clientId=\"myClient\""));
+        
assertTrue(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG).contains("scope=\"kafka\""));
+        
assertFalse(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG).contains("oauth.token.endpoint.uri"));
+        
assertEquals("org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler",
+                
props.getProperty(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+        assertEquals("https://auth.example.com/token";,
+                
props.getProperty(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL));
+    }
+
+    @Test
+    public void testSaslAuthTypeNoneWithExplicitSsl() {
+        String uri = 
"kafka:mytopic?brokers=broker1:12345&saslAuthType=NONE&securityProtocol=SSL";
+
+        KafkaEndpoint endpoint = context.getEndpoint(uri, KafkaEndpoint.class);
+        assertEquals(KafkaAuthType.NONE, 
endpoint.getConfiguration().getSaslAuthType());
+
+        Properties props = 
endpoint.getConfiguration().createProducerProperties();
+        assertEquals("SSL", 
props.getProperty(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+    }
+
     // ========================================================================
     // Backward compatibility tests - existing security options should still 
work
     // ========================================================================
diff --git 
a/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/security/KafkaSecurityConfigurerTest.java
 
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/security/KafkaSecurityConfigurerTest.java
index b70ad3449bc4..f2de4a435fe6 100644
--- 
a/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/security/KafkaSecurityConfigurerTest.java
+++ 
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/security/KafkaSecurityConfigurerTest.java
@@ -231,8 +231,9 @@ public class KafkaSecurityConfigurerTest {
         
assertTrue(jaasConfig.contains("org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule
 required"));
         assertTrue(jaasConfig.contains("clientId=\"my-client\""));
         assertTrue(jaasConfig.contains("clientSecret=\"my-secret\""));
-        
assertTrue(jaasConfig.contains("oauth.token.endpoint.uri=\"https://auth.example.com/token\"";));
-        assertTrue(jaasConfig.contains("oauth.scope=\"kafka\""));
+        assertTrue(jaasConfig.contains("scope=\"kafka\""));
+        assertFalse(jaasConfig.contains("oauth.token.endpoint.uri"));
+        assertFalse(jaasConfig.contains("oauth.scope"));
     }
 
     @Test
@@ -466,6 +467,84 @@ public class KafkaSecurityConfigurerTest {
         assertNull(props.getProperty(SaslConfigs.SASL_JAAS_CONFIG));
     }
 
+    // ========================================================================
+    // Kerberos without principal (external JAAS) tests
+    // ========================================================================
+
+    @Test
+    public void testBuildJaasConfigKerberosNoPrincipal() {
+        KafkaSecurityConfigurer configurer = 
KafkaSecurityConfigurer.forAuthType(KafkaAuthType.KERBEROS);
+        assertNull(configurer.buildJaasConfig());
+    }
+
+    @Test
+    public void testKerberosNoPrincipalSetsProtocolAndMechanism() {
+        KafkaConfiguration config = new KafkaConfiguration();
+        KafkaSecurityConfigurer.forAuthType(KafkaAuthType.KERBEROS)
+                .configure(config);
+
+        assertEquals("SASL_SSL", config.getSecurityProtocol());
+        assertEquals("GSSAPI", config.getSaslMechanism());
+        assertNull(config.getSaslJaasConfig());
+    }
+
+    // ========================================================================
+    // Callback handler and additional SASL properties tests
+    // ========================================================================
+
+    @Test
+    public void testToPropertiesOAuthIncludesCallbackHandler() {
+        Properties props = KafkaSecurityConfigurer.oauth("client", "secret", 
"https://auth.example.com/token";)
+                .toProperties();
+
+        
assertEquals("org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler",
+                
props.getProperty(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+        assertEquals("https://auth.example.com/token";,
+                
props.getProperty(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL));
+    }
+
+    @Test
+    public void testToPropertiesAwsMskIamIncludesCallbackHandler() {
+        Properties props = KafkaSecurityConfigurer.awsMskIam().toProperties();
+
+        assertEquals("software.amazon.msk.auth.iam.IAMClientCallbackHandler",
+                
props.getProperty(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS));
+    }
+
+    @Test
+    public void testConfigureOAuthSetsCallbackHandler() {
+        KafkaConfiguration config = new KafkaConfiguration();
+        KafkaSecurityConfigurer.oauth("client", "secret", 
"https://auth.example.com/token";)
+                .configure(config);
+
+        
assertEquals("org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler",
+                
config.getAdditionalProperties().get(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+        assertEquals("https://auth.example.com/token";,
+                
config.getAdditionalProperties().get(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL));
+    }
+
+    @Test
+    public void testConfigureAwsMskIamSetsCallbackHandler() {
+        KafkaConfiguration config = new KafkaConfiguration();
+        KafkaSecurityConfigurer.awsMskIam()
+                .configure(config);
+
+        assertEquals("software.amazon.msk.auth.iam.IAMClientCallbackHandler",
+                
config.getAdditionalProperties().get(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS));
+    }
+
+    @Test
+    public void testConfigureOAuthDoesNotOverrideUserCallbackHandler() {
+        KafkaConfiguration config = new KafkaConfiguration();
+        
config.getAdditionalProperties().put(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS,
 "com.custom.MyHandler");
+
+        KafkaSecurityConfigurer.oauth("client", "secret", 
"https://auth.example.com/token";)
+                .configure(config);
+
+        assertEquals("com.custom.MyHandler",
+                
config.getAdditionalProperties().get(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+    }
+
     // ========================================================================
     // toString() test
     // ========================================================================
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index e58517c676e4..935ef84e5273 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -240,6 +240,22 @@ but is applied to both consumer and producer Kafka 
clients. The label has been c
 If you use the Endpoint DSL, the `metadataMaxAgeMs` method has moved from the 
producer (advanced) builder
 to the common builder.
 
+=== camel-kafka - saslAuthType behavior changes
+
+When `saslAuthType` is set, the generated JAAS configuration and additional 
SASL properties are now
+applied using `putIfAbsent` semantics, so explicitly configured 
`saslJaasConfig`, callback handler
+classes, or token endpoint URLs take precedence over the auto-generated values.
+
+For OAuth (`saslAuthType=OAUTH`), the JAAS string now contains only 
login-module parameters
+(`clientId`, `clientSecret`, `scope`). The `oauth.token.endpoint.uri` and 
callback handler
+class are set as separate Kafka client properties instead of being embedded in 
the JAAS string,
+matching the Kafka client's expected configuration format.
+
+For Kerberos (`saslAuthType=KERBEROS`), if the principal is not configured, 
the configurer now
+assumes an external JAAS configuration (e.g. via 
`java.security.auth.login.config`) and skips
+JAAS string generation instead of throwing an exception. This supports 
deployment scenarios
+where Kerberos is configured externally.
+
 === camel-kafka - Producer no longer silently drops scalar Jackson nodes
 
 With `useIterator=true`, the Kafka producer splits an `Iterable` body into one 
record per element.

Reply via email to