Skip to content

KAFKA-18573: Add support for OAuth jwt-bearer grant type#19754

Merged
lianetm merged 25 commits into
apache:trunkfrom
kirktrue:KAFKA-18573-add-oauth-jwt-bearer-support
Jun 4, 2025
Merged

KAFKA-18573: Add support for OAuth jwt-bearer grant type#19754
lianetm merged 25 commits into
apache:trunkfrom
kirktrue:KAFKA-18573-add-oauth-jwt-bearer-support

Conversation

@kirktrue

@kirktrue kirktrue commented May 18, 2025

Copy link
Copy Markdown
Contributor

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:

Reviewers: Manikumar Reddy manikumar@confluent.io, Lianet Magrans lmagrans@confluent.io, Chia-Ping Tsai chia7712@gmail.com

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.

Co-Authored-By: Zachary Hamilton <77027819+zacharydhamilton@users.noreply.github.com>
@github-actions github-actions Bot added triage PRs from the community tools clients labels May 18, 2025
@kirktrue

Copy link
Copy Markdown
Contributor Author

cc @lianetm @omkreddy @zacharydhamilton

@github-actions github-actions Bot added the core Kafka Broker label May 20, 2025
@github-actions

Copy link
Copy Markdown

A label of 'needs-attention' was automatically added to this PR in order to raise the
attention of the committers. Once this issue has been triaged, the triage label
should be removed to prevent this automation from happening again.

@github-actions

Copy link
Copy Markdown

A label of 'needs-attention' was automatically added to this PR in order to raise the
attention of the committers. Once this issue has been triaged, the triage label
should be removed to prevent this automation from happening again.

