Skip to content

Commit 75044c8

Browse files
hmadisonmstruk
andauthored
Support disabling the Accept header when requesting Json Web Key Sets. (#201)
* Document how to test with Minikube and Helm Signed-off-by: Hunter Madison <[email protected]> * Support disabling the "Accept" header when fetching JWK keys. When attemping to use Kubernetes' api-server as the source of JWK keys for JWT validation, it will only process requests which either do not have the "Accept" header set or requests which have the header set to "application/jwk-set+json". Signed-off-by: Hunter Madison <[email protected]> * Add an example making use of the new configuration option. Signed-off-by: Hunter Madison <[email protected]> * Update JWKSKeyUseTest to match the new constructor definition. Signed-off-by: Hunter Madison <[email protected]> * Address comments from code review. Signed-off-by: Hunter Madison <[email protected]> * Address pull request feedback. Signed-off-by: Hunter Madison <[email protected]> * Back out service account example. Signed-off-by: Hunter Madison <[email protected]> * Added configuration tests to the testsuite + fixed some issues Signed-off-by: Marko Strukelj <[email protected]> --------- Signed-off-by: Hunter Madison <[email protected]> Signed-off-by: Marko Strukelj <[email protected]> Co-authored-by: Marko Strukelj <[email protected]>
1 parent 2c0bf64 commit 75044c8

File tree

32 files changed

+563
-141
lines changed

32 files changed

+563
-141
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -1485,3 +1485,7 @@ The JWT tokens are signed by the authorization server when they are issued. The
14851485
The client may have obtained a new access token, but the Kafka broker has not yet refreshed the public keys from JWKS endpoint resulting in a mismatch. The Kafka Broker will automatically refresh JWT keys if it encounters an unknown `kid`, and the problem will self-correct in this case, you may just need to repeat your request a few times.
14861486

14871487
It can also happen the other way around. Your existing client may still use the refresh token or the access token issued by the previous authorization server instance while the Kafka broker has already refreshed the keys from JWKS endpoint - resulting in a mismatch between the private key used by authorization server to sign the token, and the published public keys (JWKS endpoint). Since the problem is on the client you may need to configure your client with a newly obtained refresh token, or access token. If you configure your client with clientId and secret, it should autocorrect by itself, you just need to restart it.
1488+
1489+
### HTTP 406: Not Acceptable errors.
1490+
1491+
For certain servers setting the `Accept` header on outbound requests to `application/json` can cause the identity provider to reject the request. If that is an issue, you can set `oauth.include.accept.header` to `false` and remove the `Accept` header from outbound requests made by the Kafka server or client.

examples/docker/strimzi-kafka-image/README.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The result is that the most recent Strimzi Kafka OAuth jars and their dependenci
1212
Building
1313
--------
1414

15-
Use `docker build` to build the image:
15+
Run `mvn install` then, use `docker build` to build the image:
1616

1717
docker build -t strimzi/kafka:latest-kafka-3.3.2-oauth .
1818

@@ -69,10 +69,14 @@ You need to retag the built image before so you can push it to Docker Registry:
6969
Actually, Kubernetes Kind supports an even simpler option how to make an image available to Kubernetes:
7070

7171
kind load docker-image strimzi/kafka:latest-kafka-3.3.2-oauth
72+
73+
If you're using minikube, you'll need to run `minikube docker-env` before building the image.
7274

7375
Deploying
7476
---------
7577

78+
## Via the Strimzi Repository
79+
7680
In order for the operator to use your Kafka image, you have to replace the Kafka image coordinates in `packaging/install/cluster-operator/060-Deployment-strimzi-cluster-operator.yaml` in your `strimzi-kafka-operator` project.
7781

7882
This image builds the kafka-3.3.2 replacement image, so we need to replace all occurrences where kafka-3.3.2 is referred to into the proper coordinates to our image:
@@ -88,3 +92,15 @@ It's best to check the `060-Deployment-strimzi-cluster-operator.yaml` file manua
8892

8993
You can now deploy Strimzi Kafka Operator following instructions in [HACKING.md](../../../HACKING.md)
9094

95+
## Via Helm
96+
97+
You can also run the operator via its Helm chart and set the `kafka.image.registry` property to your local registry. As an example, if you've built and tagged the image as `local.dev/strimzi/kafka:0.36.0-kafka-3.5.0 `. You can run it using the operator as:
98+
99+
helm repo add strimzi https://strimzi.io/charts/ --force-update
100+
helm upgrade -i -n strimzi strimzi strimzi/strimzi-kafka-operator \
101+
--version 0.36.0 \
102+
--set watchNamespaces="{default}" \
103+
--set generateNetworkPolicy=false \
104+
--set kafka.image.registry="local.dev" \
105+
--wait \
106+
--create-namespace

examples/kubernetes/README.md

-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ They assume Keycloak is used as an authorization server, with properly configure
3535
A single node Kafka cluster with OAuth 2 authentication with OAuth metrics enabled.
3636
See [README-metrics.md]() for how to setup this example.
3737

38-
3938
### Deploying Keycloak and accessing the Keycloak Admin Console
4039

4140
Before deploying any of the Kafka cluster definitions, you need to deploy a Keycloak instance, and configure the realms with the necessary client definitions.

oauth-client/src/main/java/io/strimzi/kafka/oauth/client/JaasClientOauthLoginCallbackHandler.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public class JaasClientOauthLoginCallbackHandler implements AuthenticateCallback
8282
private SensorKeyProducer tokenSensorKeyProducer;
8383

8484
private final ClientMetricsHandler authenticatorMetrics = new ClientMetricsHandler();
85+
private boolean includeAcceptHeader;
8586

8687
@Override
8788
public void configure(Map<String, ?> configs, String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries) {
@@ -125,6 +126,7 @@ public void configure(Map<String, ?> configs, String saslMechanism, List<AppConf
125126
readTimeout = getReadTimeout(config);
126127
retries = getHttpRetries(config);
127128
retryPauseMillis = getHttpRetryPauseMillis(config, retries);
129+
includeAcceptHeader = config.getValueAsBoolean(Config.OAUTH_INCLUDE_ACCEPT_HEADER, true);
128130
checkConfiguration();
129131

130132
principalExtractor = new PrincipalExtractor(
@@ -164,7 +166,8 @@ public void configure(Map<String, ?> configs, String saslMechanism, List<AppConf
164166
+ "\n readTimeout: " + readTimeout
165167
+ "\n retries: " + retries
166168
+ "\n retryPauseMillis: " + retryPauseMillis
167-
+ "\n enableMetrics: " + enableMetrics);
169+
+ "\n enableMetrics: " + enableMetrics
170+
+ "\n includeAcceptHeader: " + includeAcceptHeader);
168171
}
169172
}
170173

@@ -274,11 +277,11 @@ private void handleCallback(OAuthBearerTokenCallback callback) throws IOExceptio
274277
// we could check if it's a JWT - in that case we could check if it's expired
275278
result = loginWithAccessToken(token, isJwt, principalExtractor);
276279
} else if (refreshToken != null) {
277-
result = loginWithRefreshToken(tokenEndpoint, socketFactory, hostnameVerifier, refreshToken, clientId, clientSecret, isJwt, principalExtractor, scope, audience, connectTimeout, readTimeout, authenticatorMetrics, retries, retryPauseMillis);
280+
result = loginWithRefreshToken(tokenEndpoint, socketFactory, hostnameVerifier, refreshToken, clientId, clientSecret, isJwt, principalExtractor, scope, audience, connectTimeout, readTimeout, authenticatorMetrics, retries, retryPauseMillis, includeAcceptHeader);
278281
} else if (username != null) {
279-
result = loginWithPassword(tokenEndpoint, socketFactory, hostnameVerifier, username, password, clientId, clientSecret, isJwt, principalExtractor, scope, audience, connectTimeout, readTimeout, authenticatorMetrics, retries, retryPauseMillis);
282+
result = loginWithPassword(tokenEndpoint, socketFactory, hostnameVerifier, username, password, clientId, clientSecret, isJwt, principalExtractor, scope, audience, connectTimeout, readTimeout, authenticatorMetrics, retries, retryPauseMillis, includeAcceptHeader);
280283
} else if (clientSecret != null) {
281-
result = loginWithClientSecret(tokenEndpoint, socketFactory, hostnameVerifier, clientId, clientSecret, isJwt, principalExtractor, scope, audience, connectTimeout, readTimeout, authenticatorMetrics, retries, retryPauseMillis);
284+
result = loginWithClientSecret(tokenEndpoint, socketFactory, hostnameVerifier, clientId, clientSecret, isJwt, principalExtractor, scope, audience, connectTimeout, readTimeout, authenticatorMetrics, retries, retryPauseMillis, includeAcceptHeader);
282285
} else {
283286
throw new IllegalStateException("Invalid oauth client configuration - no credentials");
284287
}

