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

omkreddy 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 9c7fad33973 Require expected issuer and audience for the 
SASL/OAUTHBEARER broker validator
9c7fad33973 is described below

commit 9c7fad339731199da395d90690fc62efae69615b
Author: Evan Zhou <[email protected]>
AuthorDate: Wed Jun 17 23:51:55 2026 -0500

    Require expected issuer and audience for the SASL/OAUTHBEARER broker 
validator
    
    When a JWKS endpoint is configured, the broker's SASL/OAUTHBEARER validator 
now requires
    both sasl.oauthbearer.expected.issuer and 
sasl.oauthbearer.expected.audience to be set, and
    the broker fails fast at startup otherwise, so the issuer and audience of 
incoming tokens are
    verified by default.
    
    Operators that intentionally do not restrict the issuer or audience can opt 
out via
    sasl.oauthbearer.allow.unverified.issuer=true and/or 
sasl.oauthbearer.allow.unverified.audience=true;
    a startup warning is logged when an opt-out is enabled.
    
    - Add sasl.oauthbearer.allow.unverified.issuer and 
sasl.oauthbearer.allow.unverified.audience
      (boolean, default false) to both the client and broker config definitions.
    - Raise the importance of sasl.oauthbearer.expected.issuer and 
sasl.oauthbearer.expected.audience
      to HIGH.
    - BrokerJwtValidator fails fast when an expected value is missing without 
the matching opt-out,
      and skips jose4j's default audience validation when the audience opt-out 
is enabled.
    
    Reviewers: Manikumar Reddy <[email protected]>
---
 checkstyle/import-control.xml                      |   1 +
 .../apache/kafka/common/config/SaslConfigs.java    |  34 +++-
 .../config/internals/BrokerSecurityConfigs.java    |   7 +-
 .../security/oauthbearer/BrokerJwtValidator.java   |  60 +++++-
 .../oauthbearer/BrokerJwtValidatorTest.java        | 212 ++++++++++++++++++++-
 .../oauthbearer/DefaultJwtValidatorTest.java       |  66 ++++++-
 .../OAuthBearerValidatorCallbackHandlerTest.java   |  36 +++-
 .../kafka/api/ClientOAuthIntegrationTest.scala     |   1 +
 docs/getting-started/upgrade.md                    |   1 +
 9 files changed, 398 insertions(+), 20 deletions(-)

diff --git a/checkstyle/import-control.xml b/checkstyle/import-control.xml
index 49c57b8dffa..c76980b572e 100644
--- a/checkstyle/import-control.xml
+++ b/checkstyle/import-control.xml
@@ -135,6 +135,7 @@
       </subpackage>
       <subpackage name="oauthbearer">
         <allow pkg="com.fasterxml.jackson.databind" />
+        <allow class="org.apache.logging.log4j.Level" />
         <allow pkg="org.jose4j" />
         <allow pkg="javax.crypto"/>
         <allow pkg="org.testcontainers" />
