This is an automated email from the ASF dual-hosted git repository. Croway pushed a commit to branch camel-4.22.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit ea02d417a1b814a505c60882b57a8d5483b0a499 Author: Andrea Cosentino <[email protected]> AuthorDate: Mon Aug 31 10:08:25 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]> (cherry picked from commit 4e2ddabd0392441b48ce591105851933b04b34b0) Co-authored-by: Claude Opus 5 (1M context) <[email protected]> --- .../component/http/OAuth2ClientConfigurer.java | 14 ++++++- .../component/http/HttpOAuth2TokenCachingTest.java | 46 ++++++++++++++++++++++ 2 files changed, 58 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()) {