oauth-common/src/main/java/io/strimzi/kafka/oauth/common/Config.java

+5
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ public class Config {
7878
/** The name of 'oauth.enable.metrics' config option */
7979
public static final String OAUTH_ENABLE_METRICS = "oauth.enable.metrics";
8080

81+
/**
82+
* Whether http requests should include "application/json" when being sent to the upstream OIDC server.
83+
*/
84+
public static final String OAUTH_INCLUDE_ACCEPT_HEADER = "oauth.include.accept.header";
85+
8186
/** The name of 'oauth.tokens.not.jwt' config option */
8287
@Deprecated
8388
public static final String OAUTH_TOKENS_NOT_JWT = "oauth.tokens.not.jwt";

oauth-common/src/main/java/io/strimzi/kafka/oauth/common/ConfigUtil.java

+18
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,22 @@ public static String getConfigWithFallbackLookup(Config c, String key, String fa
139139
}
140140
return result;
141141
}
142+
143+
/**
144+
* Resolve the configuration value for the key as a string.
145+
* If the key is not present, fallback to using a secondary key.
146+
*
147+
* @param c the Config object
148+
* @param key the configuration key
149+
* @param fallbackKey the fallback key
150+
* @param defautValue the default value
151+
* @return Configured value as String
152+
*/
153+
public static Boolean getDefaultBooleanConfigWithFallbackLookup(Config c, String key, String fallbackKey, boolean defautValue) {
154+
String result = c.getValue(key);
155+
if (result == null) {
156+
return c.getValueAsBoolean(fallbackKey, defautValue);
157+
}
158+
return c.getValueAsBoolean(key, defautValue);
159+
}
142160
}