diff --git 
a/clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java 
b/clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java
index 01f7ad1f927..02704a2300d 100644
--- a/clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java
+++ b/clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java
@@ -352,15 +352,31 @@ public class SaslConfigs {
     public static final String SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS_DOC = "The 
(optional) value in seconds to allow for differences between the time of the 
OAuth/OIDC identity provider and"
             + " the broker.";
 
+    public static final String SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE = 
"sasl.oauthbearer.allow.unverified.audience";
+    public static final String SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE_DOC 
= "The (optional) setting that allows the broker to validate JWTs without 
verifying the audience. By default the broker"
+            + " requires sasl.oauthbearer.expected.audience to be set whenever 
" + SASL_OAUTHBEARER_JWKS_ENDPOINT_URL + " is configured, and rejects any JWT 
whose \"aud\" claim does"
+            + " not match one of the expected audiences. When this is set to 
true that requirement is lifted: the \"aud\" claim is no longer checked and a 
JWT bearing any (or no)"
+            + " audience is accepted. Leaving the audience unverified is 
insecure and strongly discouraged; enable it only for deployments that 
intentionally accept tokens regardless"
+            + " of their intended audience. The default value is 'false'.";
+
     public static final String SASL_OAUTHBEARER_EXPECTED_AUDIENCE = 
"sasl.oauthbearer.expected.audience";
-    public static final String SASL_OAUTHBEARER_EXPECTED_AUDIENCE_DOC = "The 
(optional) comma-delimited setting for the broker to use to verify that the JWT 
was issued for one of the"
-            + " expected audiences. The JWT will be inspected for the standard 
OAuth \"aud\" claim and if this value is set, the broker will match the value 
from JWT's \"aud\" claim "
-            + " to see if there is an exact match. If there is no match, the 
broker will reject the JWT and authentication will fail.";
+    public static final String SASL_OAUTHBEARER_EXPECTED_AUDIENCE_DOC = "The 
comma-delimited setting for the broker to use to verify that the JWT was issued 
for one of the"
+            + " expected audiences. The JWT will be inspected for the standard 
OAuth \"aud\" claim and the broker will match the value(s) from the JWT's 
\"aud\" claim"
+            + " to see if there is an exact match. If there is no match, the 
broker will reject the JWT and authentication will fail. Required if " + 
SASL_OAUTHBEARER_JWKS_ENDPOINT_URL + " is set," +
+            " with the ability to opt-out using " + 
SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE + ".";
+
+    public static final String SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER = 
"sasl.oauthbearer.allow.unverified.issuer";
+    public static final String SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER_DOC = 
"The (optional) setting that allows the broker to validate JWTs without 
verifying the issuer. By default the broker"
+            + " requires sasl.oauthbearer.expected.issuer to be set whenever " 
+ SASL_OAUTHBEARER_JWKS_ENDPOINT_URL + " is configured, and rejects any JWT 
whose \"iss\" claim does not"
+            + " match the expected issuer. When this is set to true that 
requirement is lifted: the \"iss\" claim is no longer checked and a JWT bearing 
any (or no) issuer is accepted."
+            + " Leaving the issuer unverified is insecure and strongly 
discouraged; enable it only for deployments that intentionally accept tokens 
regardless of which issuer minted"
+            + " them. The default value is 'false'.";
 
     public static final String SASL_OAUTHBEARER_EXPECTED_ISSUER = 
"sasl.oauthbearer.expected.issuer";
-    public static final String SASL_OAUTHBEARER_EXPECTED_ISSUER_DOC = "The 
(optional) setting for the broker to use to verify that the JWT was created by 
the expected issuer. The JWT will"
-            + " be inspected for the standard OAuth \"iss\" claim and if this 
value is set, the broker will match it exactly against what is in the JWT's 
\"iss\" claim. If there is no"
-            + " match, the broker will reject the JWT and authentication will 
fail.";
+    public static final String SASL_OAUTHBEARER_EXPECTED_ISSUER_DOC = "The 
setting for the broker to use to verify that the JWT was created by the 
expected issuer. The JWT will"
+            + " be inspected for the standard OAuth \"iss\" claim and the 
broker will match it exactly against what is in the JWT's \"iss\" claim. If 
there is no"
+            + " match, the broker will reject the JWT and authentication will 
fail. Required if " + SASL_OAUTHBEARER_JWKS_ENDPOINT_URL + " is set, with the 
ability to " +
+            "opt-out using " + SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER + ".";
 
     public static final String SASL_OAUTHBEARER_HEADER_URLENCODE = 
"sasl.oauthbearer.header.urlencode";
     public static final boolean DEFAULT_SASL_OAUTHBEARER_HEADER_URLENCODE = 
false;
@@ -409,8 +425,10 @@ public class SaslConfigs {
                 
.define(SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MAX_MS, 
ConfigDef.Type.LONG, 
DEFAULT_SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MAX_MS, 
ConfigDef.Importance.LOW, 
SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MAX_MS_DOC)
                 
.define(SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MS, 
ConfigDef.Type.LONG, DEFAULT_SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MS, 
ConfigDef.Importance.LOW, SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MS_DOC)
                 .define(SaslConfigs.SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS, 
ConfigDef.Type.INT, DEFAULT_SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS, 
ConfigDef.Importance.LOW, SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS_DOC)
-                .define(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
ConfigDef.Type.LIST, List.of(), ConfigDef.ValidList.anyNonDuplicateValues(true, 
false), ConfigDef.Importance.LOW, SASL_OAUTHBEARER_EXPECTED_AUDIENCE_DOC)
-                .define(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, 
ConfigDef.Type.STRING, null, ConfigDef.Importance.LOW, 
SASL_OAUTHBEARER_EXPECTED_ISSUER_DOC)
+                
.define(SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE, 
ConfigDef.Type.BOOLEAN, false, ConfigDef.Importance.MEDIUM, 
SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE_DOC)
+                .define(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
ConfigDef.Type.LIST, List.of(), ConfigDef.ValidList.anyNonDuplicateValues(true, 
false), ConfigDef.Importance.HIGH, SASL_OAUTHBEARER_EXPECTED_AUDIENCE_DOC)
+                .define(SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER, 
ConfigDef.Type.BOOLEAN, false, ConfigDef.Importance.MEDIUM, 
SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER_DOC)
+                .define(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, 
ConfigDef.Type.STRING, null, ConfigDef.Importance.HIGH, 
SASL_OAUTHBEARER_EXPECTED_ISSUER_DOC)
                 .define(SaslConfigs.SASL_OAUTHBEARER_HEADER_URLENCODE, 
ConfigDef.Type.BOOLEAN, DEFAULT_SASL_OAUTHBEARER_HEADER_URLENCODE, 
ConfigDef.Importance.LOW, SASL_OAUTHBEARER_HEADER_URLENCODE_DOC);
     }
 }
diff --git 
a/clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java
 
b/clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java
index ad714803e04..a7c7c1b2073 100644
--- 
a/clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java
+++ 
b/clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java
@@ -27,6 +27,7 @@ import org.apache.kafka.common.utils.Utils;
 
 import java.util.List;
 
+import static org.apache.kafka.common.config.ConfigDef.Importance.HIGH;
 import static org.apache.kafka.common.config.ConfigDef.Importance.LOW;
 import static org.apache.kafka.common.config.ConfigDef.Importance.MEDIUM;
 import static org.apache.kafka.common.config.ConfigDef.Type.BOOLEAN;
