Skip to content

Commit 1e91790

Browse files
kirktruezdhrclianetm
authored
KAFKA-18573: Add support for OAuth jwt-bearer grant type (#19754)
Adding support for the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type (AKA `jwt-bearer`). Includes further refactoring of the existing OAuth layer and addition of generic JWT assertion layer that can be leveraged in the future. This constitutes the main piece of the JWT Bearer grant type support. Forthcoming commits/PRs will include improvements for both the `client_credentials` and `jwt-bearer` grant types in the following areas: * Integration test coverage (KAFKA-19153) * Unit test coverage (KAFKA-19308) * Top-level documentation (KAFKA-19152) * Improvements to and documentation for `OAuthCompatibilityTool` (KAFKA-19307) Reviewers: Manikumar Reddy <manikumar@confluent.io>, Lianet Magrans <lmagrans@confluent.io> --------- Co-authored-by: Zachary Hamilton <77027819+zacharydhamilton@users.noreply.github.com> Co-authored-by: Lianet Magrans <98415067+lianetm@users.noreply.github.com>
1 parent ee3f80e commit 1e91790

67 files changed

Lines changed: 4232 additions & 1258 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

checkstyle/import-control.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
<subpackage name="oauthbearer">
146146
<allow pkg="com.fasterxml.jackson.databind" />
147147
<allow pkg="org.jose4j" />
148+
<allow pkg="javax.crypto"/>
148149
</subpackage>
149150
</subpackage>
150151

clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java

Lines changed: 186 additions & 2 deletions
Large diffs are not rendered by default.

clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ public class BrokerSecurityConfigs {
136136
// The allowlist of the SASL OAUTHBEARER endpoints
137137
public static final String ALLOWED_SASL_OAUTHBEARER_URLS_CONFIG = "org.apache.kafka.sasl.oauthbearer.allowed.urls";
138138
public static final String ALLOWED_SASL_OAUTHBEARER_URLS_DEFAULT = "";
139+
140+
public static final String ALLOWED_SASL_OAUTHBEARER_FILES_CONFIG = "org.apache.kafka.sasl.oauthbearer.allowed.files";
141+
public static final String ALLOWED_SASL_OAUTHBEARER_FILES_DEFAULT = "";
142+
139143
public static final ConfigDef CONFIG_DEF = new ConfigDef()
140144
// General Security Configuration
141145
.define(BrokerSecurityConfigs.CONNECTIONS_MAX_REAUTH_MS_CONFIG, LONG, BrokerSecurityConfigs.DEFAULT_CONNECTIONS_MAX_REAUTH_MS, MEDIUM, BrokerSecurityConfigs.CONNECTIONS_MAX_REAUTH_MS_DOC)
@@ -190,6 +194,22 @@ public class BrokerSecurityConfigs {
190194
.define(SaslConfigs.SASL_LOGIN_READ_TIMEOUT_MS, INT, null, LOW, SaslConfigs.SASL_LOGIN_READ_TIMEOUT_MS_DOC)
191195
.define(SaslConfigs.SASL_LOGIN_RETRY_BACKOFF_MAX_MS, LONG, SaslConfigs.DEFAULT_SASL_LOGIN_RETRY_BACKOFF_MAX_MS, LOW, SaslConfigs.SASL_LOGIN_RETRY_BACKOFF_MAX_MS_DOC)
192196
.define(SaslConfigs.SASL_LOGIN_RETRY_BACKOFF_MS, LONG, SaslConfigs.DEFAULT_SASL_LOGIN_RETRY_BACKOFF_MS, LOW, SaslConfigs.SASL_LOGIN_RETRY_BACKOFF_MS_DOC)
197+
.define(SaslConfigs.SASL_OAUTHBEARER_JWT_RETRIEVER_CLASS, CLASS, SaslConfigs.DEFAULT_SASL_OAUTHBEARER_JWT_RETRIEVER_CLASS, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_JWT_RETRIEVER_CLASS_DOC)
198+
.define(SaslConfigs.SASL_OAUTHBEARER_JWT_VALIDATOR_CLASS, CLASS, SaslConfigs.DEFAULT_SASL_OAUTHBEARER_JWT_VALIDATOR_CLASS, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_JWT_VALIDATOR_CLASS_DOC)
199+
.define(SaslConfigs.SASL_OAUTHBEARER_SCOPE, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_SCOPE_DOC)
200+
.define(SaslConfigs.SASL_OAUTHBEARER_CLIENT_CREDENTIALS_CLIENT_ID, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_CLIENT_CREDENTIALS_CLIENT_ID_DOC)
201+
.define(SaslConfigs.SASL_OAUTHBEARER_CLIENT_CREDENTIALS_CLIENT_SECRET, PASSWORD, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_CLIENT_CREDENTIALS_CLIENT_SECRET_DOC)
202+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_ALGORITHM, STRING, SaslConfigs.DEFAULT_SASL_OAUTHBEARER_ASSERTION_ALGORITHM, ConfigDef.CaseInsensitiveValidString.in("ES256", "RS256"), MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_ALGORITHM_DOC)
203+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_AUD, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_AUD_DOC)
204+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_EXP_SECONDS, INT, SaslConfigs.DEFAULT_SASL_OAUTHBEARER_ASSERTION_CLAIM_EXP_SECONDS, ConfigDef.Range.between(0, 86400), LOW, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_EXP_SECONDS_DOC)
205+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_ISS, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_ISS_DOC)
206+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_JTI_INCLUDE, BOOLEAN, SaslConfigs.DEFAULT_SASL_OAUTHBEARER_ASSERTION_CLAIM_JTI_INCLUDE, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_JTI_INCLUDE_DOC)
207+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_NBF_SECONDS, INT, SaslConfigs.DEFAULT_SASL_OAUTHBEARER_ASSERTION_CLAIM_NBF_SECONDS, ConfigDef.Range.between(0, 3600), LOW, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_NBF_SECONDS_DOC)
208+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_SUB, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_CLAIM_SUB_DOC)
209+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_FILE, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_FILE_DOC)
210+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_PRIVATE_KEY_FILE, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_PRIVATE_KEY_FILE_DOC)
211+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_PRIVATE_KEY_PASSPHRASE, PASSWORD, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_PRIVATE_KEY_PASSPHRASE_DOC)
212+
.define(SaslConfigs.SASL_OAUTHBEARER_ASSERTION_TEMPLATE_FILE, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_ASSERTION_TEMPLATE_FILE_DOC)
193213
.define(SaslConfigs.SASL_OAUTHBEARER_SCOPE_CLAIM_NAME, STRING, SaslConfigs.DEFAULT_SASL_OAUTHBEARER_SCOPE_CLAIM_NAME, LOW, SaslConfigs.SASL_OAUTHBEARER_SCOPE_CLAIM_NAME_DOC)
194214
.define(SaslConfigs.SASL_OAUTHBEARER_SUB_CLAIM_NAME, STRING, SaslConfigs.DEFAULT_SASL_OAUTHBEARER_SUB_CLAIM_NAME, LOW, SaslConfigs.SASL_OAUTHBEARER_SUB_CLAIM_NAME_DOC)
195215
.define(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL, STRING, null, MEDIUM, SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL_DOC)

