This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 4e2ddabd0392 CAMEL-24456: camel-http - key the OAuth2 token cache on
every field that shapes the token (#25834)
4e2ddabd0392 is described below
commit 4e2ddabd0392441b48ce591105851933b04b34b0
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 11:12:29 2026 +0200
CAMEL-24456: camel-http - key the OAuth2 token cache on every field that
shapes the token (#25834)
* CAMEL-24456: camel-http - key the OAuth2 token cache on every field that
shapes the token
The cache key was the record OAuth2URIAndCredentials(uri, clientId,
clientSecret), while
scope, tokenEndpoint and resourceIndicator all influence the token that
getAccessTokenResponse() mints. The map is static, so it is shared by every
OAuth2ClientConfigurer instance and every CamelContext in the JVM.
A route configured with a narrow scope could therefore be handed a
broad-scope token that
another route had cached first for the same target and credentials, which
defeats the
scoping the operator configured and makes the audit trail misleading. Where
several
CamelContexts run in one JVM, a token minted for one could serve another's
requests.
Add tokenEndpoint, scope and resourceIndicator to the key. The map stays
JVM wide, but a
hit now requires every field of the token request to match, so it is the
same token
request by construction; scoping the cache per CamelContext is noted on the
issue as a
separate question.
The added test follows the idiom of the tests around it: cache a token,
close the token
endpoint, then request the same target with a different scope. A cache hit
succeeds, a
miss cannot mint and fails - so without the fix the narrow-scope token is
silently reused.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24456: Regenerate YAML DSL schema
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Federico Mariani <[email protected]>
Co-authored-by: Croway <[email protected]>
---
.../component/http/OAuth2ClientConfigurer.java | 14 ++++++-
.../component/http/HttpOAuth2TokenCachingTest.java | 46 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 15 +++++++
3 files changed, 73 insertions(+), 2 deletions(-)
diff --git
a/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2ClientConfigurer.java
b/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2ClientConfigurer.java
index 08ff86b9566f..8edb187e02f2 100644
---
a/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2ClientConfigurer.java
+++
b/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2ClientConfigurer.java
@@ -78,7 +78,8 @@ public class OAuth2ClientConfigurer extends ServiceSupport
implements HttpClient
clientBuilder.addRequestInterceptorFirst((HttpRequest request,
EntityDetails entity, HttpContext context) -> {
URI requestUri = getUriFromRequest(request);
- OAuth2URIAndCredentials uriAndCredentials = new
OAuth2URIAndCredentials(requestUri, clientId, clientSecret);
+ OAuth2URIAndCredentials uriAndCredentials = new
OAuth2URIAndCredentials(
+ requestUri, clientId, clientSecret, tokenEndpoint, scope,
resourceIndicator);
if (cacheTokens) {
if (tokenCache.containsKey(uriAndCredentials)
&&
!tokenCache.get(uriAndCredentials).isExpiredWithMargin(cachedTokensExpirationMarginSeconds))
{
@@ -177,7 +178,16 @@ public class OAuth2ClientConfigurer extends ServiceSupport
implements HttpClient
}
}
- private record OAuth2URIAndCredentials(URI uri, String clientId, String
clientSecret) {
+ /**
+ * Cache key for a minted token.
+ * <p>
+ * Every field that shapes the token request has to be part of it. The map
is static, so it is shared by every
+ * configurer instance and every CamelContext in the JVM; a key that left
out the scope, the token endpoint or the
+ * resource indicator would let a route configured for a narrow scope be
served a broad-scope token that another
+ * route cached first, which defeats the scoping the operator asked for
and makes the audit trail misleading.
+ */
+ private record OAuth2URIAndCredentials(URI uri, String clientId, String
clientSecret, String tokenEndpoint,
+ String scope, String resourceIndicator) {
}
@Override
diff --git
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2TokenCachingTest.java
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2TokenCachingTest.java
index fb8cd4c95c2a..540f6c37d1ab 100644
---
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2TokenCachingTest.java
+++
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2TokenCachingTest.java
@@ -63,6 +63,52 @@ public class HttpOAuth2TokenCachingTest extends BaseHttpTest
{
}
}
+ /**
+ * The cache is a static map shared by every configurer instance and every
CamelContext in the JVM, so its key has
+ * to name every field that shapes the token request. When the scope was
left out, a route asking for a narrow scope
+ * was served whatever token another route had cached for the same target
and credentials.
+ * <p>
+ * Uses the same trick as the tests around it: close the token endpoint,
then make the second request. A cache hit
+ * succeeds; a miss has to mint a token and cannot.
+ */
+ @Test
+ public void aDifferentScopeDoesNotReuseACachedToken() throws Exception {
+ try (var localServer = createLocalServer(); var localOAuth2Server =
createLocalOAuth2Server()) {
+ String tokenEndpoint = "http://localhost:" +
localOAuth2Server.getLocalPort() + "/token";
+ String base = "http://localhost:" + localServer.getLocalPort() +
"/post?httpMethod=POST&oauth2ClientId="
+ + clientId + "&oauth2ClientSecret=" + clientSecret +
"&oauth2TokenEndpoint=" + tokenEndpoint
+ + "&oauth2CacheTokens=true&oauth2Scope=";
+
+ // caches a token for the narrow scope
+ template.request(base + "read", exchange -> {
+ });
+ localOAuth2Server.close();
+
+ // same target and credentials, different scope: the narrow-scope
token must not be handed out
+ Exchange exchange = template.request(base + "read+write",
exchange1 -> {
+ });
+ assertExceptionExchange(exchange);
+ }
+ }
+
+ @Test
+ public void theSameScopeStillReusesTheCachedToken() throws Exception {
+ try (var localServer = createLocalServer(); var localOAuth2Server =
createLocalOAuth2Server()) {
+ String tokenEndpoint = "http://localhost:" +
localOAuth2Server.getLocalPort() + "/token";
+ String requestUrl = "http://localhost:" +
localServer.getLocalPort() + "/post?httpMethod=POST&oauth2ClientId="
+ + clientId + "&oauth2ClientSecret=" +
clientSecret + "&oauth2TokenEndpoint=" + tokenEndpoint
+ + "&oauth2CacheTokens=true&oauth2Scope=read";
+
+ template.request(requestUrl, exchange -> {
+ });
+ localOAuth2Server.close();
+
+ Exchange exchange = template.request(requestUrl, exchange1 -> {
+ });
+ assertExchange(exchange);
+ }
+ }
+
@Test
public void tokenIsNotCachedWhenCacheTokensIsFalse() throws Exception {
try (var localServer = createLocalServer(); var localOAuth2Server =
createLocalOAuth2Server()) {
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index f71d24f48fda..ed3c9483a505 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -515,6 +515,20 @@ camel.routeController.enabled = true
camel.routeController.backOffDelay = 2000
camel.routeController.backOffMaxDelay = 60000
----
+
+=== camel-http
+
+The OAuth2 client-credentials token cache was keyed on the request URI, the
client id and the client
+secret only. `oauth2Scope`, `oauth2TokenEndpoint` and
`oauth2ResourceIndicator` all shape the token that
+gets minted, but none of them was part of the key, and the cache is a static
map shared by every endpoint
+and every `CamelContext` in the JVM. A route configured with a narrow scope
could therefore be handed a
+broad-scope token that another route had cached first for the same target and
credentials — defeating the
+scoping the operator configured, and making the audit trail misleading.
+
+All three are now part of the key. Deployments that were unknowingly sharing a
token across differing
+scopes, token endpoints or resource indicators will now request one token per
distinct combination, so
+the token endpoint sees more requests than before.
+
=== camel-pqc
`FileBasedKeyLifecycleManager` stores private keys unencrypted, as Base64
PKCS#8 inside a JSON file, and
@@ -529,6 +543,7 @@ is truncated rather than recreated and would otherwise keep
its original permiss
Deployments where another account legitimately reads these files — a sidecar
or a backup agent running as
a different user — need to run as the owner, or use a group-aware key store
instead.
+
=== camel-grpc
The gRPC consumer no longer returns the route exception's message to the
client.