@@ -217,6 +218,8 @@ public class BrokerSecurityConfigs {
             
.define(SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MS, LONG, 
SaslConfigs.DEFAULT_SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MS, LOW, 
SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MS_DOC)
             
.define(SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MAX_MS, LONG, 
SaslConfigs.DEFAULT_SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MAX_MS, LOW, 
SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_RETRY_BACKOFF_MAX_MS_DOC)
             .define(SaslConfigs.SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS, INT, 
SaslConfigs.DEFAULT_SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS, LOW, 
SaslConfigs.SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS_DOC)
-            .define(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, LIST, 
List.of(), ConfigDef.ValidList.anyNonDuplicateValues(true, false), LOW, 
SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE_DOC)
-            .define(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, STRING, 
null, LOW, SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER_DOC);
+            .define(SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE, 
BOOLEAN, false, MEDIUM, 
SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE_DOC)
+            .define(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, LIST, 
List.of(), ConfigDef.ValidList.anyNonDuplicateValues(true, false), HIGH, 
SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE_DOC)
+            .define(SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER, 
BOOLEAN, false, MEDIUM, 
SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER_DOC)
+            .define(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, STRING, 
null, HIGH, SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER_DOC);
 }
diff --git 
a/clients/src/main/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidator.java
 
b/clients/src/main/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidator.java
index 60b065a5e45..9a3c6810640 100644
--- 
a/clients/src/main/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidator.java
+++ 
b/clients/src/main/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidator.java
@@ -17,6 +17,7 @@
 
 package org.apache.kafka.common.security.oauthbearer;
 
+import org.apache.kafka.common.config.ConfigException;
 import 
org.apache.kafka.common.security.oauthbearer.internals.secured.BasicOAuthBearerToken;
 import 
org.apache.kafka.common.security.oauthbearer.internals.secured.ClaimValidationUtils;
 import 
org.apache.kafka.common.security.oauthbearer.internals.secured.CloseableVerificationKeyResolver;
@@ -43,6 +44,8 @@ import java.util.Set;
 
 import javax.security.auth.login.AppConfigurationEntry;
 
+import static 
org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE;
+import static 
org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER;
 import static 
org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS;
 import static 
org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE;
 import static 
org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER;
@@ -74,6 +77,20 @@ import static 
org.jose4j.jwa.AlgorithmConstraints.DISALLOW_NONE;
  *         Signature matching validation against the <code>kid</code> and 
those provided by
  *         the OAuth/OIDC provider's JWKS
  *     </li>