Comment on lines +87 to +92
if (algorithm.equalsIgnoreCase(TOKEN_SIGNING_ALGORITHM_RS256)) {
return Signature.getInstance("SHA256withRSA");
} else if (algorithm.equalsIgnoreCase(TOKEN_SIGNING_ALGORITHM_ES256)) {
return Signature.getInstance("SHA256withECDSA");
} else {
throw new NoSuchAlgorithmException(String.format("Unsupported signing algorithm: %s", algorithm));

@emasab emasab May 28, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be future proof, could we support all JOSE hash lengths for RS and ES: RS256, RS384, RS512, ES256, ES384, ES512.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has this been addressed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I spoke with @emasab offline and he said he's OK postponing additional algorithms until a later release.

@github-actions github-actions Bot removed the triage PRs from the community label May 29, 2025
Comment thread clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java Outdated

@lianetm lianetm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @kirktrue! Here are some comments from a partial review

public static final String DEFAULT_CLIENT_SASL_OAUTHBEARER_JWT_VALIDATOR_CLASS = "org.apache.kafka.common.security.oauthbearer.ClientJwtValidator";
public static final String SASL_OAUTHBEARER_JWT_VALIDATOR_CLASS_DOC = "<p>The fully-qualified class name of a <code>JwtValidator</code> implementation used to"
+ " validate the JWT from the identity provider.</p>"
+ "<p>The default configuration value represents a class that maintains backward compatibility with previous versions of"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to refer to the "default validator" here instead of "default configuration value"?? (referring to the "default configuration value" is confusing, because there is really no config for the DefaultJwtValidator right? (only for DEFAULT_BROKER.. and DEFAULT_CLIENT...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java Outdated
+ "<p>The default configuration value represents a class that maintains backward compatibility with previous versions of"
+ " Apache Kafka. The default implementation uses the configuration to determine which concrete implementation to create."
+ "<p>Other implementations that are provided include:</p>"
+ "<ul>"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like we use tags in config docs like these elsewhere (I may be missing something), and it surely shows polluted with the literal tags when looking at the java doc for this var usages. Is this intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is rare; there are only nine occurrences other than those in this PR. These two client configuration were my inspiration:

+ " included in the token request. If provided, it should match one or more scopes configured in the identity provider.</p>"
+ "<p>"
+ "The scope was previously stored as part of the <code>sasl.jaas.config</code> configuration with the key <code>scope</code>."
+ " For backward compatibility, the <code>scope</code> JAAS option can still be used, but it is deprecated and will be removed in a future version."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sasl.jaas.config is not really marked as deprecated yet, right? Do we plan to do it as follow-up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sasl.jaas.config itself is not deprecated. Just the OAuth layer's use of JAAS options.

I'm not entirely sure how to deprecate JAAS options because they're not listed anywhere explicitly like configuration are 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, interesting case then. Should we then add a note on the docs of the sasl.jaas.config config itself? (so users know that the oauth options of it are deprecated and they should use the sasl.oauthbearer properties instead?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing (Do we want to add the note the sasl.jaas.config? about the deprecated options and how the new props will take precedence)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a statement about that under that line:

Order of precedence:

  • sasl.oauthbearer.client.credentials.client.id from configuration
  • clientId from JAAS

Or are you referring to something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referring to a note on the jaas.config doc, to add the note on the one being deprecated, not only the one being added

Comment thread clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java Outdated
* should be specified with a listener-based property:
*
* <pre>
* listener.name.<listener name>.oauthbearer.sasl.oauthbearer.jwt.retriever.class=org.apache.kafka.common.security.oauthbearer.JwtBearerJwtRetriever

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uhm having unescaped "<listener name>" within the <pre> will probably mess it up?

(ha ha, I had to escape the tags myself on this comment to show properly)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

* @param file File containing the source data
* @param contents Data from file; could be zero length but not {@code null}
*/
T transform(File file, String contents);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems redundant that we need to pass the File and the contents, is it needed? (or could we consider passing the file only, and retrieve the contents from it?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The File is used for metadata (size, last modified time, etc.) and the String is used for the actual contents.

Because CachedFile.snapshot() performs the file loading step and provides the result to transform(), it's less work for the different Transformer implementations.

kirktrue and others added 3 commits May 29, 2025 14:49
…gs.java

Co-authored-by: Lianet Magrans <98415067+lianetm@users.noreply.github.com>
…arer/DefaultJwtRetriever.java

Co-authored-by: Lianet Magrans <98415067+lianetm@users.noreply.github.com>
…gs.java

Co-authored-by: Lianet Magrans <98415067+lianetm@users.noreply.github.com>
@kirktrue kirktrue requested review from lianetm and omkreddy June 3, 2025 00:20

@lianetm lianetm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kirktrue ! A few more comments

+ " included in the token request. If provided, it should match one or more scopes configured in the identity provider.</p>"
+ "<p>"
+ "The scope was previously stored as part of the <code>sasl.jaas.config</code> configuration with the key <code>scope</code>."
+ " For backward compatibility, the <code>scope</code> JAAS option can still be used, but it is deprecated and will be removed in a future version."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing (Do we want to add the note the sasl.jaas.config? about the deprecated options and how the new props will take precedence)

public static final String SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL = "sasl.oauthbearer.token.endpoint.url";
public static final String SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL_DOC = "The URL for the OAuth/OIDC identity provider. If the URL is HTTP(S)-based, it is the issuer's token"
+ " endpoint URL to which requests will be made to login based on the configuration in " + SASL_JAAS_CONFIG + ". If the URL is file-based, it"
+ " endpoint URL to which requests will be made to login based on the configured <code>JwtRetriever</code>. If the URL is file-based, it"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we reference the SASL_OAUTHBEARER_JWT_RETRIEVER_CLASS config here instead of the class name? Seeing the related config name is probably more helpful (similar to what was done before for the old config)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I made the change.

Function<String, String> configValueGetter,
Function<String, String> jaasValueGetter) {
if (cu.containsKey(configName)) {
return configValueGetter.apply(configName);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we consider logging a warn in case cu.containsKey(configName) && jou.containsKey(jaasName)?
Otherwise it will go silently that the user has 2 conflicting properties, and that the jaas oauth option will be ignored

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more logging in this case. I struggled with the wording a bit, so happy to take suggestions.

}

@Override
public void configure(Map<String, ?> configs, String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any test for this configure logic on the JwtBearerJwtRetriever, just checking it is coming in the following PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a few unit tests for the configure().

Comment on lines +87 to +92
if (algorithm.equalsIgnoreCase(TOKEN_SIGNING_ALGORITHM_RS256)) {
return Signature.getInstance("SHA256withRSA");
} else if (algorithm.equalsIgnoreCase(TOKEN_SIGNING_ALGORITHM_ES256)) {
return Signature.getInstance("SHA256withECDSA");
} else {
throw new NoSuchAlgorithmException(String.format("Unsupported signing algorithm: %s", algorithm));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has this been addressed?


public class JwtResponseParser {

private static final String[] JSON_PATHS = new String[] {"/access_token", "/id_token"};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we intend to use id_tokens? I was expecting only access_token since those are the ones intended for API authorization (but I could be missing why we are expecting to use ID tokens?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the specs, access_token is correct. However, in my testing with GCP, the access_token was not present and only id_token was there as a JWT. Let me perform my testing against GCP again and see if I can get more clarity.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, let's get to the bottom of it, but we can take it as follow-up if it needs more investigation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need some more investigation. I tested against GCP and, depending on the claims that I include in the assertion JWT, the response includes either access_token or id_token.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the RFC it should return an access token, here it says:

This document defines how a JWT Bearer Token can be used to request
an access token when a client wishes to utilize an existing trust
relationship

so I think it's fine to look first for access_token and then for the id_token.

@omkreddy omkreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kirktrue Thanks for the PR. LGTM

@lianetm lianetm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! LGTM.

@lianetm lianetm merged commit 1e91790 into apache:trunk Jun 4, 2025
25 checks passed
Mirai1129 pushed a commit to Mirai1129/kafka that referenced this pull request Jun 5, 2025
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>
@kirktrue kirktrue deleted the KAFKA-18573-add-oauth-jwt-bearer-support branch June 18, 2025 15:25
@chia7712

chia7712 commented Jul 2, 2026

Copy link
Copy Markdown
Member
    @Override
    public void configure(Map<String, ?> configs, String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries) {
        ConfigurationUtils cu = new ConfigurationUtils(configs, saslMechanism);
        List<String> expectedAudiencesList = cu.get(SASL_OAUTHBEARER_EXPECTED_AUDIENCE);
        Set<String> expectedAudiences = expectedAudiencesList != null ? Set.copyOf(expectedAudiencesList) : null;
        Integer clockSkew = cu.validateInteger(SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS, false);
        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);

        CloseableVerificationKeyResolver verificationKeyResolver = verificationKeyResolverOpt.orElseGet(
            () -> VerificationKeyResolverFactory.get(configs, saslMechanism, jaasConfigEntries)
        );

Sorry for bumping this old PR :)

Should we close verificationKeyResolver ? I observed it in reviewing #22520

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants