This is an automated email from the ASF dual-hosted git repository.

davsclaus 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 81374bd7e426 CAMEL-22080: camel-http - make the OAuth2 token cache key 
pluggable (#27589)
81374bd7e426 is described below

commit 81374bd7e426ce59358d463c55e9b4b05b737cca
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Oct 10 01:29:04 2026 +0530

    CAMEL-22080: camel-http - make the OAuth2 token cache key pluggable (#27589)
    
    With oauth2CacheTokens=true the token cache was keyed on the full request 
URI,
    so every new path or query requested a new token and added an entry to the
    JVM-wide cache. The key is now computed by a pluggable
    OAuth2CachedTokensKeyResolver; the OAuth2CachedTokensKey enum offers 
FULL_URI
    (old behaviour), HOST_AND_PATH and HOST_ONLY, the new default. Client id,
    secret, token endpoint, scope and resource indicator stay part of the key.
    Upgrade guide note added.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/components/http.json  |  18 +-
 .../org/apache/camel/catalog/components/https.json |  18 +-
 .../apache/camel/catalog/docs/http-component.adoc  |  57 ++++++
 .../component/http/HttpEndpointConfigurer.java     |  12 ++
 .../component/http/HttpEndpointUriFactory.java     |   4 +-
 .../org/apache/camel/component/http/http.json      |  18 +-
 .../org/apache/camel/component/http/https.json     |  18 +-
 .../camel-http/src/main/docs/http-component.adoc   |  57 ++++++
 .../apache/camel/component/http/HttpComponent.java |  22 ++-
 .../apache/camel/component/http/HttpEndpoint.java  |  34 ++++
 .../component/http/OAuth2CachedTokensKey.java      |  64 +++++++
 .../http/OAuth2CachedTokensKeyResolver.java        |  46 +++++
 .../component/http/OAuth2ClientConfigurer.java     |  34 +++-
 .../http/HttpOAuth2CachedTokensKeyTest.java        | 202 +++++++++++++++++++++
 .../component/http/HttpOAuth2TokenCachingTest.java | 131 +++++++++++++
 .../component/http/OAuth2CachedTokensKeyTest.java  |  58 ++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  10 +
 .../endpoint/dsl/HttpEndpointBuilderFactory.java   |  76 ++++++++
 18 files changed, 835 insertions(+), 44 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/http.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/http.json
index 62bd200a838a..f19251f13fd1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/http.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/http.json
@@ -156,13 +156,15 @@
     "oauth2BodyAuthentication": { "index": 57, "kind": "parameter", 
"displayName": "Oauth2 Body Authentication", "group": "security", "label": 
"producer,security", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether to use OAuth2 body 
authentication." },
     "oauth2CachedTokensDefaultExpirySeconds": { "index": 58, "kind": 
"parameter", "displayName": "Oauth2 Cached Tokens Default Expiry Seconds", 
"group": "security", "label": "producer,security", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 3600, "description": "Default expiration time 
for cached OAuth2 tokens, in seconds. Used if token response does not contain 
'expires_in' field." },
     "oauth2CachedTokensExpirationMarginSeconds": { "index": 59, "kind": 
"parameter", "displayName": "Oauth2 Cached Tokens Expiration Margin Seconds", 
"group": "security", "label": "producer,security", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 5, "description": "Amount of time which is 
deducted from OAuth2 tokens expiry time to compensate for the time it takes 
OAuth2 Token Endpoint to send the token  [...]
-    "oauth2CacheTokens": { "index": 60, "kind": "parameter", "displayName": 
"Oauth2 Cache Tokens", "group": "security", "label": "producer,security", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Whether to cache OAuth2 client tokens." },
-    "oauth2ClientId": { "index": 61, "kind": "parameter", "displayName": 
"Oauth2 Client Id", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client id" },
-    "oauth2ClientSecret": { "index": 62, "kind": "parameter", "displayName": 
"Oauth2 Client Secret", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client secret" },
-    "oauth2ResourceIndicator": { "index": 63, "kind": "parameter", 
"displayName": "Oauth2 Resource Indicator", "group": "security", "label": 
"producer,security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "OAuth2 Token endpoint" },
-    "oauth2Scope": { "index": 64, "kind": "parameter", "displayName": "Oauth2 
Scope", "group": "security", "label": "producer,security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "description": "OAuth2 scope" },
-    "oauth2TokenEndpoint": { "index": 65, "kind": "parameter", "displayName": 
"Oauth2 Token Endpoint", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"OAuth2 Resource Indicator" },
-    "sslContextParameters": { "index": 66, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"autowired": false, "secret": false, "description": "To configure security 
using SSLContextParameters. Important: Only one instance of 
org.apache.camel.util.jsse.SSLContextParameters is supported per HttpComponent. 
If you [...]
-    "x509HostnameVerifier": { "index": 67, "kind": "parameter", "displayName": 
"X509 Hostname Verifier", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier", 
"deprecated": false, "autowired": false, "secret": false, "security": 
"insecure:ssl", "description": "To use a custom X509HostnameVerifier such as 
DefaultHostnameVerifier or NoopHostnameVerifier" }
+    "oauth2CachedTokensKey": { "index": 60, "kind": "parameter", 
"displayName": "Oauth2 Cached Tokens Key", "group": "security", "label": 
"producer,security", "required": false, "type": "enum", "javaType": 
"org.apache.camel.component.http.OAuth2CachedTokensKey", "enum": [ "HOST_ONLY", 
"HOST_AND_PATH", "FULL_URI" ], "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "HOST_ONLY", "description": "How cached OAuth2 
tokens are shared between requests when oauth2CacheTok [...]
+    "oauth2CacheTokens": { "index": 61, "kind": "parameter", "displayName": 
"Oauth2 Cache Tokens", "group": "security", "label": "producer,security", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Whether to cache OAuth2 client tokens." },
+    "oauth2ClientId": { "index": 62, "kind": "parameter", "displayName": 
"Oauth2 Client Id", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client id" },
+    "oauth2ClientSecret": { "index": 63, "kind": "parameter", "displayName": 
"Oauth2 Client Secret", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client secret" },
+    "oauth2ResourceIndicator": { "index": 64, "kind": "parameter", 
"displayName": "Oauth2 Resource Indicator", "group": "security", "label": 
"producer,security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "OAuth2 Token endpoint" },
+    "oauth2Scope": { "index": 65, "kind": "parameter", "displayName": "Oauth2 
Scope", "group": "security", "label": "producer,security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "description": "OAuth2 scope" },
+    "oauth2TokenEndpoint": { "index": 66, "kind": "parameter", "displayName": 
"Oauth2 Token Endpoint", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"OAuth2 Resource Indicator" },
+    "sslContextParameters": { "index": 67, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"autowired": false, "secret": false, "description": "To configure security 
using SSLContextParameters. Important: Only one instance of 
org.apache.camel.util.jsse.SSLContextParameters is supported per HttpComponent. 
If you [...]
+    "x509HostnameVerifier": { "index": 68, "kind": "parameter", "displayName": 
"X509 Hostname Verifier", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier", 
"deprecated": false, "autowired": false, "secret": false, "security": 
"insecure:ssl", "description": "To use a custom X509HostnameVerifier such as 
DefaultHostnameVerifier or NoopHostnameVerifier" },
+    "oauth2CachedTokensKeyResolver": { "index": 69, "kind": "parameter", 
"displayName": "Oauth2 Cached Tokens Key Resolver", "group": "security 
(advanced)", "label": "producer,security,advanced", "required": false, "type": 
"object", "javaType": 
"org.apache.camel.component.http.OAuth2CachedTokensKeyResolver", "deprecated": 
false, "autowired": false, "secret": false, "description": "To use a custom 
strategy to compute the key of cached OAuth2 tokens when oauth2CacheTokens is 
enabled. When  [...]
   }
 }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/https.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/https.json
index 684054be5524..a7e563593087 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/https.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/https.json
@@ -156,13 +156,15 @@
     "oauth2BodyAuthentication": { "index": 57, "kind": "parameter", 
"displayName": "Oauth2 Body Authentication", "group": "security", "label": 
"producer,security", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether to use OAuth2 body 
authentication." },
     "oauth2CachedTokensDefaultExpirySeconds": { "index": 58, "kind": 
"parameter", "displayName": "Oauth2 Cached Tokens Default Expiry Seconds", 
"group": "security", "label": "producer,security", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 3600, "description": "Default expiration time 
for cached OAuth2 tokens, in seconds. Used if token response does not contain 
'expires_in' field." },
     "oauth2CachedTokensExpirationMarginSeconds": { "index": 59, "kind": 
"parameter", "displayName": "Oauth2 Cached Tokens Expiration Margin Seconds", 
"group": "security", "label": "producer,security", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 5, "description": "Amount of time which is 
deducted from OAuth2 tokens expiry time to compensate for the time it takes 
OAuth2 Token Endpoint to send the token  [...]
-    "oauth2CacheTokens": { "index": 60, "kind": "parameter", "displayName": 
"Oauth2 Cache Tokens", "group": "security", "label": "producer,security", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Whether to cache OAuth2 client tokens." },
-    "oauth2ClientId": { "index": 61, "kind": "parameter", "displayName": 
"Oauth2 Client Id", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client id" },
-    "oauth2ClientSecret": { "index": 62, "kind": "parameter", "displayName": 
"Oauth2 Client Secret", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client secret" },
-    "oauth2ResourceIndicator": { "index": 63, "kind": "parameter", 
"displayName": "Oauth2 Resource Indicator", "group": "security", "label": 
"producer,security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "OAuth2 Token endpoint" },
-    "oauth2Scope": { "index": 64, "kind": "parameter", "displayName": "Oauth2 
Scope", "group": "security", "label": "producer,security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "description": "OAuth2 scope" },
-    "oauth2TokenEndpoint": { "index": 65, "kind": "parameter", "displayName": 
"Oauth2 Token Endpoint", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"OAuth2 Resource Indicator" },
-    "sslContextParameters": { "index": 66, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"autowired": false, "secret": false, "description": "To configure security 
using SSLContextParameters. Important: Only one instance of 
org.apache.camel.util.jsse.SSLContextParameters is supported per HttpComponent. 
If you [...]
-    "x509HostnameVerifier": { "index": 67, "kind": "parameter", "displayName": 
"X509 Hostname Verifier", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier", 
"deprecated": false, "autowired": false, "secret": false, "security": 
"insecure:ssl", "description": "To use a custom X509HostnameVerifier such as 
DefaultHostnameVerifier or NoopHostnameVerifier" }
+    "oauth2CachedTokensKey": { "index": 60, "kind": "parameter", 
"displayName": "Oauth2 Cached Tokens Key", "group": "security", "label": 
"producer,security", "required": false, "type": "enum", "javaType": 
"org.apache.camel.component.http.OAuth2CachedTokensKey", "enum": [ "HOST_ONLY", 
"HOST_AND_PATH", "FULL_URI" ], "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "HOST_ONLY", "description": "How cached OAuth2 
tokens are shared between requests when oauth2CacheTok [...]
+    "oauth2CacheTokens": { "index": 61, "kind": "parameter", "displayName": 
"Oauth2 Cache Tokens", "group": "security", "label": "producer,security", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Whether to cache OAuth2 client tokens." },
+    "oauth2ClientId": { "index": 62, "kind": "parameter", "displayName": 
"Oauth2 Client Id", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client id" },
+    "oauth2ClientSecret": { "index": 63, "kind": "parameter", "displayName": 
"Oauth2 Client Secret", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client secret" },
+    "oauth2ResourceIndicator": { "index": 64, "kind": "parameter", 
"displayName": "Oauth2 Resource Indicator", "group": "security", "label": 
"producer,security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "OAuth2 Token endpoint" },
+    "oauth2Scope": { "index": 65, "kind": "parameter", "displayName": "Oauth2 
Scope", "group": "security", "label": "producer,security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "description": "OAuth2 scope" },
+    "oauth2TokenEndpoint": { "index": 66, "kind": "parameter", "displayName": 
"Oauth2 Token Endpoint", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"OAuth2 Resource Indicator" },
+    "sslContextParameters": { "index": 67, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"autowired": false, "secret": false, "description": "To configure security 
using SSLContextParameters. Important: Only one instance of 
org.apache.camel.util.jsse.SSLContextParameters is supported per HttpComponent. 
If you [...]
+    "x509HostnameVerifier": { "index": 68, "kind": "parameter", "displayName": 
"X509 Hostname Verifier", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier", 
"deprecated": false, "autowired": false, "secret": false, "security": 
"insecure:ssl", "description": "To use a custom X509HostnameVerifier such as 
DefaultHostnameVerifier or NoopHostnameVerifier" },
+    "oauth2CachedTokensKeyResolver": { "index": 69, "kind": "parameter", 
"displayName": "Oauth2 Cached Tokens Key Resolver", "group": "security 
(advanced)", "label": "producer,security,advanced", "required": false, "type": 
"object", "javaType": 
"org.apache.camel.component.http.OAuth2CachedTokensKeyResolver", "deprecated": 
false, "autowired": false, "secret": false, "description": "To use a custom 
strategy to compute the key of cached OAuth2 tokens when oauth2CacheTokens is 
enabled. When  [...]
   }
 }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/http-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/http-component.adoc
index ddca2c8f7c32..094bfccca34a 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/http-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/http-component.adoc
@@ -678,6 +678,63 @@ Camel only provides support for OAuth2 client credentials 
flow
 [IMPORTANT]
 Camel does not perform any validation in access token. It's up to the 
underlying service to validate it.
 
+==== Caching tokens
+
+With `oauth2CacheTokens=true`, Camel caches the token until it expires (see 
`oauth2CachedTokensDefaultExpirySeconds`
+and `oauth2CachedTokensExpirationMarginSeconds`). The option 
`oauth2CachedTokensKey` decides which requests share a
+cached token:
+
+* `HOST_ONLY` (default): one token shared by all requests to the same scheme, 
host and port.
+* `HOST_AND_PATH`: one token per path. The query is not taken into account.
+* `FULL_URI`: one token per request URI, including the query (the behaviour 
before Camel 4.23).
+
+The client id, client secret, token endpoint, scope and resource indicator are 
always part of the key. The token
+request itself does not depend on the request URI, so the default `HOST_ONLY` 
does not request a new token for every
+new URI, for example with dynamic paths or queries in `toD`.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:start")
+  
.toD("https://localhost:9090/event/${header.eventId}?oauth2ClientId=my-client-id&oauth2ClientSecret=my-client-secret&oauth2TokenEndpoint=https://localhost:8080/realms/master/protocol/openid-connect/token&oauth2CacheTokens=true&oauth2CachedTokensKey=HOST_ONLY";);
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:start"/>
+  <toD 
uri="https://localhost:9090/event/${header.eventId}?oauth2ClientId=my-client-id&amp;oauth2ClientSecret=my-client-secret&amp;oauth2TokenEndpoint=https://localhost:8080/realms/master/protocol/openid-connect/token&amp;oauth2CacheTokens=true&amp;oauth2CachedTokensKey=HOST_ONLY"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:start
+      steps:
+        - toD:
+            uri: "https://localhost:9090/event/${header.eventId}";
+            parameters:
+              oauth2ClientId: my-client-id
+              oauth2ClientSecret: my-client-secret
+              oauth2TokenEndpoint: 
"https://localhost:8080/realms/master/protocol/openid-connect/token";
+              oauth2CacheTokens: true
+              oauth2CachedTokensKey: HOST_ONLY
+----
+====
+
+For other rules, implement 
`org.apache.camel.component.http.OAuth2CachedTokensKeyResolver` and refer to it 
with
+`oauth2CachedTokensKeyResolver=#myResolver`. It takes precedence over 
`oauth2CachedTokensKey`. When it returns `null`,
+the token for that request is not cached.
+
 === Vault-backed credential rotation
 
 When using vault-backed property placeholders (e.g. 
`{{aws:mySecret/password}}`) with
diff --git 
a/components/camel-http/src/generated/java/org/apache/camel/component/http/HttpEndpointConfigurer.java
 
b/components/camel-http/src/generated/java/org/apache/camel/component/http/HttpEndpointConfigurer.java
index a3e047010724..cfb7c711bca6 100644
--- 
a/components/camel-http/src/generated/java/org/apache/camel/component/http/HttpEndpointConfigurer.java
+++ 
b/components/camel-http/src/generated/java/org/apache/camel/component/http/HttpEndpointConfigurer.java
@@ -107,6 +107,10 @@ public class HttpEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "oauth2CachedTokensDefaultExpirySeconds": 
target.setOauth2CachedTokensDefaultExpirySeconds(property(camelContext, 
long.class, value)); return true;
         case "oauth2cachedtokensexpirationmarginseconds":
         case "oauth2CachedTokensExpirationMarginSeconds": 
target.setOauth2CachedTokensExpirationMarginSeconds(property(camelContext, 
long.class, value)); return true;
+        case "oauth2cachedtokenskey":
+        case "oauth2CachedTokensKey": 
target.setOauth2CachedTokensKey(property(camelContext, 
org.apache.camel.component.http.OAuth2CachedTokensKey.class, value)); return 
true;
+        case "oauth2cachedtokenskeyresolver":
+        case "oauth2CachedTokensKeyResolver": 
target.setOauth2CachedTokensKeyResolver(property(camelContext, 
org.apache.camel.component.http.OAuth2CachedTokensKeyResolver.class, value)); 
return true;
         case "oauth2clientid":
         case "oauth2ClientId": target.setOauth2ClientId(property(camelContext, 
java.lang.String.class, value)); return true;
         case "oauth2clientsecret":
@@ -248,6 +252,10 @@ public class HttpEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "oauth2CachedTokensDefaultExpirySeconds": return long.class;
         case "oauth2cachedtokensexpirationmarginseconds":
         case "oauth2CachedTokensExpirationMarginSeconds": return long.class;
+        case "oauth2cachedtokenskey":
+        case "oauth2CachedTokensKey": return 
org.apache.camel.component.http.OAuth2CachedTokensKey.class;
+        case "oauth2cachedtokenskeyresolver":
+        case "oauth2CachedTokensKeyResolver": return 
org.apache.camel.component.http.OAuth2CachedTokensKeyResolver.class;
         case "oauth2clientid":
         case "oauth2ClientId": return java.lang.String.class;
         case "oauth2clientsecret":
@@ -390,6 +398,10 @@ public class HttpEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "oauth2CachedTokensDefaultExpirySeconds": return 
target.getOauth2CachedTokensDefaultExpirySeconds();
         case "oauth2cachedtokensexpirationmarginseconds":
         case "oauth2CachedTokensExpirationMarginSeconds": return 
target.getOauth2CachedTokensExpirationMarginSeconds();
+        case "oauth2cachedtokenskey":
+        case "oauth2CachedTokensKey": return target.getOauth2CachedTokensKey();
+        case "oauth2cachedtokenskeyresolver":
+        case "oauth2CachedTokensKeyResolver": return 
target.getOauth2CachedTokensKeyResolver();
         case "oauth2clientid":
         case "oauth2ClientId": return target.getOauth2ClientId();
         case "oauth2clientsecret":
diff --git 
a/components/camel-http/src/generated/java/org/apache/camel/component/http/HttpEndpointUriFactory.java
 
b/components/camel-http/src/generated/java/org/apache/camel/component/http/HttpEndpointUriFactory.java
index 98c585c53de1..a2597b3ccdec 100644
--- 
a/components/camel-http/src/generated/java/org/apache/camel/component/http/HttpEndpointUriFactory.java
+++ 
b/components/camel-http/src/generated/java/org/apache/camel/component/http/HttpEndpointUriFactory.java
@@ -25,7 +25,7 @@ public class HttpEndpointUriFactory extends 
org.apache.camel.support.component.E
     private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
     private static final Map<String, String> MULTI_VALUE_PREFIXES;
     static {
-        Set<String> props = new HashSet<>(68);
+        Set<String> props = new HashSet<>(70);
         props.add("authBearerToken");
         props.add("authDomain");
         props.add("authHost");
@@ -69,6 +69,8 @@ public class HttpEndpointUriFactory extends 
org.apache.camel.support.component.E
         props.add("oauth2CacheTokens");
         props.add("oauth2CachedTokensDefaultExpirySeconds");
         props.add("oauth2CachedTokensExpirationMarginSeconds");
+        props.add("oauth2CachedTokensKey");
+        props.add("oauth2CachedTokensKeyResolver");
         props.add("oauth2ClientId");
         props.add("oauth2ClientSecret");
         props.add("oauth2ResourceIndicator");
diff --git 
a/components/camel-http/src/generated/resources/META-INF/org/apache/camel/component/http/http.json
 
b/components/camel-http/src/generated/resources/META-INF/org/apache/camel/component/http/http.json
index 62bd200a838a..f19251f13fd1 100644
--- 
a/components/camel-http/src/generated/resources/META-INF/org/apache/camel/component/http/http.json
+++ 
b/components/camel-http/src/generated/resources/META-INF/org/apache/camel/component/http/http.json
@@ -156,13 +156,15 @@
     "oauth2BodyAuthentication": { "index": 57, "kind": "parameter", 
"displayName": "Oauth2 Body Authentication", "group": "security", "label": 
"producer,security", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether to use OAuth2 body 
authentication." },
     "oauth2CachedTokensDefaultExpirySeconds": { "index": 58, "kind": 
"parameter", "displayName": "Oauth2 Cached Tokens Default Expiry Seconds", 
"group": "security", "label": "producer,security", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 3600, "description": "Default expiration time 
for cached OAuth2 tokens, in seconds. Used if token response does not contain 
'expires_in' field." },
     "oauth2CachedTokensExpirationMarginSeconds": { "index": 59, "kind": 
"parameter", "displayName": "Oauth2 Cached Tokens Expiration Margin Seconds", 
"group": "security", "label": "producer,security", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 5, "description": "Amount of time which is 
deducted from OAuth2 tokens expiry time to compensate for the time it takes 
OAuth2 Token Endpoint to send the token  [...]
-    "oauth2CacheTokens": { "index": 60, "kind": "parameter", "displayName": 
"Oauth2 Cache Tokens", "group": "security", "label": "producer,security", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Whether to cache OAuth2 client tokens." },
-    "oauth2ClientId": { "index": 61, "kind": "parameter", "displayName": 
"Oauth2 Client Id", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client id" },
-    "oauth2ClientSecret": { "index": 62, "kind": "parameter", "displayName": 
"Oauth2 Client Secret", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client secret" },
-    "oauth2ResourceIndicator": { "index": 63, "kind": "parameter", 
"displayName": "Oauth2 Resource Indicator", "group": "security", "label": 
"producer,security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "OAuth2 Token endpoint" },
-    "oauth2Scope": { "index": 64, "kind": "parameter", "displayName": "Oauth2 
Scope", "group": "security", "label": "producer,security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "description": "OAuth2 scope" },
-    "oauth2TokenEndpoint": { "index": 65, "kind": "parameter", "displayName": 
"Oauth2 Token Endpoint", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"OAuth2 Resource Indicator" },
-    "sslContextParameters": { "index": 66, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"autowired": false, "secret": false, "description": "To configure security 
using SSLContextParameters. Important: Only one instance of 
org.apache.camel.util.jsse.SSLContextParameters is supported per HttpComponent. 
If you [...]
-    "x509HostnameVerifier": { "index": 67, "kind": "parameter", "displayName": 
"X509 Hostname Verifier", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier", 
"deprecated": false, "autowired": false, "secret": false, "security": 
"insecure:ssl", "description": "To use a custom X509HostnameVerifier such as 
DefaultHostnameVerifier or NoopHostnameVerifier" }
+    "oauth2CachedTokensKey": { "index": 60, "kind": "parameter", 
"displayName": "Oauth2 Cached Tokens Key", "group": "security", "label": 
"producer,security", "required": false, "type": "enum", "javaType": 
"org.apache.camel.component.http.OAuth2CachedTokensKey", "enum": [ "HOST_ONLY", 
"HOST_AND_PATH", "FULL_URI" ], "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "HOST_ONLY", "description": "How cached OAuth2 
tokens are shared between requests when oauth2CacheTok [...]
+    "oauth2CacheTokens": { "index": 61, "kind": "parameter", "displayName": 
"Oauth2 Cache Tokens", "group": "security", "label": "producer,security", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Whether to cache OAuth2 client tokens." },
+    "oauth2ClientId": { "index": 62, "kind": "parameter", "displayName": 
"Oauth2 Client Id", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client id" },
+    "oauth2ClientSecret": { "index": 63, "kind": "parameter", "displayName": 
"Oauth2 Client Secret", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client secret" },
+    "oauth2ResourceIndicator": { "index": 64, "kind": "parameter", 
"displayName": "Oauth2 Resource Indicator", "group": "security", "label": 
"producer,security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "OAuth2 Token endpoint" },
+    "oauth2Scope": { "index": 65, "kind": "parameter", "displayName": "Oauth2 
Scope", "group": "security", "label": "producer,security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "description": "OAuth2 scope" },
+    "oauth2TokenEndpoint": { "index": 66, "kind": "parameter", "displayName": 
"Oauth2 Token Endpoint", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"OAuth2 Resource Indicator" },
+    "sslContextParameters": { "index": 67, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"autowired": false, "secret": false, "description": "To configure security 
using SSLContextParameters. Important: Only one instance of 
org.apache.camel.util.jsse.SSLContextParameters is supported per HttpComponent. 
If you [...]
+    "x509HostnameVerifier": { "index": 68, "kind": "parameter", "displayName": 
"X509 Hostname Verifier", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier", 
"deprecated": false, "autowired": false, "secret": false, "security": 
"insecure:ssl", "description": "To use a custom X509HostnameVerifier such as 
DefaultHostnameVerifier or NoopHostnameVerifier" },
+    "oauth2CachedTokensKeyResolver": { "index": 69, "kind": "parameter", 
"displayName": "Oauth2 Cached Tokens Key Resolver", "group": "security 
(advanced)", "label": "producer,security,advanced", "required": false, "type": 
"object", "javaType": 
"org.apache.camel.component.http.OAuth2CachedTokensKeyResolver", "deprecated": 
false, "autowired": false, "secret": false, "description": "To use a custom 
strategy to compute the key of cached OAuth2 tokens when oauth2CacheTokens is 
enabled. When  [...]
   }
 }
diff --git 
a/components/camel-http/src/generated/resources/META-INF/org/apache/camel/component/http/https.json
 
b/components/camel-http/src/generated/resources/META-INF/org/apache/camel/component/http/https.json
index 684054be5524..a7e563593087 100644
--- 
a/components/camel-http/src/generated/resources/META-INF/org/apache/camel/component/http/https.json
+++ 
b/components/camel-http/src/generated/resources/META-INF/org/apache/camel/component/http/https.json
@@ -156,13 +156,15 @@
     "oauth2BodyAuthentication": { "index": 57, "kind": "parameter", 
"displayName": "Oauth2 Body Authentication", "group": "security", "label": 
"producer,security", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether to use OAuth2 body 
authentication." },
     "oauth2CachedTokensDefaultExpirySeconds": { "index": 58, "kind": 
"parameter", "displayName": "Oauth2 Cached Tokens Default Expiry Seconds", 
"group": "security", "label": "producer,security", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 3600, "description": "Default expiration time 
for cached OAuth2 tokens, in seconds. Used if token response does not contain 
'expires_in' field." },
     "oauth2CachedTokensExpirationMarginSeconds": { "index": 59, "kind": 
"parameter", "displayName": "Oauth2 Cached Tokens Expiration Margin Seconds", 
"group": "security", "label": "producer,security", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 5, "description": "Amount of time which is 
deducted from OAuth2 tokens expiry time to compensate for the time it takes 
OAuth2 Token Endpoint to send the token  [...]
-    "oauth2CacheTokens": { "index": 60, "kind": "parameter", "displayName": 
"Oauth2 Cache Tokens", "group": "security", "label": "producer,security", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Whether to cache OAuth2 client tokens." },
-    "oauth2ClientId": { "index": 61, "kind": "parameter", "displayName": 
"Oauth2 Client Id", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client id" },
-    "oauth2ClientSecret": { "index": 62, "kind": "parameter", "displayName": 
"Oauth2 Client Secret", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client secret" },
-    "oauth2ResourceIndicator": { "index": 63, "kind": "parameter", 
"displayName": "Oauth2 Resource Indicator", "group": "security", "label": 
"producer,security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "OAuth2 Token endpoint" },
-    "oauth2Scope": { "index": 64, "kind": "parameter", "displayName": "Oauth2 
Scope", "group": "security", "label": "producer,security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "description": "OAuth2 scope" },
-    "oauth2TokenEndpoint": { "index": 65, "kind": "parameter", "displayName": 
"Oauth2 Token Endpoint", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"OAuth2 Resource Indicator" },
-    "sslContextParameters": { "index": 66, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"autowired": false, "secret": false, "description": "To configure security 
using SSLContextParameters. Important: Only one instance of 
org.apache.camel.util.jsse.SSLContextParameters is supported per HttpComponent. 
If you [...]
-    "x509HostnameVerifier": { "index": 67, "kind": "parameter", "displayName": 
"X509 Hostname Verifier", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier", 
"deprecated": false, "autowired": false, "secret": false, "security": 
"insecure:ssl", "description": "To use a custom X509HostnameVerifier such as 
DefaultHostnameVerifier or NoopHostnameVerifier" }
+    "oauth2CachedTokensKey": { "index": 60, "kind": "parameter", 
"displayName": "Oauth2 Cached Tokens Key", "group": "security", "label": 
"producer,security", "required": false, "type": "enum", "javaType": 
"org.apache.camel.component.http.OAuth2CachedTokensKey", "enum": [ "HOST_ONLY", 
"HOST_AND_PATH", "FULL_URI" ], "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "HOST_ONLY", "description": "How cached OAuth2 
tokens are shared between requests when oauth2CacheTok [...]
+    "oauth2CacheTokens": { "index": 61, "kind": "parameter", "displayName": 
"Oauth2 Cache Tokens", "group": "security", "label": "producer,security", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Whether to cache OAuth2 client tokens." },
+    "oauth2ClientId": { "index": 62, "kind": "parameter", "displayName": 
"Oauth2 Client Id", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client id" },
+    "oauth2ClientSecret": { "index": 63, "kind": "parameter", "displayName": 
"Oauth2 Client Secret", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": true, "security": "secret", 
"description": "OAuth2 client secret" },
+    "oauth2ResourceIndicator": { "index": 64, "kind": "parameter", 
"displayName": "Oauth2 Resource Indicator", "group": "security", "label": 
"producer,security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "OAuth2 Token endpoint" },
+    "oauth2Scope": { "index": 65, "kind": "parameter", "displayName": "Oauth2 
Scope", "group": "security", "label": "producer,security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "description": "OAuth2 scope" },
+    "oauth2TokenEndpoint": { "index": 66, "kind": "parameter", "displayName": 
"Oauth2 Token Endpoint", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"OAuth2 Resource Indicator" },
+    "sslContextParameters": { "index": 67, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"autowired": false, "secret": false, "description": "To configure security 
using SSLContextParameters. Important: Only one instance of 
org.apache.camel.util.jsse.SSLContextParameters is supported per HttpComponent. 
If you [...]
+    "x509HostnameVerifier": { "index": 68, "kind": "parameter", "displayName": 
"X509 Hostname Verifier", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier", 
"deprecated": false, "autowired": false, "secret": false, "security": 
"insecure:ssl", "description": "To use a custom X509HostnameVerifier such as 
DefaultHostnameVerifier or NoopHostnameVerifier" },
+    "oauth2CachedTokensKeyResolver": { "index": 69, "kind": "parameter", 
"displayName": "Oauth2 Cached Tokens Key Resolver", "group": "security 
(advanced)", "label": "producer,security,advanced", "required": false, "type": 
"object", "javaType": 
"org.apache.camel.component.http.OAuth2CachedTokensKeyResolver", "deprecated": 
false, "autowired": false, "secret": false, "description": "To use a custom 
strategy to compute the key of cached OAuth2 tokens when oauth2CacheTokens is 
enabled. When  [...]
   }
 }
diff --git a/components/camel-http/src/main/docs/http-component.adoc 
b/components/camel-http/src/main/docs/http-component.adoc
index ddca2c8f7c32..094bfccca34a 100644
--- a/components/camel-http/src/main/docs/http-component.adoc
+++ b/components/camel-http/src/main/docs/http-component.adoc
@@ -678,6 +678,63 @@ Camel only provides support for OAuth2 client credentials 
flow
 [IMPORTANT]
 Camel does not perform any validation in access token. It's up to the 
underlying service to validate it.
 
+==== Caching tokens
+
+With `oauth2CacheTokens=true`, Camel caches the token until it expires (see 
`oauth2CachedTokensDefaultExpirySeconds`
+and `oauth2CachedTokensExpirationMarginSeconds`). The option 
`oauth2CachedTokensKey` decides which requests share a
+cached token:
+
+* `HOST_ONLY` (default): one token shared by all requests to the same scheme, 
host and port.
+* `HOST_AND_PATH`: one token per path. The query is not taken into account.
+* `FULL_URI`: one token per request URI, including the query (the behaviour 
before Camel 4.23).
+
+The client id, client secret, token endpoint, scope and resource indicator are 
always part of the key. The token
+request itself does not depend on the request URI, so the default `HOST_ONLY` 
does not request a new token for every
+new URI, for example with dynamic paths or queries in `toD`.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:start")
+  
.toD("https://localhost:9090/event/${header.eventId}?oauth2ClientId=my-client-id&oauth2ClientSecret=my-client-secret&oauth2TokenEndpoint=https://localhost:8080/realms/master/protocol/openid-connect/token&oauth2CacheTokens=true&oauth2CachedTokensKey=HOST_ONLY";);
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:start"/>
+  <toD 
uri="https://localhost:9090/event/${header.eventId}?oauth2ClientId=my-client-id&amp;oauth2ClientSecret=my-client-secret&amp;oauth2TokenEndpoint=https://localhost:8080/realms/master/protocol/openid-connect/token&amp;oauth2CacheTokens=true&amp;oauth2CachedTokensKey=HOST_ONLY"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:start
+      steps:
+        - toD:
+            uri: "https://localhost:9090/event/${header.eventId}";
+            parameters:
+              oauth2ClientId: my-client-id
+              oauth2ClientSecret: my-client-secret
+              oauth2TokenEndpoint: 
"https://localhost:8080/realms/master/protocol/openid-connect/token";
+              oauth2CacheTokens: true
+              oauth2CachedTokensKey: HOST_ONLY
+----
+====
+
+For other rules, implement 
`org.apache.camel.component.http.OAuth2CachedTokensKeyResolver` and refer to it 
with
+`oauth2CachedTokensKeyResolver=#myResolver`. It takes precedence over 
`oauth2CachedTokensKey`. When it returns `null`,
+the token for that request is not cached.
+
 === Vault-backed credential rotation
 
 When using vault-backed property placeholders (e.g. 
`{{aws:mySecret/password}}`) with
diff --git 
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java
 
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java
index 352173cbae48..9ca7ed29d561 100644
--- 
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java
+++ 
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java
@@ -43,6 +43,7 @@ import org.apache.camel.spi.RestConfiguration;
 import org.apache.camel.spi.RestProducerFactory;
 import org.apache.camel.spi.annotations.Component;
 import org.apache.camel.support.CamelContextHelper;
+import org.apache.camel.support.EndpointHelper;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.PropertyBindingSupport;
 import org.apache.camel.support.RestProducerFactoryHelper;
@@ -299,6 +300,24 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
                 "oauth2BodyAuthentication",
                 boolean.class,
                 configDefaults.isOauth2BodyAuthentication());
+        // not removed from the parameters, so the endpoint gets the option too
+        OAuth2CachedTokensKeyResolver cachedTokensKeyResolver = null;
+        Object resolver = parameters.get("oauth2CachedTokensKeyResolver");
+        if (resolver instanceof OAuth2CachedTokensKeyResolver r) {
+            cachedTokensKeyResolver = r;
+        } else if (resolver != null) {
+            cachedTokensKeyResolver = EndpointHelper.resolveReferenceParameter(
+                    getCamelContext(), resolver.toString(), 
OAuth2CachedTokensKeyResolver.class);
+            // bind the same instance on the endpoint rather than resolving 
the reference again
+            parameters.put("oauth2CachedTokensKeyResolver", 
cachedTokensKeyResolver);
+        }
+        if (cachedTokensKeyResolver == null) {
+            cachedTokensKeyResolver = getParameter(
+                    parameters,
+                    "oauth2CachedTokensKey",
+                    OAuth2CachedTokensKey.class,
+                    OAuth2CachedTokensKey.HOST_ONLY);
+        }
         if (clientId != null && clientSecret != null && tokenEndpoint != null) 
{
             return CompositeHttpConfigurer.combineConfigurers(configurer,
                     new OAuth2ClientConfigurer(
@@ -311,7 +330,8 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
                             cachedTokensDefaultExpirySeconds,
                             cachedTokensExpirationMarginSeconds,
                             useBodyAuthentication,
-                            targetUri));
+                            targetUri,
+                            cachedTokensKeyResolver));
         }
         return configurer;
     }
diff --git 
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpEndpoint.java
 
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpEndpoint.java
index 5a453b2e5fc4..f7f3c664db22 100644
--- 
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpEndpoint.java
+++ 
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpEndpoint.java
@@ -197,6 +197,16 @@ public class HttpEndpoint extends HttpCommonEndpoint 
implements LineNumberAware
     @UriParam(label = "producer,advanced", defaultValue = "true",
               description = "Whether the Content-Type header should automatic 
include charset for string based content.")
     private boolean contentTypeCharsetEnabled = true;
+    @UriParam(label = "producer,security", defaultValue = "HOST_ONLY",
+              description = "How cached OAuth2 tokens are shared between 
requests when oauth2CacheTokens is enabled."
+                            + " FULL_URI uses one token per request URI 
including the query, HOST_AND_PATH one per path,"
+                            + " and HOST_ONLY one per scheme, host and port. 
The token request does not depend on the"
+                            + " request URI, so HOST_AND_PATH and HOST_ONLY 
request fewer tokens.")
+    private OAuth2CachedTokensKey oauth2CachedTokensKey = 
OAuth2CachedTokensKey.HOST_ONLY;
+    @UriParam(label = "producer,security,advanced",
+              description = "To use a custom strategy to compute the key of 
cached OAuth2 tokens when oauth2CacheTokens is"
+                            + " enabled. When set, oauth2CachedTokensKey is 
not used.")
+    private OAuth2CachedTokensKeyResolver oauth2CachedTokensKeyResolver;
 
     public HttpEndpoint() {
     }
@@ -869,4 +879,28 @@ public class HttpEndpoint extends HttpCommonEndpoint 
implements LineNumberAware
         return -1;
     }
 
+    public OAuth2CachedTokensKey getOauth2CachedTokensKey() {
+        return oauth2CachedTokensKey;
+    }
+
+    /**
+     * How cached OAuth2 tokens are shared between requests when 
oauth2CacheTokens is enabled. FULL_URI uses one token
+     * per request URI including the query, HOST_AND_PATH one per path, and 
HOST_ONLY one per scheme, host and port. The
+     * token request does not depend on the request URI, so HOST_AND_PATH and 
HOST_ONLY request fewer tokens.
+     */
+    public void setOauth2CachedTokensKey(OAuth2CachedTokensKey 
oauth2CachedTokensKey) {
+        this.oauth2CachedTokensKey = oauth2CachedTokensKey;
+    }
+
+    public OAuth2CachedTokensKeyResolver getOauth2CachedTokensKeyResolver() {
+        return oauth2CachedTokensKeyResolver;
+    }
+
+    /**
+     * To use a custom strategy to compute the key of cached OAuth2 tokens 
when oauth2CacheTokens is enabled. When set,
+     * oauth2CachedTokensKey is not used.
+     */
+    public void setOauth2CachedTokensKeyResolver(OAuth2CachedTokensKeyResolver 
oauth2CachedTokensKeyResolver) {
+        this.oauth2CachedTokensKeyResolver = oauth2CachedTokensKeyResolver;
+    }
 }
diff --git 
a/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2CachedTokensKey.java
 
b/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2CachedTokensKey.java
new file mode 100644
index 000000000000..4e30169fb3fb
--- /dev/null
+++ 
b/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2CachedTokensKey.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.http;
+
+import java.net.URI;
+import java.util.Locale;
+
+/**
+ * The built-in strategies for the cache key of OAuth2 tokens, see {@link 
OAuth2CachedTokensKeyResolver}.
+ *
+ * @since 4.23
+ */
+public enum OAuth2CachedTokensKey implements OAuth2CachedTokensKeyResolver {
+
+    /**
+     * One token per scheme, host and port (the default).
+     */
+    HOST_ONLY {
+        @Override
+        public String resolveKey(URI requestUri) {
+            return authority(requestUri);
+        }
+    },
+    /**
+     * One token per scheme, host, port and path. The query is not taken into 
account.
+     */
+    HOST_AND_PATH {
+        @Override
+        public String resolveKey(URI requestUri) {
+            String path = requestUri.getRawPath();
+            return authority(requestUri) + (path != null ? path : "");
+        }
+    },
+    /**
+     * One token per request URI, including the query (the default before 
Camel 4.23). The URI is compared as written,
+     * so URIs that differ only in the case of the scheme or host get separate 
tokens.
+     */
+    FULL_URI {
+        @Override
+        public String resolveKey(URI requestUri) {
+            return requestUri.toString();
+        }
+    };
+
+    private static String authority(URI uri) {
+        String scheme = uri.getScheme() != null ? 
uri.getScheme().toLowerCase(Locale.ROOT) : "";
+        String host = uri.getHost() != null ? 
uri.getHost().toLowerCase(Locale.ROOT) : "";
+        return scheme + "://" + host + ":" + 
OAuth2ClientConfigurer.effectivePort(uri);
+    }
+}
diff --git 
a/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2CachedTokensKeyResolver.java
 
b/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2CachedTokensKeyResolver.java
new file mode 100644
index 000000000000..544e1c9c07d1
--- /dev/null
+++ 
b/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2CachedTokensKeyResolver.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.http;
+
+import java.net.URI;
+
+/**
+ * Computes how cached OAuth2 tokens are shared between requests, when the 
http endpoint caches tokens
+ * ({@code oauth2CacheTokens=true}).
+ * <p>
+ * The key returned for a request decides which cached token it can reuse. The 
client id, client secret, token endpoint,
+ * scope and resource indicator are always added to the key by Camel, so a 
resolver only decides how much of the request
+ * URI it takes into account. The token request does not include the request 
URI, so a key that leaves out parts of the
+ * URI only means fewer token requests.
+ * <p>
+ * {@link OAuth2CachedTokensKey} has the built-in strategies. Use the endpoint 
option
+ * {@code oauth2CachedTokensKeyResolver} to plug in a custom one.
+ *
+ * @since 4.23
+ */
+@FunctionalInterface
+public interface OAuth2CachedTokensKeyResolver {
+
+    /**
+     * Computes the cache key for a request.
+     *
+     * @param  requestUri the URI of the HTTP request that needs a token
+     * @return            the key (requests with equal keys share a cached 
token), or <tt>null</tt> to not cache the
+     *                    token for this request
+     */
+    String resolveKey(URI requestUri);
+}
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 cf321c3e61e2..a8b482286aea 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
@@ -59,6 +59,7 @@ public class OAuth2ClientConfigurer extends ServiceSupport 
implements HttpClient
     private final boolean useBodyAuthentication;
     private final String resourceIndicator;
     private final URI targetUri;
+    private final OAuth2CachedTokensKeyResolver cachedTokensKeyResolver;
     private HttpClient httpClient;
 
     public OAuth2ClientConfigurer(String clientId, String clientSecret, String 
tokenEndpoint, String resourceIndicator,
@@ -66,19 +67,25 @@ public class OAuth2ClientConfigurer extends ServiceSupport 
implements HttpClient
                                   long cachedTokensDefaultExpirySeconds, long 
cachedTokensExpirationMarginSeconds,
                                   boolean useBodyAuthentication) {
         this(clientId, clientSecret, tokenEndpoint, resourceIndicator, scope, 
cacheTokens,
-             cachedTokensDefaultExpirySeconds, 
cachedTokensExpirationMarginSeconds, useBodyAuthentication, null);
+             cachedTokensDefaultExpirySeconds, 
cachedTokensExpirationMarginSeconds, useBodyAuthentication, null,
+             OAuth2CachedTokensKey.HOST_ONLY);
     }
 
     /**
-     * @param targetUri the URI the endpoint addresses. The bearer token is 
only attached to requests for the same
-     *                  authority, so that a redirect chosen by the remote 
server cannot collect it. Null keeps the
-     *                  previous behaviour of attaching it to whatever 
authority the request names.
+     * @param targetUri               the URI the endpoint addresses. The 
bearer token is only attached to requests for
+     *                                the same authority, so that a redirect 
chosen by the remote server cannot collect
+     *                                it. Null keeps the previous behaviour of 
attaching it to whatever authority the
+     *                                request names.
+     * @param cachedTokensKeyResolver computes which requests share a cached 
token (only used when caching tokens)
      */
     OAuth2ClientConfigurer(String clientId, String clientSecret, String 
tokenEndpoint, String resourceIndicator,
                            String scope, boolean cacheTokens,
                            long cachedTokensDefaultExpirySeconds, long 
cachedTokensExpirationMarginSeconds,
-                           boolean useBodyAuthentication, URI targetUri) {
+                           boolean useBodyAuthentication, URI targetUri,
+                           OAuth2CachedTokensKeyResolver 
cachedTokensKeyResolver) {
         this.targetUri = targetUri;
+        this.cachedTokensKeyResolver = cachedTokensKeyResolver != null
+                ? cachedTokensKeyResolver : OAuth2CachedTokensKey.HOST_ONLY;
         this.clientId = clientId;
         this.clientSecret = clientSecret;
         this.tokenEndpoint = tokenEndpoint;
@@ -105,9 +112,10 @@ public class OAuth2ClientConfigurer extends ServiceSupport 
implements HttpClient
                         requestUri, targetUri);
                 return;
             }
-            OAuth2URIAndCredentials uriAndCredentials = new 
OAuth2URIAndCredentials(
-                    requestUri, clientId, clientSecret, tokenEndpoint, scope, 
resourceIndicator);
-            if (cacheTokens) {
+            String requestKey = cacheTokens ? 
cachedTokensKeyResolver.resolveKey(requestUri) : null;
+            if (requestKey != null) {
+                OAuth2URIAndCredentials uriAndCredentials = new 
OAuth2URIAndCredentials(
+                        requestKey, clientId, clientSecret, tokenEndpoint, 
scope, resourceIndicator);
                 if (tokenCache.containsKey(uriAndCredentials)
                         && 
!tokenCache.get(uriAndCredentials).isExpiredWithMargin(cachedTokensExpirationMarginSeconds))
 {
                     request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + 
tokenCache.get(uriAndCredentials).getToken());
@@ -143,7 +151,11 @@ public class OAuth2ClientConfigurer extends ServiceSupport 
implements HttpClient
                 && effectivePort(targetUri) == effectivePort(requestUri);
     }
 
-    private static int effectivePort(URI uri) {
+    /**
+     * The port of the URI, or the default port of its scheme (80 for http, 
443 for https) when it has none, or -1 for
+     * another scheme without a port.
+     */
+    static int effectivePort(URI uri) {
         if (uri.getPort() >= 0) {
             return uri.getPort();
         }
@@ -234,12 +246,14 @@ public class OAuth2ClientConfigurer extends 
ServiceSupport implements HttpClient
     /**
      * Cache key for a minted token.
      * <p>
+     * The request key comes from the {@link OAuth2CachedTokensKeyResolver}: 
by default the full request URI.
+     * <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,
+    private record OAuth2URIAndCredentials(String requestKey, String clientId, 
String clientSecret, String tokenEndpoint,
             String scope, String resourceIndicator) {
     }
 
diff --git 
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2CachedTokensKeyTest.java
 
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2CachedTokensKeyTest.java
new file mode 100644
index 000000000000..525f1f773020
--- /dev/null
+++ 
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2CachedTokensKeyTest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.http;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.http.handler.HeaderValidationHandler;
+import org.apache.camel.component.http.handler.OAuth2TokenRequestHandler;
+import org.apache.hc.client5.http.HttpHostConnectException;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
+import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * The options that decide which requests share a cached OAuth2 token 
(CAMEL-22080). The default ({@code HOST_ONLY}) and
+ * {@code FULL_URI} are also tested in {@link HttpOAuth2TokenCachingTest}.
+ * <p>
+ * Same approach as {@link HttpOAuth2TokenCachingTest}: the first request 
caches a token, then the token endpoint is
+ * closed. A second request that reuses the cached token succeeds; one that 
has to request a new token fails to connect.
+ */
+public class HttpOAuth2CachedTokensKeyTest extends BaseHttpTest {
+
+    private static final String FAKE_TOKEN = "xxx.yyy.zzz";
+    private static final String CLIENT_ID = "test-client";
+    private static final String CLIENT_SECRET = "test-secret";
+
+    @BindToRegistry("onePerTarget")
+    private final OAuth2CachedTokensKeyResolver onePerTarget = uri -> "target";
+
+    @BindToRegistry("neverCache")
+    private final OAuth2CachedTokensKeyResolver neverCache = uri -> null;
+
+    @Override
+    public void setupResources() throws Exception {
+    }
+
+    @Test
+    public void aDifferentQueryReusesTheTokenWithHostAndPath() throws 
Exception {
+        assertSecondRequestReusesToken("/post?eventId=1", "/post?eventId=2", 
"&oauth2CachedTokensKey=HOST_AND_PATH");
+    }
+
+    @Test
+    public void aDifferentPathRequestsANewTokenWithHostAndPath() throws 
Exception {
+        assertSecondRequestRequestsNewToken("/post", "/other", 
"&oauth2CachedTokensKey=HOST_AND_PATH");
+    }
+
+    @Test
+    public void aDifferentQueryRequestsANewTokenWithFullUri() throws Exception 
{
+        assertSecondRequestRequestsNewToken("/post?eventId=1", 
"/post?eventId=2", "&oauth2CachedTokensKey=FULL_URI");
+    }
+
+    @Test
+    public void theSameUriReusesTheTokenWithFullUri() throws Exception {
+        assertSecondRequestReusesToken("/post?eventId=1", "/post?eventId=1", 
"&oauth2CachedTokensKey=FULL_URI");
+    }
+
+    @Test
+    public void aCustomResolverDecidesTheKey() throws Exception {
+        // the resolver takes precedence over oauth2CachedTokensKey
+        assertSecondRequestReusesToken("/post", "/other",
+                
"&oauth2CachedTokensKey=FULL_URI&oauth2CachedTokensKeyResolver=#onePerTarget");
+    }
+
+    @Test
+    public void aCustomResolverReturningNullDoesNotCache() throws Exception {
+        assertSecondRequestRequestsNewToken("/post", "/post", 
"&oauth2CachedTokensKeyResolver=#neverCache");
+    }
+
+    @Test
+    public void theEndpointHasTheOptions() throws Exception {
+        try (HttpServer localServer = createLocalServer()) {
+            HttpEndpoint endpoint = context.getEndpoint("http://localhost:"; + 
localServer.getLocalPort()
+                                                        + 
"/post?oauth2CachedTokensKey=HOST_AND_PATH",
+                    HttpEndpoint.class);
+            assertEquals(OAuth2CachedTokensKey.HOST_AND_PATH, 
endpoint.getOauth2CachedTokensKey());
+            assertNull(endpoint.getOauth2CachedTokensKeyResolver());
+
+            HttpEndpoint other = context.getEndpoint("http://localhost:"; + 
localServer.getLocalPort() + "/post",
+                    HttpEndpoint.class);
+            assertEquals(OAuth2CachedTokensKey.HOST_ONLY, 
other.getOauth2CachedTokensKey());
+
+            HttpEndpoint custom = context.getEndpoint("http://localhost:"; + 
localServer.getLocalPort()
+                                                      + 
"/post?oauth2CachedTokensKeyResolver=#onePerTarget",
+                    HttpEndpoint.class);
+            assertSame(onePerTarget, 
custom.getOauth2CachedTokensKeyResolver());
+        }
+    }
+
+    @Test
+    public void aClassResolverIsTheSameInstanceOnTheEndpoint() throws 
Exception {
+        try (HttpServer localServer = createLocalServer(); HttpServer 
oauth2Server = createLocalOAuth2Server()) {
+            String uri = uri(localServer, oauth2Server, "/post",
+                    "&oauth2CachedTokensKeyResolver=#class:" + 
RecordingResolver.class.getName());
+            assertOk(template.request(uri, e -> {
+            }));
+
+            RecordingResolver resolver = 
assertInstanceOf(RecordingResolver.class,
+                    context.getEndpoint(uri, 
HttpEndpoint.class).getOauth2CachedTokensKeyResolver());
+            // #class: creates a new instance each time it is resolved: the 
endpoint must have the one that was used
+            assertEquals(1, resolver.requests.size());
+            assertEquals("/post", resolver.requests.get(0).getPath());
+        }
+    }
+
+    /**
+     * Records the requests it computed a key for, and does not cache their 
tokens.
+     */
+    public static class RecordingResolver implements 
OAuth2CachedTokensKeyResolver {
+        final List<URI> requests = new CopyOnWriteArrayList<>();
+
+        @Override
+        public String resolveKey(URI requestUri) {
+            requests.add(requestUri);
+            return null;
+        }
+    }
+
+    private void assertSecondRequestReusesToken(String first, String second, 
String options) throws Exception {
+        try (HttpServer localServer = createLocalServer(); HttpServer 
oauth2Server = createLocalOAuth2Server()) {
+            assertOk(template.request(uri(localServer, oauth2Server, first, 
options), e -> {
+            }));
+            oauth2Server.close();
+            assertOk(template.request(uri(localServer, oauth2Server, second, 
options), e -> {
+            }));
+        }
+    }
+
+    private void assertSecondRequestRequestsNewToken(String first, String 
second, String options) throws Exception {
+        try (HttpServer localServer = createLocalServer(); HttpServer 
oauth2Server = createLocalOAuth2Server()) {
+            assertOk(template.request(uri(localServer, oauth2Server, first, 
options), e -> {
+            }));
+            oauth2Server.close();
+            Exchange exchange = template.request(uri(localServer, 
oauth2Server, second, options), e -> {
+            });
+            assertInstanceOf(HttpHostConnectException.class, 
exchange.getException());
+        }
+    }
+
+    private static String uri(HttpServer localServer, HttpServer oauth2Server, 
String pathAndQuery, String options) {
+        String tokenEndpoint = "http://localhost:"; + 
oauth2Server.getLocalPort() + "/token";
+        String separator = pathAndQuery.contains("?") ? "&" : "?";
+        return "http://localhost:"; + localServer.getLocalPort() + pathAndQuery 
+ separator
+               + "httpMethod=POST&oauth2ClientId=" + CLIENT_ID + 
"&oauth2ClientSecret=" + CLIENT_SECRET
+               + "&oauth2TokenEndpoint=" + tokenEndpoint + 
"&oauth2CacheTokens=true" + options;
+    }
+
+    private static void assertOk(Exchange exchange) {
+        assertNotNull(exchange);
+        assertNull(exchange.getException());
+        assertEquals(HttpStatus.SC_OK, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+    }
+
+    private HttpServer createLocalServer() throws Exception {
+        Map<String, String> expectedHeaders = Map.of("Authorization", "Bearer 
" + FAKE_TOKEN);
+        HttpServer localServer = ServerBootstrap.bootstrap()
+                
.setCanonicalHostName("localhost").setHttpProcessor(getBasicHttpProcessor())
+                
.setConnectionReuseStrategy(getConnectionReuseStrategy()).setResponseFactory(getHttpResponseFactory())
+                .setSslContext(getSSLContext())
+                .register("/post", new HeaderValidationHandler("POST", null, 
null, null, expectedHeaders))
+                .register("/other", new HeaderValidationHandler("POST", null, 
null, null, expectedHeaders))
+                .create();
+        localServer.start();
+        return localServer;
+    }
+
+    private HttpServer createLocalOAuth2Server() throws Exception {
+        HttpServer oauth2Server = ServerBootstrap.bootstrap()
+                
.setCanonicalHostName("localhost").setHttpProcessor(getBasicHttpProcessor())
+                
.setConnectionReuseStrategy(getConnectionReuseStrategy()).setResponseFactory(getHttpResponseFactory())
+                .setSslContext(getSSLContext())
+                .register("/token", new OAuth2TokenRequestHandler(FAKE_TOKEN, 
CLIENT_ID, CLIENT_SECRET))
+                .create();
+        oauth2Server.start();
+        return oauth2Server;
+    }
+}
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 540f6c37d1ab..21bb9c68bbd0 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
@@ -109,6 +109,130 @@ public class HttpOAuth2TokenCachingTest extends 
BaseHttpTest {
         }
     }
 
+    /**
+     * By default (oauth2CachedTokensKey=HOST_ONLY) a cached token is shared 
by every request to the same scheme, host
+     * and port, whatever the path and query (CAMEL-22080).
+     */
+    @Test
+    public void aDifferentQueryReusesTheCachedTokenByDefault() throws 
Exception {
+        assertDifferentQuery("", true);
+    }
+
+    @Test
+    public void aDifferentPathReusesTheCachedTokenByDefault() throws Exception 
{
+        assertDifferentPath("", true);
+    }
+
+    @Test
+    public void aDifferentHttpPathHeaderReusesTheCachedTokenByDefault() throws 
Exception {
+        assertDifferentHttpPathHeader("", true);
+    }
+
+    /**
+     * With oauth2CachedTokensKey=FULL_URI (the behaviour before 4.23) a 
cached token is only reused for the same
+     * request URI, so a request with a different query or path requests a new 
token.
+     */
+    @Test
+    public void aDifferentQueryRequestsANewTokenWithFullUri() throws Exception 
{
+        assertDifferentQuery("&oauth2CachedTokensKey=FULL_URI", false);
+    }
+
+    @Test
+    public void aDifferentPathRequestsANewTokenWithFullUri() throws Exception {
+        assertDifferentPath("&oauth2CachedTokensKey=FULL_URI", false);
+    }
+
+    @Test
+    public void aDifferentHttpPathHeaderRequestsANewTokenWithFullUri() throws 
Exception {
+        assertDifferentHttpPathHeader("&oauth2CachedTokensKey=FULL_URI", 
false);
+    }
+
+    /**
+     * The token request does not depend on the request URI, so with 
oauth2CachedTokensKey=HOST_ONLY a token is shared
+     * by every request to the same scheme, host and port, whatever the path 
and query (CAMEL-22080).
+     */
+    @Test
+    public void aDifferentQueryReusesTheCachedTokenWithHostOnly() throws 
Exception {
+        assertDifferentQuery("&oauth2CachedTokensKey=HOST_ONLY", true);
+    }
+
+    @Test
+    public void aDifferentPathReusesTheCachedTokenWithHostOnly() throws 
Exception {
+        assertDifferentPath("&oauth2CachedTokensKey=HOST_ONLY", true);
+    }
+
+    @Test
+    public void aDifferentHttpPathHeaderReusesTheCachedTokenWithHostOnly() 
throws Exception {
+        assertDifferentHttpPathHeader("&oauth2CachedTokensKey=HOST_ONLY", 
true);
+    }
+
+    private void assertDifferentQuery(String keyOption, boolean reused) throws 
Exception {
+        try (var localServer = createLocalServer(); var localOAuth2Server = 
createLocalOAuth2Server()) {
+            String tokenEndpoint = "http://localhost:"; + 
localOAuth2Server.getLocalPort() + "/token";
+            String options = "&httpMethod=POST&oauth2ClientId=" + clientId + 
"&oauth2ClientSecret=" + clientSecret
+                             + "&oauth2TokenEndpoint=" + tokenEndpoint + 
"&oauth2CacheTokens=true" + keyOption;
+            String base = "http://localhost:"; + localServer.getLocalPort() + 
"/post?eventId=";
+
+            assertExchange(template.request(base + "1" + options, exchange -> {
+            }));
+            localOAuth2Server.close();
+
+            Exchange exchange = template.request(base + "2" + options, 
exchange1 -> {
+            });
+            assertTokenReused(exchange, reused);
+        }
+    }
+
+    private void assertDifferentPath(String keyOption, boolean reused) throws 
Exception {
+        try (var localServer = createLocalServer(); var localOAuth2Server = 
createLocalOAuth2Server()) {
+            String tokenEndpoint = "http://localhost:"; + 
localOAuth2Server.getLocalPort() + "/token";
+            String options = "?httpMethod=POST&oauth2ClientId=" + clientId + 
"&oauth2ClientSecret=" + clientSecret
+                             + "&oauth2TokenEndpoint=" + tokenEndpoint + 
"&oauth2CacheTokens=true" + keyOption;
+            String base = "http://localhost:"; + localServer.getLocalPort();
+
+            assertExchange(template.request(base + "/post" + options, exchange 
-> {
+            }));
+            localOAuth2Server.close();
+
+            Exchange exchange = template.request(base + "/other" + options, 
exchange1 -> {
+            });
+            assertTokenReused(exchange, reused);
+        }
+    }
+
+    private void assertDifferentHttpPathHeader(String keyOption, boolean 
reused) throws Exception {
+        try (var localServer = createLocalServer(); var localOAuth2Server = 
createLocalOAuth2Server()) {
+            String tokenEndpoint = "http://localhost:"; + 
localOAuth2Server.getLocalPort() + "/token";
+
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:path")
+                            .to("http://localhost:"; + 
localServer.getLocalPort() + "?httpMethod=POST&oauth2ClientId="
+                                + clientId + "&oauth2ClientSecret=" + 
clientSecret + "&oauth2TokenEndpoint="
+                                + tokenEndpoint + "&oauth2CacheTokens=true" + 
keyOption);
+                }
+            });
+
+            assertExchange(template.send("direct:path", e -> 
e.getIn().setHeader(Exchange.HTTP_PATH, "/post")));
+            localOAuth2Server.close();
+            Exchange exchange = template.send("direct:path", e -> 
e.getIn().setHeader(Exchange.HTTP_PATH, "/other"));
+
+            assertTokenReused(exchange, reused);
+        }
+    }
+
+    /**
+     * The token endpoint is closed before the second request: a reused token 
succeeds, a new token request fails.
+     */
+    private void assertTokenReused(Exchange exchange, boolean reused) {
+        if (reused) {
+            assertExchange(exchange);
+        } else {
+            assertExceptionExchange(exchange);
+        }
+    }
+
     @Test
     public void tokenIsNotCachedWhenCacheTokensIsFalse() throws Exception {
         try (var localServer = createLocalServer(); var localOAuth2Server = 
createLocalOAuth2Server()) {
@@ -215,6 +339,13 @@ public class HttpOAuth2TokenCachingTest extends 
BaseHttpTest {
                                 null,
                                 null,
                                 expectedHeaders))
+                .register("/other",
+                        new HeaderValidationHandler(
+                                "POST",
+                                null,
+                                null,
+                                null,
+                                expectedHeaders))
                 .create();
 
         localServer.start();
diff --git 
a/components/camel-http/src/test/java/org/apache/camel/component/http/OAuth2CachedTokensKeyTest.java
 
b/components/camel-http/src/test/java/org/apache/camel/component/http/OAuth2CachedTokensKeyTest.java
new file mode 100644
index 000000000000..89f7a6158b12
--- /dev/null
+++ 
b/components/camel-http/src/test/java/org/apache/camel/component/http/OAuth2CachedTokensKeyTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.http;
+
+import java.net.URI;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+class OAuth2CachedTokensKeyTest {
+
+    @Test
+    void hostOnly() {
+        OAuth2CachedTokensKey key = OAuth2CachedTokensKey.HOST_ONLY;
+        assertEquals("https://example.com:443";, 
key.resolveKey(URI.create("https://example.com/event/1?eventId=1";)));
+        assertEquals(key.resolveKey(URI.create("https://example.com/event/1";)),
+                key.resolveKey(URI.create("https://EXAMPLE.com:443/event/2";)));
+        assertEquals("http://example.com:80";, 
key.resolveKey(URI.create("HTTP://example.com")));
+        assertNotEquals(key.resolveKey(URI.create("http://example.com/a";)),
+                key.resolveKey(URI.create("http://example.com:8080/a";)));
+        assertNotEquals(key.resolveKey(URI.create("http://example.com/a";)),
+                key.resolveKey(URI.create("https://example.com/a";)));
+    }
+
+    @Test
+    void hostAndPath() {
+        OAuth2CachedTokensKey key = OAuth2CachedTokensKey.HOST_AND_PATH;
+        assertEquals("https://example.com:443/event/1";, 
key.resolveKey(URI.create("https://example.com/event/1?eventId=1";)));
+        
assertEquals(key.resolveKey(URI.create("https://example.com/event?eventId=1";)),
+                
key.resolveKey(URI.create("https://example.com/event?eventId=2";)));
+        
assertNotEquals(key.resolveKey(URI.create("https://example.com/event/1";)),
+                key.resolveKey(URI.create("https://example.com/event/2";)));
+    }
+
+    @Test
+    void fullUri() {
+        OAuth2CachedTokensKey key = OAuth2CachedTokensKey.FULL_URI;
+        assertEquals("https://example.com/event?eventId=1";, 
key.resolveKey(URI.create("https://example.com/event?eventId=1";)));
+        
assertNotEquals(key.resolveKey(URI.create("https://example.com/event?eventId=1";)),
+                
key.resolveKey(URI.create("https://example.com/event?eventId=2";)));
+    }
+}
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 b3c7ed0ddba2..fa318f661406 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
@@ -2659,6 +2659,16 @@ All three are now part of the key. Deployments that were 
unknowingly sharing a t
 scopes, token endpoints or resource indicators will now request one token per 
distinct combination, so
 the token endpoint sees more requests than before.
 
+==== OAuth2 cached tokens are shared per host
+
+With `oauth2CacheTokens=true`, a cached OAuth2 token was only reused for the 
same request URI, so every new path or
+query (for example with `toD`) requested a new token and added an entry to a 
JVM-wide cache that was never pruned. The new
+option `oauth2CachedTokensKey` decides which requests share a cached token, 
and its default is now `HOST_ONLY`: one token
+for all requests to the same scheme, host and port. The client id, client 
secret, token endpoint, scope and resource
+indicator are still part of the key, and the bearer token is still only sent 
to the authority of the endpoint. Set
+`oauth2CachedTokensKey=FULL_URI` to keep one token per request URI as before, 
or `HOST_AND_PATH` for one per path. A
+custom `OAuth2CachedTokensKeyResolver` can be set with 
`oauth2CachedTokensKeyResolver`.
+
 === camel-pqc
 
 `FileBasedKeyLifecycleManager` stores private keys unencrypted, as Base64 
PKCS#8 inside a JSON file, and
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/HttpEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/HttpEndpointBuilderFactory.java
index a6bb3487b085..25de62629f8a 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/HttpEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/HttpEndpointBuilderFactory.java
@@ -891,6 +891,48 @@ public interface HttpEndpointBuilderFactory {
             doSetProperty("oauth2CachedTokensExpirationMarginSeconds", 
oauth2CachedTokensExpirationMarginSeconds);
             return this;
         }
+        /**
+         * How cached OAuth2 tokens are shared between requests when
+         * oauth2CacheTokens is enabled. FULL_URI uses one token per request 
URI
+         * including the query, HOST_AND_PATH one per path, and HOST_ONLY one
+         * per scheme, host and port. The token request does not depend on the
+         * request URI, so HOST_AND_PATH and HOST_ONLY request fewer tokens.
+         * 
+         * The option is a:
+         * <code>org.apache.camel.component.http.OAuth2CachedTokensKey</code>
+         * type.
+         * 
+         * Default: HOST_ONLY
+         * Group: security
+         * 
+         * @param oauth2CachedTokensKey the value to set
+         * @return the dsl builder
+         */
+        default HttpEndpointBuilder 
oauth2CachedTokensKey(org.apache.camel.component.http.OAuth2CachedTokensKey 
oauth2CachedTokensKey) {
+            doSetProperty("oauth2CachedTokensKey", oauth2CachedTokensKey);
+            return this;
+        }
+        /**
+         * How cached OAuth2 tokens are shared between requests when
+         * oauth2CacheTokens is enabled. FULL_URI uses one token per request 
URI
+         * including the query, HOST_AND_PATH one per path, and HOST_ONLY one
+         * per scheme, host and port. The token request does not depend on the
+         * request URI, so HOST_AND_PATH and HOST_ONLY request fewer tokens.
+         * 
+         * The option will be converted to a
+         * <code>org.apache.camel.component.http.OAuth2CachedTokensKey</code>
+         * type.
+         * 
+         * Default: HOST_ONLY
+         * Group: security
+         * 
+         * @param oauth2CachedTokensKey the value to set
+         * @return the dsl builder
+         */
+        default HttpEndpointBuilder oauth2CachedTokensKey(String 
oauth2CachedTokensKey) {
+            doSetProperty("oauth2CachedTokensKey", oauth2CachedTokensKey);
+            return this;
+        }
         /**
          * Whether to cache OAuth2 client tokens.
          * 
@@ -1909,6 +1951,40 @@ public interface HttpEndpointBuilderFactory {
             doSetProperty("useSystemProperties", useSystemProperties);
             return this;
         }
+        /**
+         * To use a custom strategy to compute the key of cached OAuth2 tokens
+         * when oauth2CacheTokens is enabled. When set, oauth2CachedTokensKey 
is
+         * not used.
+         * 
+         * The option is a:
+         * 
<code>org.apache.camel.component.http.OAuth2CachedTokensKeyResolver</code> type.
+         * 
+         * Group: security (advanced)
+         * 
+         * @param oauth2CachedTokensKeyResolver the value to set
+         * @return the dsl builder
+         */
+        default AdvancedHttpEndpointBuilder 
oauth2CachedTokensKeyResolver(org.apache.camel.component.http.OAuth2CachedTokensKeyResolver
 oauth2CachedTokensKeyResolver) {
+            doSetProperty("oauth2CachedTokensKeyResolver", 
oauth2CachedTokensKeyResolver);
+            return this;
+        }
+        /**
+         * To use a custom strategy to compute the key of cached OAuth2 tokens
+         * when oauth2CacheTokens is enabled. When set, oauth2CachedTokensKey 
is
+         * not used.
+         * 
+         * The option will be converted to a
+         * 
<code>org.apache.camel.component.http.OAuth2CachedTokensKeyResolver</code> type.
+         * 
+         * Group: security (advanced)
+         * 
+         * @param oauth2CachedTokensKeyResolver the value to set
+         * @return the dsl builder
+         */
+        default AdvancedHttpEndpointBuilder 
oauth2CachedTokensKeyResolver(String oauth2CachedTokensKeyResolver) {
+            doSetProperty("oauth2CachedTokensKeyResolver", 
oauth2CachedTokensKeyResolver);
+            return this;
+        }
     }
 
     public interface HttpBuilders {

Reply via email to