+ *     <li>
+ *         Validation of the <code>aud</code> (audience) claim. The broker 
must be configured with
+ *         {@code sasl.oauthbearer.expected.audience}; the token's 
<code>aud</code> claim is matched
+ *         against those values. If no expected audience is configured, the 
audience cannot be
+ *         verified, so configuration fails fast (the broker refuses to start) 
unless
+ *         {@code sasl.oauthbearer.allow.unverified.audience} is set to {@code 
true}.
+ *     </li>
+ *     <li>
+ *         Validation of the <code>iss</code> (issuer) claim. The broker must 
be configured with
+ *         {@code sasl.oauthbearer.expected.issuer}; the token's 
<code>iss</code> claim is matched
+ *         exactly against that value. If the expected issuer is not 
configured, the issuer cannot be
+ *         verified, so configuration fails fast (the broker refuses to start) 
unless
+ *         {@code sasl.oauthbearer.allow.unverified.issuer} is set to {@code 
true}.
+ *     </li>
  * </ol>
  */
 public class BrokerJwtValidator implements JwtValidator {
@@ -110,6 +127,30 @@ public class BrokerJwtValidator implements JwtValidator {
         String expectedIssuer = 
cu.validateString(SASL_OAUTHBEARER_EXPECTED_ISSUER, false);
         String scopeClaimName = 
cu.validateString(SASL_OAUTHBEARER_SCOPE_CLAIM_NAME);
         String subClaimName = 
cu.validateString(SASL_OAUTHBEARER_SUB_CLAIM_NAME);
+        boolean allowUnverifiedAudience = 
Boolean.TRUE.equals(cu.validateBoolean(SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE,
 false));
+        boolean allowUnverifiedIssuer = 
Boolean.TRUE.equals(cu.validateBoolean(SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER,
 false));
+
+        // The broker must be configured with at least one expected audience 
so it can verify that the token was minted
+        // for this cluster. Without it, a token issued for any other service 
by the same provider would be accepted.
+        // Fail fast at configuration time unless the operator has explicitly 
opted out.
+        if (expectedAudiences.isEmpty() && !allowUnverifiedAudience) {
+            throw new ConfigException(String.format(
+                "The OAuth validator for the broker requires \"%s\" to be 
configured so that the token's audience" +
+                " (\"aud\") claim can be verified, but it was not set. Set 
\"%s\" to the audience(s) that identify this" +
+                " cluster, or set \"%s\" to true to accept tokens regardless 
of their audience.",
+                SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE));
+        }
+
+        // The broker must be configured with an expected issuer. Without it, 
jose4j accepts a token bearing any (or no)
+        // "iss" claim, so the issuer cannot be verified. Fail fast at 
configuration time unless the operator has
+        // explicitly opted out.
+        if (expectedIssuer == null && !allowUnverifiedIssuer) {
+            throw new ConfigException(String.format(
+                "The OAuth validator for the broker requires \"%s\" to be 
configured so that the token's issuer" +
+                " (\"iss\") claim can be verified, but it was not set. Set 
\"%s\" to the issuer URL of the OAuth/OIDC" +
+                " provider that signs the tokens, or set \"%s\" to true to 
accept tokens regardless of their issuer.",
+                SASL_OAUTHBEARER_EXPECTED_ISSUER, 
SASL_OAUTHBEARER_EXPECTED_ISSUER, SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER));
+        }
 
         CloseableVerificationKeyResolver verificationKeyResolver = 
verificationKeyResolverOpt.orElseGet(
             () -> VerificationKeyResolverFactory.get(configs, saslMechanism, 
jaasConfigEntries)
@@ -120,11 +161,26 @@ public class BrokerJwtValidator implements JwtValidator {
         if (clockSkew != null)
             jwtConsumerBuilder.setAllowedClockSkewInSeconds(clockSkew);
 
-        if (!expectedAudiences.isEmpty())
+        if (!expectedAudiences.isEmpty()) {
             
jwtConsumerBuilder.setExpectedAudience(expectedAudiences.toArray(new 
String[0]));
+        } else {
+            // Reached only when the audience opt-out is enabled (otherwise 
configuration fails fast above). jose4j
+            // otherwise rejects any token that carries an "aud" claim when no 
expected audience is set, so skip its
+            // default audience validation entirely.
+            log.warn("The OAuth broker validator is configured with a JWKS 
endpoint but without \"{}\", and \"{}\" is" +
+                " set to true, so it will accept a JWT regardless of its 
\"aud\" (audience) claim.",
+                SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE);
+            jwtConsumerBuilder.setSkipDefaultAudienceValidation();
+        }
 
-        if (expectedIssuer != null)
+        if (expectedIssuer != null) {
             jwtConsumerBuilder.setExpectedIssuer(expectedIssuer);
+        } else {
+            // Reached only when the issuer opt-out is enabled (otherwise 
configuration fails fast above).
+            log.warn("The OAuth broker validator is configured with a JWKS 
endpoint but without \"{}\", and \"{}\" is" +
+                " set to true, so it will accept a JWT bearing any (or no) 
\"iss\" (issuer) claim.",
+                SASL_OAUTHBEARER_EXPECTED_ISSUER, 
SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER);
+        }
 
         this.jwtConsumer = jwtConsumerBuilder
             .setJwsAlgorithmConstraints(DISALLOW_NONE)
diff --git 
a/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidatorTest.java
 
b/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidatorTest.java
index 5f76f508513..6bc992673c2 100644
--- 
a/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidatorTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidatorTest.java
@@ -17,22 +17,37 @@
 
 package org.apache.kafka.common.security.oauthbearer;
 
+import org.apache.kafka.common.config.ConfigException;
 import org.apache.kafka.common.config.SaslConfigs;
 import 
org.apache.kafka.common.security.oauthbearer.internals.secured.AccessTokenBuilder;
 import 
org.apache.kafka.common.security.oauthbearer.internals.secured.CloseableVerificationKeyResolver;
+import org.apache.kafka.common.utils.LogCaptureAppender;
 
+import org.apache.logging.log4j.Level;
 import org.jose4j.jwk.PublicJsonWebKey;
 import org.jose4j.jws.AlgorithmIdentifiers;
 import org.jose4j.lang.InvalidAlgorithmException;
 import org.junit.jupiter.api.Test;
 
+import java.util.List;
 import java.util.Map;
 
+import static 
org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER;
 import static 
org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule.OAUTHBEARER_MECHANISM;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class BrokerJwtValidatorTest extends JwtValidatorTest {
 
+    private static final String ATTACKER_ISSUER = "https://evil.example/";;
+
+    private static final String EXPECTED_ISSUER = "https://legit.example/";;
+
+    private static final String ATTACKER_AUDIENCE = 
"https://evil.example/other-service";;
+
+    private static final String EXPECTED_AUDIENCE = 
"https://legit.example/other-service";;
+
     @Override
     protected JwtValidator createJwtValidator(AccessTokenBuilder builder) {
         CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
builder.jwk().getKey();
@@ -69,10 +84,15 @@ public class BrokerJwtValidatorTest extends 
JwtValidatorTest {
             .jwk(jwk)
             .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
             .addCustomClaim(subClaimName, subject)
+            .addCustomClaim("iss", EXPECTED_ISSUER)
+            .audience(EXPECTED_AUDIENCE)
             .subjectClaimName(subClaimName)
             .subject(null);
         JwtValidator validator = createJwtValidator(tokenBuilder);
-        Map<String, ?> saslConfigs = 
getSaslConfigs(SaslConfigs.SASL_OAUTHBEARER_SUB_CLAIM_NAME, subClaimName);
+        Map<String, ?> saslConfigs = getSaslConfigs(Map.of(
+            SaslConfigs.SASL_OAUTHBEARER_SUB_CLAIM_NAME, subClaimName,
+            SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE)));
         validator.configure(saslConfigs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries());
 
         // Validation should succeed (e.g. signature verification) even if sub 
claim is missing
@@ -81,10 +101,196 @@ public class BrokerJwtValidatorTest extends 
JwtValidatorTest {
         assertEquals(subject, token.principalName());
     }
 
+    @Test
+    public void testRejectMissingIssuerClaimWhenExpectedIssuerSet() throws 
Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        AccessTokenBuilder builder = new AccessTokenBuilder()
+                .jwk(jwk)
+                .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
+                .audience(EXPECTED_AUDIENCE);   // valid audience, but no iss 
claim is set
+        String accessToken = builder.build();
+
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        Map<String, ?> saslConfigs = getSaslConfigs(Map.of(
+                SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+                SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE)));
+        validator.configure(saslConfigs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries());
+
+        // With an expected issuer configured, jose4j requires the "iss" claim 
to be present; a token without it
+        // must be rejected at validation time rather than silently accepted.
+        JwtValidatorException e = assertThrows(JwtValidatorException.class,
+                () -> validator.validate(accessToken));
+        assertErrorMessageContains(e.getMessage(), EXPECTED_ISSUER);
+    }
+
+    @Test
+    public void testConfigureFailsWhenExpectedIssuerUnset() throws Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        assertThrowsWithMessage(ConfigException.class,
+                () -> validator.configure(
+                        
getSaslConfigs(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE)),
+                        OAUTHBEARER_MECHANISM, getJaasConfigEntries()),
+                SASL_OAUTHBEARER_EXPECTED_ISSUER);
+    }
+
+    @Test
+    public void testExpectedIssuerSet() throws Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        AccessTokenBuilder builder = new AccessTokenBuilder()
+                .jwk(jwk)
+                .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
+                .addCustomClaim("iss", ATTACKER_ISSUER)
+                .audience(EXPECTED_AUDIENCE);
+        String accessToken = builder.build();
+
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        Map<String, ?> saslConfigs = getSaslConfigs(Map.of(
+                SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+                SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE)));
+        validator.configure(saslConfigs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries());
+
+        JwtValidatorException e = assertThrows(JwtValidatorException.class,
+                () -> validator.validate(accessToken));
+        // jose4j reports the issuer mismatch; the message includes the 
offending issuer value
+        assertErrorMessageContains(e.getMessage(), ATTACKER_ISSUER);
+    }
+
+    @Test
+    public void testConfigureFailsWhenExpectedAudienceUnset() throws Exception 
{
+        PublicJsonWebKey jwk = createRsaJwk();
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        assertThrowsWithMessage(ConfigException.class,
+                () -> validator.configure(
+                        getSaslConfigs(SASL_OAUTHBEARER_EXPECTED_ISSUER, 
EXPECTED_ISSUER),
+                        OAUTHBEARER_MECHANISM, getJaasConfigEntries()),
+                SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE);
+    }
+
+    @Test
+    public void testRejectWrongAudienceWhenExpectedAudienceSet() throws 
Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        AccessTokenBuilder builder = new AccessTokenBuilder()
+                .jwk(jwk)
+                .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
+                .addCustomClaim("iss", EXPECTED_ISSUER)
+                .audience(ATTACKER_AUDIENCE);
+        String accessToken = builder.build();
+
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        Map<String, ?> saslConfigs = getSaslConfigs(Map.of(
+                SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+                SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE)));
+        validator.configure(saslConfigs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries());
+
+        JwtValidatorException e = assertThrows(JwtValidatorException.class,
+                () -> validator.validate(accessToken));
+        // jose4j reports the audience mismatch; the message includes the 
offending audience value
+        assertErrorMessageContains(e.getMessage(), ATTACKER_AUDIENCE);
+    }
+
+    @Test
+    public void testAllowUnverifiedAudience() throws Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        AccessTokenBuilder builder = new AccessTokenBuilder()
+                .jwk(jwk)
+                .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
+                .addCustomClaim("iss", EXPECTED_ISSUER)
+                .audience(ATTACKER_AUDIENCE);   // an audience that matches no 
expected audience
+        String accessToken = builder.build();
+
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        Map<String, ?> saslConfigs = getSaslConfigs(Map.of(
+                SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+                SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE, true));
+        validator.configure(saslConfigs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries());
+
+        OAuthBearerToken token = validator.validate(accessToken);
+        assertEquals(builder.subject(), token.principalName());
+    }
+
+    @Test
+    public void testAllowUnverifiedIssuer() throws Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        AccessTokenBuilder builder = new AccessTokenBuilder()
+                .jwk(jwk)
+                .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
+                .addCustomClaim("iss", ATTACKER_ISSUER)   // an issuer that 
matches no expected issuer
+                .audience(EXPECTED_AUDIENCE);
+        String accessToken = builder.build();
+
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        Map<String, ?> saslConfigs = getSaslConfigs(Map.of(
+                SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE),
+                SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER, true));
+        validator.configure(saslConfigs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries());
+
+        OAuthBearerToken token = validator.validate(accessToken);
+        assertEquals(builder.subject(), token.principalName());
+    }
+
+    @Test
+    public void testAllowUnverifiedIssuerAndAudience() throws Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        AccessTokenBuilder builder = new AccessTokenBuilder()
+                .jwk(jwk)
+                .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
+                .addCustomClaim("iss", ATTACKER_ISSUER)   // neither the 
issuer nor the audience matches anything expected
+                .audience(ATTACKER_AUDIENCE);
+        String accessToken = builder.build();
+
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        // Neither expected.issuer nor expected.audience is set; both opt-outs 
are enabled so configuration succeeds.
+        Map<String, ?> saslConfigs = getSaslConfigs(Map.of(
+                SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER, true,
+                SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE, true));
+
+        try (LogCaptureAppender appender = 
LogCaptureAppender.createAndRegister(BrokerJwtValidator.class)) {
+            appender.setClassLogger(BrokerJwtValidator.class, Level.WARN);
+            validator.configure(saslConfigs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries());
+
+            // Each opt-out path must log a WARN naming the config that 
disabled the check, so the weakened
+            // posture is visible in the broker logs.
+            List<String> warnings = appender.getMessages("WARN");
+            assertTrue(warnings.stream().anyMatch(m -> 
m.contains(SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER)),
+                    "Expected a WARN mentioning " + 
SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_ISSUER + ", got " + warnings);
+            assertTrue(warnings.stream().anyMatch(m -> 
m.contains(SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE)),
+                    "Expected a WARN mentioning " + 
SaslConfigs.SASL_OAUTHBEARER_ALLOW_UNVERIFIED_AUDIENCE + ", got " + warnings);
+        }
+
+        // With both opt-outs in effect, a token whose issuer and audience 
match nothing configured is still accepted.
+        OAuthBearerToken token = validator.validate(accessToken);
+        assertEquals(builder.subject(), token.principalName());
+    }
+
+    @Test
+    public void testConfigureFailsWhenNeitherExpectedSet() throws Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        CloseableVerificationKeyResolver resolver = (jws, nestingContext) -> 
jwk.getKey();
+        BrokerJwtValidator validator = new BrokerJwtValidator(resolver);
+        // With neither expected value set and no opt-out, configuration must 
fail fast. The audience guard runs
+        // first, so its message is the one that surfaces.
+        assertThrowsWithMessage(ConfigException.class,
+                () -> validator.configure(getSaslConfigs(), 
OAUTHBEARER_MECHANISM, getJaasConfigEntries()),
+                SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE);
+    }
+
     private void testEncryptionAlgorithm(PublicJsonWebKey jwk, String alg) 