oauth-common/src/main/java/io/strimzi/kafka/oauth/common/HttpUtil.java

+37-11
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,27 @@ public static <T> T get(URI uri, SSLSocketFactory socketFactory, HostnameVerifie
173173
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
174174
*/
175175
public static <T> T get(URI uri, SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier, String authorization, Class<T> responseType, int connectTimeout, int readTimeout) throws IOException {
176-
return request(uri, "GET", socketFactory, hostnameVerifier, authorization, null, null, responseType, connectTimeout, readTimeout);
176+
return request(uri, "GET", socketFactory, hostnameVerifier, authorization, null, null, responseType, connectTimeout, readTimeout, true);
177+
}
178+
179+
/**
180+
* Perform HTTP GET request and return the response in the specified type.
181+
*
182+
* @param uri The target url
183+
* @param socketFactory Socket factory to use with https:// url
184+
* @param hostnameVerifier HostnameVerifier to use with https:// url
185+
* @param authorization The Authorization header value
186+
* @param responseType The type to which to convert the response (String or one of the Jackson Mapper types)
187+
* @param connectTimeout Connect timeout in seconds
188+
* @param readTimeout Read timeout in seconds
189+
* @param includeAcceptHeader Determines if <code>Accept application/json</code> is sent to the remote server.
190+
* @return The response as specified by the <code>responseType</code>.
191+
* @param <T> Generic type of the <code>responseType</code>
192+
* @throws IOException A connection, timeout, or network exception that occurs while performing the request
193+
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
194+
*/
195+
public static <T> T get(URI uri, SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier, String authorization, Class<T> responseType, int connectTimeout, int readTimeout, boolean includeAcceptHeader) throws IOException {
196+
return request(uri, "GET", socketFactory, hostnameVerifier, authorization, null, null, responseType, connectTimeout, readTimeout, includeAcceptHeader);
177197
}
178198

179199
/**
@@ -242,13 +262,14 @@ public static <T> T post(URI uri, SSLSocketFactory socketFactory, HostnameVerifi
242262
* @param responseType The type to which to convert the response (String or one of the Jackson Mapper types)
243263
* @param connectTimeout Connect timeout in seconds
244264
* @param readTimeout Read timeout in seconds
265+
* @param includeAcceptHeader TODO
245266
* @return The response as specified by the <code>responseType</code>.
246267
* @param <T> Generic type of the <code>responseType</code>
247268
* @throws IOException A connection, timeout, or network exception that occurs while performing the request
248269
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
249270
*/
250-
public static <T> T post(URI uri, SSLSocketFactory socketFactory, HostnameVerifier verifier, String authorization, String contentType, String body, Class<T> responseType, int connectTimeout, int readTimeout) throws IOException {
251-
return request(uri, "POST", socketFactory, verifier, authorization, contentType, body, responseType, connectTimeout, readTimeout);
271+
public static <T> T post(URI uri, SSLSocketFactory socketFactory, HostnameVerifier verifier, String authorization, String contentType, String body, Class<T> responseType, int connectTimeout, int readTimeout, boolean includeAcceptHeader) throws IOException {
272+
return request(uri, "POST", socketFactory, verifier, authorization, contentType, body, responseType, connectTimeout, readTimeout, includeAcceptHeader);
252273
}
253274

254275
/**
@@ -311,7 +332,7 @@ public static void put(URI uri, SSLSocketFactory socketFactory, HostnameVerifier
311332
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
312333
*/
313334
public static void put(URI uri, SSLSocketFactory socketFactory, HostnameVerifier verifier, String authorization, String contentType, String body, int connectTimeout, int readTimeout) throws IOException {
314-
request(uri, "PUT", socketFactory, verifier, authorization, contentType, body, null, connectTimeout, readTimeout);
335+
request(uri, "PUT", socketFactory, verifier, authorization, contentType, body, null, connectTimeout, readTimeout, true);
315336
}
316337

317338
/**
@@ -323,7 +344,7 @@ public static void put(URI uri, SSLSocketFactory socketFactory, HostnameVerifier
323344
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
324345
*/
325346
public static void delete(URI uri, String authorization) throws IOException {
326-
request(uri, "DELETE", null, null, authorization, null, null, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT);
347+
request(uri, "DELETE", null, null, authorization, null, null, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, true);
327348
}
328349

329350
/**
@@ -337,7 +358,7 @@ public static void delete(URI uri, String authorization) throws IOException {
337358
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
338359
*/
339360
public static void delete(URI uri, SSLSocketFactory socketFactory, HostnameVerifier verifier, String authorization) throws IOException {
340-
request(uri, "DELETE", socketFactory, verifier, authorization, null, null, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT);
361+
request(uri, "DELETE", socketFactory, verifier, authorization, null, null, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, true);
341362
}
342363

343364
/**
@@ -353,7 +374,7 @@ public static void delete(URI uri, SSLSocketFactory socketFactory, HostnameVerif
353374
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
354375
*/
355376
public static void delete(URI uri, SSLSocketFactory socketFactory, HostnameVerifier verifier, String authorization, int connectTimeout, int readTimeout) throws IOException {
356-
request(uri, "DELETE", socketFactory, verifier, authorization, null, null, null, connectTimeout, readTimeout);
377+
request(uri, "DELETE", socketFactory, verifier, authorization, null, null, null, connectTimeout, readTimeout, true);
357378
}
358379

359380
/**
@@ -375,7 +396,7 @@ public static void delete(URI uri, SSLSocketFactory socketFactory, HostnameVerif
375396
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
376397
*/
377398
public static <T> T request(URI uri, SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier, String authorization, String contentType, String body, Class<T> responseType) throws IOException {
378-
return request(uri, null, socketFactory, hostnameVerifier, authorization, contentType, body, responseType, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT);
399+
return request(uri, null, socketFactory, hostnameVerifier, authorization, contentType, body, responseType, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, true);
379400
}
380401

381402
/**
@@ -395,7 +416,7 @@ public static <T> T request(URI uri, SSLSocketFactory socketFactory, HostnameVer
395416
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
396417
*/
397418
public static <T> T request(URI uri, String method, SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier, String authorization, String contentType, String body, Class<T> responseType) throws IOException {
398-
return request(uri, method, socketFactory, hostnameVerifier, authorization, contentType, body, responseType, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT);
419+
return request(uri, method, socketFactory, hostnameVerifier, authorization, contentType, body, responseType, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, true);
399420
}
400421

401422
/**
@@ -413,13 +434,14 @@ public static <T> T request(URI uri, String method, SSLSocketFactory socketFacto
413434
* @param readTimeout Read timeout in seconds
414435
* @return The response as specified by the <code>responseType</code>.
415436
* @param <T> Generic type of the <code>responseType</code>
437+
* @param includeAcceptHeader Determines if <code>Accept application/json</code> is sent to the remote server.
416438
* @throws IOException A connection, timeout, or network exception that occurs while performing the request
417439
* @throws HttpException A runtime exception when an HTTP response status signals a failed request
418440
*/
419441
// Suppressed because of Spotbugs Java 11 bug - https://github.com/spotbugs/spotbugs/issues/756
420442
@SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
421443
public static <T> T request(URI uri, String method, SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier, String authorization,
422-
String contentType, String body, Class<T> responseType, int connectTimeout, int readTimeout) throws IOException {
444+
String contentType, String body, Class<T> responseType, int connectTimeout, int readTimeout, boolean includeAcceptHeader) throws IOException {
423445
HttpURLConnection con;
424446
try {
425447
con = (HttpURLConnection) uri.toURL().openConnection();
@@ -443,7 +465,11 @@ public static <T> T request(URI uri, String method, SSLSocketFactory socketFacto
443465
if (authorization != null) {
444466
con.setRequestProperty("Authorization", authorization);
445467
}
446-
con.setRequestProperty("Accept", "application/json");
468+
469+
if (includeAcceptHeader) {
470+
con.setRequestProperty("Accept", "application/json");
471+
}
472+
447473
if (body != null && body.length() > 0) {
448474
if (contentType == null) {
449475
throw new IllegalArgumentException("contentType must be set when body is not null");

0 commit comments

Comments
 (0)