clients/src/main/java/org/apache/kafka/common/security/oauthbearer/internals/secured/BrokerJwtValidator.java renamed to clients/src/main/java/org/apache/kafka/common/security/oauthbearer/BrokerJwtValidator.java

Lines changed: 57 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515
* limitations under the License.
1616
*/
1717

18-
package org.apache.kafka.common.security.oauthbearer.internals.secured;
18+
package org.apache.kafka.common.security.oauthbearer;
1919

20-
import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken;
20+
import org.apache.kafka.common.security.oauthbearer.internals.secured.BasicOAuthBearerToken;
21+
import org.apache.kafka.common.security.oauthbearer.internals.secured.ClaimValidationUtils;
22+
import org.apache.kafka.common.security.oauthbearer.internals.secured.CloseableVerificationKeyResolver;
23+
import org.apache.kafka.common.security.oauthbearer.internals.secured.ConfigurationUtils;
24+
import org.apache.kafka.common.security.oauthbearer.internals.secured.SerializedJwt;
25+
import org.apache.kafka.common.security.oauthbearer.internals.secured.VerificationKeyResolverFactory;
2126

2227
import org.jose4j.jwt.JwtClaims;
2328
import org.jose4j.jwt.MalformedClaimException;
@@ -27,14 +32,23 @@
2732
import org.jose4j.jwt.consumer.JwtConsumer;
2833
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
2934
import org.jose4j.jwt.consumer.JwtContext;
30-
import org.jose4j.keys.resolvers.VerificationKeyResolver;
3135
import org.slf4j.Logger;
3236
import org.slf4j.LoggerFactory;
3337