throws Exception {
-        AccessTokenBuilder builder = new 
AccessTokenBuilder().jwk(jwk).alg(alg);
+        AccessTokenBuilder builder = new AccessTokenBuilder().jwk(jwk).alg(alg)
+                .addCustomClaim("iss", 
EXPECTED_ISSUER).audience(EXPECTED_AUDIENCE);
         JwtValidator validator = createJwtValidator(builder);
-        validator.configure(getSaslConfigs(), OAUTHBEARER_MECHANISM, 
getJaasConfigEntries());
+        validator.configure(getSaslConfigs(Map.of(
+                        SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+                        SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE))),
+                OAUTHBEARER_MECHANISM, getJaasConfigEntries());
         String accessToken = builder.build();
         OAuthBearerToken token = validator.validate(accessToken);
 
diff --git 
a/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/DefaultJwtValidatorTest.java
 
b/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/DefaultJwtValidatorTest.java
index cf3754a77ac..a8e95ecfb4d 100644
--- 
a/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/DefaultJwtValidatorTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/DefaultJwtValidatorTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.kafka.common.security.oauthbearer;
 
+import org.apache.kafka.common.config.ConfigException;
 import org.apache.kafka.common.config.SaslConfigs;
 import org.apache.kafka.common.config.internals.BrokerSecurityConfigs;
 import 
