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&oauth2ClientSecret=my-client-secret&oauth2TokenEndpoint=https://localhost:8080/realms/master/protocol/openid-connect/token&oauth2CacheTokens=true&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&oauth2ClientSecret=my-client-secret&oauth2TokenEndpoint=https://localhost:8080/realms/master/protocol/openid-connect/token&oauth2CacheTokens=true&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 {