3438
import java.util.Collection;
3539
import java.util.Collections;
40+
import java.util.List;
41+
import java.util.Map;
42+
import java.util.Optional;
3643
import java.util.Set;
3744

45+
import javax.security.auth.login.AppConfigurationEntry;
46+
47+
import static org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS;
48+
import static org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE;
49+
import static org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER;
50+
import static org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_SCOPE_CLAIM_NAME;
51+
import static org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_SUB_CLAIM_NAME;
3852
import static org.jose4j.jwa.AlgorithmConstraints.DISALLOW_NONE;
3953

4054
/**
@@ -43,16 +57,18 @@
4357
* from the client, but ultimately from posting the client credentials to the OAuth/OIDC provider's
4458
* token endpoint.
4559
*
46-
* The validation steps performed (primary by the jose4j library) are:
60+
* The validation steps performed (primarily by the jose4j library) are:
4761
*
4862
* <ol>
4963
* <li>
5064
* Basic structural validation of the <code>b64token</code> value as defined in
5165
* <a href="https://tools.ietf.org/html/rfc6750#section-2.1">RFC 6750 Section 2.1</a>
5266
* </li>
53-
* <li>Basic conversion of the token into an in-memory data structure</li>
5467
* <li>
55-
* Presence of scope, <code>exp</code>, subject, <code>iss</code>, and
68+
* Basic conversion of the token into an in-memory data structure
69+
* </li>
70+
* <li>
71+
* Presence of <code>scope</code>, <code>exp</code>, <code>subject</code>, <code>iss</code>, and
5672
* <code>iat</code> claims
5773
* </li>
5874
* <li>
@@ -61,63 +77,46 @@
6177
* </li>
6278
* </ol>
6379
*/
64-
6580
public class BrokerJwtValidator implements JwtValidator {
6681

6782
private static final Logger log = LoggerFactory.getLogger(BrokerJwtValidator.class);
6883

69-
private final JwtConsumer jwtConsumer;
84+
private final Optional<CloseableVerificationKeyResolver> verificationKeyResolverOpt;
7085

71-
private final String scopeClaimName;
86+
private JwtConsumer jwtConsumer;
7287

73-
private final String subClaimName;
88+
private String scopeClaimName;
89+
90+
private String subClaimName;
7491

7592
/**
76-
* Creates a new {@code BrokerJwtValidator} that will be used by the broker for more
77-
* thorough validation of the JWT.
78-
*
79-
* @param clockSkew The optional value (in seconds) to allow for differences
80-
* between the time of the OAuth/OIDC identity provider and
81-
* the broker. If <code>null</code> is provided, the broker
82-
* and the OAUth/OIDC identity provider are assumed to have
83-
* very close clock settings.
84-
* @param expectedAudiences The (optional) set the broker will use to verify that
85-
* the JWT was issued for one of the expected audiences.
86-
* The JWT will be inspected for the standard OAuth
87-
* <code>aud</code> claim and if this value is set, the
88-
* broker will match the value from JWT's <code>aud</code>
89-
* claim to see if there is an <b>exact</b> match. If there is no
90-
* match, the broker will reject the JWT and authentication
91-
* will fail. May be <code>null</code> to not perform any
92-
* check to verify the JWT's <code>aud</code> claim matches any
93-
* fixed set of known/expected audiences.
94-
* @param expectedIssuer The (optional) value for the broker to use to verify that
95-
* the JWT was created by the expected issuer. The JWT will
96-
* be inspected for the standard OAuth <code>iss</code> claim
97-
* and if this value is set, the broker will match it
98-
* <b>exactly</b> against what is in the JWT's <code>iss</code>
99-
* claim. If there is no match, the broker will reject the JWT
100-
* and authentication will fail. May be <code>null</code> to not
101-
* perform any check to verify the JWT's <code>iss</code> claim
102-
* matches a specific issuer.
103-
* @param verificationKeyResolver jose4j-based {@link VerificationKeyResolver} that is used
104-
* to validate the signature matches the contents of the header
105-
* and payload
106-
* @param scopeClaimName Name of the scope claim to use; must be non-<code>null</code>
107-
* @param subClaimName Name of the subject claim to use; must be
108-
* non-<code>null</code>
109-
*
110-
* @see JwtConsumerBuilder
111-
* @see JwtConsumer
112-
* @see VerificationKeyResolver
93+
* A public, no-args constructor is necessary for instantiation via configuration.
11394
*/
95+
public BrokerJwtValidator() {
96+
this.verificationKeyResolverOpt = Optional.empty();
97+
}
98+
99+
/*
100+
* Package-visible for testing.
101+
*/
102+
BrokerJwtValidator(CloseableVerificationKeyResolver verificationKeyResolver) {
103+
this.verificationKeyResolverOpt = Optional.of(verificationKeyResolver);
104+
}
105+
106+
@Override
107+
public void configure(Map<String, ?> configs, String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries) {
108+
ConfigurationUtils cu = new ConfigurationUtils(configs, saslMechanism);
109+
List<String> expectedAudiencesList = cu.get(SASL_OAUTHBEARER_EXPECTED_AUDIENCE);
110+
Set<String> expectedAudiences = expectedAudiencesList != null ? Set.copyOf(expectedAudiencesList) : null;
111+
Integer clockSkew = cu.validateInteger(SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS, false);
112+
String expectedIssuer = cu.validateString(SASL_OAUTHBEARER_EXPECTED_ISSUER, false);
113+
String scopeClaimName = cu.validateString(SASL_OAUTHBEARER_SCOPE_CLAIM_NAME);
114+
String subClaimName = cu.validateString(SASL_OAUTHBEARER_SUB_CLAIM_NAME);
115+
116+
CloseableVerificationKeyResolver verificationKeyResolver = verificationKeyResolverOpt.orElseGet(
117+
() -> VerificationKeyResolverFactory.get(configs, saslMechanism, jaasConfigEntries)
118+
);
114119

115-
public BrokerJwtValidator(Integer clockSkew,
116-
Set<String> expectedAudiences,
117-
String expectedIssuer,
118-
VerificationKeyResolver verificationKeyResolver,
119-
String scopeClaimName,
120-
String subClaimName) {
121120
final JwtConsumerBuilder jwtConsumerBuilder = new JwtConsumerBuilder();
122121

123122
if (clockSkew != null)
@@ -145,19 +144,19 @@ public BrokerJwtValidator(Integer clockSkew,
145144
*
146145
* @param accessToken Non-<code>null</code> JWT access token
147146
* @return {@link OAuthBearerToken}
148-
* @throws ValidateException Thrown on errors performing validation of given token
147+
* @throws JwtValidatorException Thrown on errors performing validation of given token
149148
*/
150149

151150
@SuppressWarnings("unchecked")
152-
public OAuthBearerToken validate(String accessToken) throws ValidateException {
151+
public OAuthBearerToken validate(String accessToken) throws JwtValidatorException {
153152
SerializedJwt serializedJwt = new SerializedJwt(accessToken);
154153

155154
JwtContext jwt;
156155

157156
try {
158157
jwt = jwtConsumer.process(serializedJwt.getToken());
159158
} catch (InvalidJwtException e) {
160-
throw new ValidateException(String.format("Could not validate the access token: %s", e.getMessage()), e);
159+
throw new JwtValidatorException(String.format("Could not validate the access token: %s", e.getMessage()), e);
161160
}
162161

163162
JwtClaims claims = jwt.getJwtClaims();
@@ -190,13 +189,13 @@ else if (scopeRaw instanceof Collection)
190189
issuedAt);
191190
}
192191

193-
private <T> T getClaim(ClaimSupplier<T> supplier, String claimName) throws ValidateException {
192+
private <T> T getClaim(ClaimSupplier<T> supplier, String claimName) throws JwtValidatorException {
194193
try {
195194
T value = supplier.get();
196195
log.debug("getClaim - {}: {}", claimName, value);
197196
return value;
198197
} catch (MalformedClaimException e) {
199-
throw new ValidateException(String.format("Could not extract the '%s' claim from the access token", claimName), e);
198+
throw new JwtValidatorException(String.format("Could not extract the '%s' claim from the access token", claimName), e);
200199
}
201200
}
202201

@@ -205,5 +204,4 @@ public interface ClaimSupplier<T> {
205204
T get() throws MalformedClaimException;
206205

207206
}
208-
209207
}

0 commit comments

Comments
 (0)