org.apache.kafka.common.security.oauthbearer.internals.secured.AccessTokenBuilder;
@@ -29,7 +30,10 @@ import org.jose4j.jwk.PublicJsonWebKey;
 import org.jose4j.jws.AlgorithmIdentifiers;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static 
org.apache.kafka.common.config.internals.BrokerSecurityConfigs.ALLOWED_SASL_OAUTHBEARER_URLS_CONFIG;
@@ -37,9 +41,16 @@ import static 
org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModul
 import static org.apache.kafka.test.TestUtils.tempFile;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.fail;
 
 public class DefaultJwtValidatorTest extends OAuthBearerTest {
 
+    private static final String ATTACKER_ISSUER = 
"https://evil.example/attacker";;
+
+    private static final String EXPECTED_ISSUER = "https://idp.legit.example/";;
+
+    private static final String EXPECTED_AUDIENCE = "kafka-cluster";
+
     @AfterEach
     public void tearDown() {
         
System.clearProperty(BrokerSecurityConfigs.ALLOWED_SASL_OAUTHBEARER_URLS_CONFIG);
@@ -50,7 +61,9 @@ public class DefaultJwtValidatorTest extends OAuthBearerTest {
         AccessTokenBuilder builder = new AccessTokenBuilder()
             .alg(AlgorithmIdentifiers.RSA_USING_SHA256);
         CloseableVerificationKeyResolver verificationKeyResolver = 
createVerificationKeyResolver(builder);
-        Map<String, ?> configs = getSaslConfigs();
+        Map<String, ?> configs = getSaslConfigs(Map.of(
+            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE)));
         DefaultJwtValidator jwtValidator = new 
DefaultJwtValidator(verificationKeyResolver);
         assertDoesNotThrow(() -> jwtValidator.configure(configs, 
OAUTHBEARER_MECHANISM, getJaasConfigEntries()));
         assertInstanceOf(BrokerJwtValidator.class, jwtValidator.delegate());
@@ -69,14 +82,20 @@ public class DefaultJwtValidatorTest extends 
OAuthBearerTest {
         PublicJsonWebKey jwk = createRsaJwk();
         AccessTokenBuilder builder = new AccessTokenBuilder()
             .jwk(jwk)
-            .alg(AlgorithmIdentifiers.RSA_USING_SHA256);
+            .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
+            .addCustomClaim("iss", EXPECTED_ISSUER)
+            .audience(EXPECTED_AUDIENCE);
         String accessToken = builder.build();
 
         JsonWebKeySet jwks = new JsonWebKeySet(jwk);
         String jwksJson = 
jwks.toJson(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
         String fileUrl = tempFile(jwksJson).toURI().toString();
         System.setProperty(ALLOWED_SASL_OAUTHBEARER_URLS_CONFIG, fileUrl);
-        Map<String, ?> configs = 
getSaslConfigs(SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_URL, fileUrl);
+        Map<String, Object> rawConfigs = new HashMap<>();
+        rawConfigs.put(SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_URL, 
fileUrl);
+        rawConfigs.put(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, 
EXPECTED_ISSUER);
+        rawConfigs.put(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE));
+        Map<String, ?> configs = getSaslConfigs(rawConfigs);
 
         DefaultJwtValidator jwtValidator = new DefaultJwtValidator();
         assertDoesNotThrow(() -> jwtValidator.configure(configs, 
OAUTHBEARER_MECHANISM, getJaasConfigEntries()));
@@ -84,7 +103,48 @@ public class DefaultJwtValidatorTest extends 
OAuthBearerTest {
         assertDoesNotThrow(() -> jwtValidator.validate(accessToken));
     }
 
+    @Test
+    public void testRejectsAttackerIssuerViaDefaultJwtValidatorWithJwksUrl() 
throws Exception {
+        PublicJsonWebKey jwk = createRsaJwk();
+        AccessTokenBuilder builder = new AccessTokenBuilder()
+                .jwk(jwk)
+                .alg(AlgorithmIdentifiers.RSA_USING_SHA256)
+                .addCustomClaim("iss", ATTACKER_ISSUER);
+        String accessToken = builder.build();
+
+        JsonWebKeySet jwks = new JsonWebKeySet(jwk);
+        String jwksJson = 
jwks.toJson(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
+        String fileUrl = tempFile(jwksJson).toURI().toString();
+        System.setProperty(ALLOWED_SASL_OAUTHBEARER_URLS_CONFIG, fileUrl);
+
+        Map<String, Object> configs = new HashMap<>();
+        configs.put(SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_URL, fileUrl);
+        // expected.issuer intentionally omitted; expected.audience is set so 
the issuer check is the one that fails
+        configs.put(SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE));
+
+        assertSecurelyRejected(() -> {
+            DefaultJwtValidator validator = new DefaultJwtValidator();
+            validator.configure(getSaslConfigs(configs), 
OAUTHBEARER_MECHANISM, getJaasConfigEntries());
+            validator.validate(accessToken);
+        }, "an attacker-issuer token via DefaultJwtValidator with 
jwks.endpoint.url set (expected.issuer unset)");
+    }
+
     private CloseableVerificationKeyResolver 
createVerificationKeyResolver(AccessTokenBuilder builder) {
         return (jws, nestingContext) -> builder.jwk().getPublicKey();
     }
+
+    private void assertSecurelyRejected(Executable flow, String what) {
+        Throwable thrown = null;
+        try {
+            flow.execute();
+        } catch (Throwable t) {
+            thrown = t;
+        }
+        if (thrown == null) {
+            fail("INSECURE: " + what + " was accepted");
+        } else if (!(thrown instanceof JwtValidatorException || thrown 
instanceof ConfigException)) {
+            fail("Unexpected failure validating " + what + ": " + thrown, 
thrown);
+        }
+        // else: ConfigException or JwtValidatorException -> securely rejected 
-> pass
+    }
 }
diff --git 
a/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/OAuthBearerValidatorCallbackHandlerTest.java
 
b/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/OAuthBearerValidatorCallbackHandlerTest.java
index 96ef6aecc6e..0e17df1ee1d 100644
--- 
a/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/OAuthBearerValidatorCallbackHandlerTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/common/security/oauthbearer/OAuthBearerValidatorCallbackHandlerTest.java
@@ -59,6 +59,8 @@ public class OAuthBearerValidatorCallbackHandlerTest extends 
OAuthBearerTest {
 
     private static final String EXPECTED_ISSUER = "https://idp.legit.example/";;
 
+    private static final String EXPECTED_AUDIENCE = "kafka-cluster";
+
     @AfterEach
     public void tearDown() {
         System.clearProperty(ALLOWED_SASL_OAUTHBEARER_URLS_CONFIG);
@@ -209,7 +211,8 @@ public class OAuthBearerValidatorCallbackHandlerTest 
extends OAuthBearerTest {
 
         Map<String, ?> configs = getSaslConfigs(Map.of(
             SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_URL, fileUrl,
-            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER));
+            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+            SASL_OAUTHBEARER_EXPECTED_AUDIENCE, List.of(EXPECTED_AUDIENCE)));
 
         OAuthBearerValidatorCallbackHandler handler = new 
OAuthBearerValidatorCallbackHandler();
         assertDoesNotThrow(() -> handler.configure(configs, 
OAUTHBEARER_MECHANISM, getJaasConfigEntries()));
@@ -227,6 +230,32 @@ public class OAuthBearerValidatorCallbackHandlerTest 
extends OAuthBearerTest {
         }
     }
 
+    @Test
+    public void testFailsToStartWhenExpectedAudienceMissingWithJwksUrl() {
+        // A broker validator handler configured with a JWKS endpoint URL and 
expected.issuer but no expected.audience
+        // must fail to start, rather than come up and accept tokens whose 
audience cannot be verified.
+        Map<String, ?> configs = getSaslConfigs(Map.of(
+            SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_URL, 
"https://example.com/jwks";,
+            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER));
+        OAuthBearerValidatorCallbackHandler handler = new 
OAuthBearerValidatorCallbackHandler();
+        assertThrowsWithMessage(ConfigException.class,
+            () -> handler.configure(configs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries()),
+            SASL_OAUTHBEARER_EXPECTED_AUDIENCE);
+    }
+
+    @Test
+    public void testFailsToStartWhenExpectedIssuerMissingWithJwksUrl() {
+        // A broker validator handler configured with a JWKS endpoint URL and 
expected.audience but no expected.issuer
+        // must fail to start, rather than come up and accept tokens whose 
issuer cannot be verified.
+        Map<String, ?> configs = getSaslConfigs(Map.of(
+            SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_URL, 
"https://example.com/jwks";,
+            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE, 
List.of(EXPECTED_AUDIENCE)));
+        OAuthBearerValidatorCallbackHandler handler = new 
OAuthBearerValidatorCallbackHandler();
+        assertThrowsWithMessage(ConfigException.class,
+            () -> handler.configure(configs, OAUTHBEARER_MECHANISM, 
getJaasConfigEntries()),
+            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER);
+    }
+
     @Test
     public void testConfigureAcceptsCustomValidatorClass() {
         // A custom (non-default) validator class must be accepted; the 
startup check only applies
@@ -244,7 +273,10 @@ public class OAuthBearerValidatorCallbackHandlerTest 
extends OAuthBearerTest {
     private void assertInvalidAccessTokenFails(String accessToken, String 
expectedMessageSubstring) throws Exception {
         AccessTokenBuilder builder = new AccessTokenBuilder()
             .alg(AlgorithmIdentifiers.RSA_USING_SHA256);
-        Map<String, ?> configs = getSaslConfigs();
+        // The injected resolver routes to BrokerJwtValidator, which requires 
expected.issuer and expected.audience.
+        Map<String, ?> configs = getSaslConfigs(Map.of(
+            SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER, EXPECTED_ISSUER,
+            SASL_OAUTHBEARER_EXPECTED_AUDIENCE, List.of(EXPECTED_AUDIENCE)));
         CloseableVerificationKeyResolver verificationKeyResolver = 
createVerificationKeyResolver(builder);
         JwtValidator jwtValidator = 
createJwtValidator(verificationKeyResolver);
 
diff --git 
a/core/src/test/scala/integration/kafka/api/ClientOAuthIntegrationTest.scala 
b/core/src/test/scala/integration/kafka/api/ClientOAuthIntegrationTest.scala
index d1b2161f7cb..c7e26c1be7d 100644
--- a/core/src/test/scala/integration/kafka/api/ClientOAuthIntegrationTest.scala
+++ b/core/src/test/scala/integration/kafka/api/ClientOAuthIntegrationTest.scala
@@ -95,6 +95,7 @@ class ClientOAuthIntegrationTest extends 
IntegrationTestHarness with SaslSetup {
 
     
serverConfig.setProperty(s"$listenerNamePrefix.oauthbearer.${SaslConfigs.SASL_JAAS_CONFIG}",
 s"${classOf[OAuthBearerLoginModule].getName} required ;")
     
serverConfig.setProperty(s"$listenerNamePrefix.oauthbearer.${SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE}",
 issuerId)
+    
serverConfig.setProperty(s"$listenerNamePrefix.oauthbearer.${SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER}",
 mockOAuthServer.issuerUrl(issuerId).toString)
     
serverConfig.setProperty(s"$listenerNamePrefix.oauthbearer.${SaslConfigs.SASL_OAUTHBEARER_JWKS_ENDPOINT_URL}",
 jwksUrl)
     
serverConfig.setProperty(s"$listenerNamePrefix.oauthbearer.${BrokerSecurityConfigs.SASL_SERVER_CALLBACK_HANDLER_CLASS_CONFIG}",
 classOf[OAuthBearerValidatorCallbackHandler].getName)
 
diff --git a/docs/getting-started/upgrade.md b/docs/getting-started/upgrade.md
index c6d3052059c..04f79cdb95b 100644
--- a/docs/getting-started/upgrade.md
+++ b/docs/getting-started/upgrade.md
@@ -40,6 +40,7 @@ type: docs
     `kafka.server:type=group-coordinator-metrics,name=offset-count` instead of 
`kafka.coordinator.group:type=GroupMetadataManager,name=NumOffsets`, and
     
`kafka.server:type=group-coordinator-metrics,name=classic-group-count,state={PreparingRebalance|CompletingRebalance|Stable|Dead|Empty}`
 instead of the 
`kafka.coordinator.group:type=GroupMetadataManager,name=NumGroups{PreparingRebalance|CompletingRebalance|Stable|Dead|Empty}`
 metrics.
     For further details, please refer to 
[KIP-1301](https://cwiki.apache.org/confluence/x/Z5U8G).
+  * The broker-side OAUTHBEARER JWT validator now fails fast at startup when a 
JWKS endpoint (`sasl.oauthbearer.jwks.endpoint.url`) is configured but 
`sasl.oauthbearer.expected.audience` or `sasl.oauthbearer.expected.issuer` is 
not set. Brokers that previously started without these settings will now fail 
to start until they are configured. To intentionally accept tokens regardless 
of their audience or issuer, set the new 
`sasl.oauthbearer.allow.unverified.audience` or `sasl.oauthbearer.a [...]
 
 ## Upgrading to 4.3.0
 


Reply via email to