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.

Reply via email to