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 2b08741b74b CAMEL-20503: camel-http OAuth2 support for caching / 
refreshing tokens (#16227)
2b08741b74b is described below

commit 2b08741b74bece6c54be5bcf0f08995d78d9fc44
Author: Ivan Kulaga <kulagaivanandreev...@gmail.com>
AuthorDate: Wed Nov 13 11:14:08 2024 +0500

    CAMEL-20503: camel-http OAuth2 support for caching / refreshing tokens 
(#16227)
    
    * CAMEL-20503: camel-http OAuth2 support for caching / refreshing tokens
    - added caching oauth2 tokens for http component, and configuration 
parameters for caching
    
    * CAMEL-20503: camel-http OAuth2 support for caching / refreshing tokens
    - added new oauth2 options to the exclude list in WebsocketEndpoint
---
 .../org/apache/camel/catalog/components/http.json  |  15 +-
 .../org/apache/camel/catalog/components/https.json |  15 +-
 .../atmosphere/websocket/WebsocketEndpoint.java    |   2 +-
 .../camel/http/common/HttpCommonEndpoint.java      |  51 ++++++
 .../camel/http/common/HttpConfiguration.java       |  52 ++++++
 .../component/http/HttpEndpointConfigurer.java     |  18 ++
 .../component/http/HttpEndpointUriFactory.java     |   5 +-
 .../org/apache/camel/component/http/http.json      |  15 +-
 .../org/apache/camel/component/http/https.json     |  15 +-
 .../apache/camel/component/http/HttpComponent.java |  26 ++-
 .../component/http/OAuth2ClientConfigurer.java     | 150 +++++++++++++---
 .../component/http/HttpOAuth2TokenCachingTest.java | 189 +++++++++++++++++++++
 .../http/handler/OAuth2TokenRequestHandler.java    |   1 -
 .../endpoint/dsl/HttpEndpointBuilderFactory.java   | 106 ++++++++++++
 14 files changed, 605 insertions(+), 55 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 caa43c1b699..2e2f0473f08 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
@@ -134,11 +134,14 @@
     "authMethodPriority": { "index": 46, "kind": "parameter", "displayName": 
"Auth Method Priority", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", "enum": [ 
"Basic", "Digest", "NTLM" ], "deprecated": false, "autowired": false, "secret": 
false, "description": "Which authentication method to prioritize to use, either 
as Basic, Digest or NTLM." },
     "authPassword": { "index": 47, "kind": "parameter", "displayName": "Auth 
Password", "group": "security", "label": "producer,security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "description": "Authentication password" },
     "authUsername": { "index": 48, "kind": "parameter", "displayName": "Auth 
Username", "group": "security", "label": "producer,security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "description": "Authentication username" },
-    "oauth2ClientId": { "index": 49, "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, "description": "OAuth2 
client id" },
-    "oauth2ClientSecret": { "index": 50, "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, "description": "OAuth2 
client secret" },
-    "oauth2Scope": { "index": 51, "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": 52, "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 Token endpoint" },
-    "sslContextParameters": { "index": 53, "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": 54, "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, "description": "To 
use a custom X509HostnameVerifier such as DefaultHostnameVerifier or 
NoopHostnameVerifier" }
+    "oauth2CachedTokensDefaultExpirySeconds": { "index": 49, "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": 50, "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": 51, "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": 52, "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, "description": "OAuth2 
client id" },
+    "oauth2ClientSecret": { "index": 53, "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, "description": "OAuth2 
client secret" },
+    "oauth2Scope": { "index": 54, "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": 55, "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 Token endpoint" },
+    "sslContextParameters": { "index": 56, "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": 57, "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, "description": "To 
use a custom X509HostnameVerifier such as DefaultHostnameVerifier or 
NoopHostnameVerifier" }
   }
 }
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 269f80bfe23..d897219586d 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
@@ -134,11 +134,14 @@
     "authMethodPriority": { "index": 46, "kind": "parameter", "displayName": 
"Auth Method Priority", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", "enum": [ 
"Basic", "Digest", "NTLM" ], "deprecated": false, "autowired": false, "secret": 
false, "description": "Which authentication method to prioritize to use, either 
as Basic, Digest or NTLM." },
     "authPassword": { "index": 47, "kind": "parameter", "displayName": "Auth 
Password", "group": "security", "label": "producer,security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "description": "Authentication password" },
     "authUsername": { "index": 48, "kind": "parameter", "displayName": "Auth 
Username", "group": "security", "label": "producer,security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "description": "Authentication username" },
-    "oauth2ClientId": { "index": 49, "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, "description": "OAuth2 
client id" },
-    "oauth2ClientSecret": { "index": 50, "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, "description": "OAuth2 
client secret" },
-    "oauth2Scope": { "index": 51, "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": 52, "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 Token endpoint" },
-    "sslContextParameters": { "index": 53, "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": 54, "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, "description": "To 
use a custom X509HostnameVerifier such as DefaultHostnameVerifier or 
NoopHostnameVerifier" }
+    "oauth2CachedTokensDefaultExpirySeconds": { "index": 49, "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": 50, "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": 51, "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": 52, "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, "description": "OAuth2 
client id" },
+    "oauth2ClientSecret": { "index": 53, "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, "description": "OAuth2 
client secret" },
+    "oauth2Scope": { "index": 54, "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": 55, "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 Token endpoint" },
+    "sslContextParameters": { "index": 56, "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": 57, "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, "description": "To 
use a custom X509HostnameVerifier such as DefaultHostnameVerifier or 
NoopHostnameVerifier" }
   }
 }
diff --git 
a/components/camel-atmosphere-websocket/src/main/java/org/apache/camel/component/atmosphere/websocket/WebsocketEndpoint.java
 
b/components/camel-atmosphere-websocket/src/main/java/org/apache/camel/component/atmosphere/websocket/WebsocketEndpoint.java
index 186ddae6813..87fb493d523 100644
--- 
a/components/camel-atmosphere-websocket/src/main/java/org/apache/camel/component/atmosphere/websocket/WebsocketEndpoint.java
+++ 
b/components/camel-atmosphere-websocket/src/main/java/org/apache/camel/component/atmosphere/websocket/WebsocketEndpoint.java
@@ -41,7 +41,7 @@ import org.apache.camel.util.StringHelper;
                               + 
"copyHeaders,httpMethod,ignoreResponseBody,preserveHostHeader,throwExceptionOnFailure,okStatusCodeRange,"
                               + 
"proxyAuthScheme,proxyAuthMethod,proxyAuthUsername,proxyAuthPassword,proxyAuthHost,proxyAuthPort,proxyAuthDomain,"
                               + 
"proxyAuthNtHost,proxyAuthScheme,proxyHost,proxyPort,"
-                              + 
"oauth2ClientId,oauth2ClientSecret,oauth2TokenEndpoint,oauth2Scope",
+                              + 
"oauth2ClientId,oauth2ClientSecret,oauth2TokenEndpoint,oauth2Scope,oauth2CacheTokens,oauth2CachedTokensDefaultExpirySeconds,oauth2CachedTokensExpirationMarginSeconds",
           annotations = {
                   "protocol=http",
           })
diff --git 
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpCommonEndpoint.java
 
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpCommonEndpoint.java
index 7ad028318c3..0d04b2514d1 100644
--- 
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpCommonEndpoint.java
+++ 
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpCommonEndpoint.java
@@ -162,6 +162,19 @@ public abstract class HttpCommonEndpoint extends 
DefaultEndpoint
     private String oauth2TokenEndpoint;
     @UriParam(label = "producer,security", description = "OAuth2 scope")
     private String oauth2Scope;
+    @UriParam(label = "producer,security", defaultValue = "false",
+              description = "Whether to cache OAuth2 client tokens.")
+    private boolean oauth2CacheTokens = false;
+    @UriParam(label = "producer,security", defaultValue = "3600",
+              description = "Default expiration time for cached OAuth2 tokens, 
in seconds. Used if token response does not contain 'expires_in' field.")
+    private long oauth2CachedTokensDefaultExpirySeconds = 3600L;
+    @UriParam(label = "producer,security", 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 over http, in seconds. "
+                            +
+                            "Set this parameter to high value if you OAuth2 
Token Endpoint answers slowly or you tokens expire quickly. "
+                            +
+                            "If you set this parameter to too small value, you 
can get 4xx http errors because camel will think that the received token is 
still valid, while in reality the token is expired for the Authentication 
server.")
+    private long oauth2CachedTokensExpirationMarginSeconds = 5L;
     @UriParam(label = "producer,security", description = "Authentication 
domain to use with NTML")
     private String authDomain;
     @UriParam(label = "producer,security", description = "Authentication host 
to use with NTML")
@@ -843,4 +856,42 @@ public abstract class HttpCommonEndpoint extends 
DefaultEndpoint
     public void setOauth2Scope(String oauth2Scope) {
         this.oauth2Scope = oauth2Scope;
     }
+
+    public boolean isOauth2CacheTokens() {
+        return oauth2CacheTokens;
+    }
+
+    /**
+     * Whether to cache OAuth2 client tokens.
+     */
+    public void setOauth2CacheTokens(boolean oauth2CacheTokens) {
+        this.oauth2CacheTokens = oauth2CacheTokens;
+    }
+
+    public long getOauth2CachedTokensDefaultExpirySeconds() {
+        return oauth2CachedTokensDefaultExpirySeconds;
+    }
+
+    /**
+     * Default expiration time for cached OAuth2 tokens, in seconds. Used if 
token response does not contain
+     * 'expires_in' field.
+     */
+    public void setOauth2CachedTokensDefaultExpirySeconds(long 
oauth2CachedTokensDefaultExpirySeconds) {
+        this.oauth2CachedTokensDefaultExpirySeconds = 
oauth2CachedTokensDefaultExpirySeconds;
+    }
+
+    public long getOauth2CachedTokensExpirationMarginSeconds() {
+        return oauth2CachedTokensExpirationMarginSeconds;
+    }
+
+    /**
+     * 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 over http, in seconds. Set this parameter to 
high value if you OAuth2 Token Endpoint
+     * answers slowly or you tokens expire quickly. If you set this parameter 
to too small value, you can get 4xx http
+     * errors because camel will think that the received token is still valid, 
while in reality the token is expired for
+     * the Authentication server.
+     */
+    public void setOauth2CachedTokensExpirationMarginSeconds(long 
cachedTokensExpirationMarginSeconds) {
+        this.oauth2CachedTokensExpirationMarginSeconds = 
cachedTokensExpirationMarginSeconds;
+    }
 }
diff --git 
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpConfiguration.java
 
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpConfiguration.java
index c0a404ab59c..4975645a296 100644
--- 
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpConfiguration.java
+++ 
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpConfiguration.java
@@ -19,6 +19,7 @@ package org.apache.camel.http.common;
 import java.io.Serializable;
 
 import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriParam;
 
 public class HttpConfiguration implements Serializable {
     private static final long serialVersionUID = 1L;
@@ -41,6 +42,19 @@ public class HttpConfiguration implements Serializable {
     private String oauth2TokenEndpoint;
     @Metadata(label = "producer,security", description = "OAuth2 scope")
     private String oauth2Scope;
+    @UriParam(label = "producer,security", defaultValue = "false",
+              description = "Whether to cache OAuth2 client tokens.")
+    private boolean oauth2CacheTokens = false;
+    @UriParam(label = "producer,security", defaultValue = "3600",
+              description = "Default expiration time for cached OAuth2 tokens, 
in seconds. Used if token response does not contain 'expires_in' field.")
+    private long oauth2CachedTokensDefaultExpirySeconds = 3600L;
+    @UriParam(label = "producer,security", 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 over http, in seconds. "
+                            +
+                            "Set this parameter to high value if you OAuth2 
Token Endpoint answers slowly or you tokens expire quickly. "
+                            +
+                            "If you set this parameter to too small value, you 
can get 4xx http errors because camel will think that the received token is 
still valid, while in reality the token is expired for the Authentication 
server.")
+    private long oauth2CachedTokensExpirationMarginSeconds = 5L;
     @Metadata(label = "producer,security", description = "Authentication 
domain to use with NTML")
     private String authDomain;
     @Metadata(label = "producer,security", description = "Authentication host 
to use with NTML")
@@ -272,4 +286,42 @@ public class HttpConfiguration implements Serializable {
     public void setOauth2Scope(String oauth2Scope) {
         this.oauth2Scope = oauth2Scope;
     }
+
+    public boolean isOauth2CacheTokens() {
+        return oauth2CacheTokens;
+    }
+
+    /**
+     * Whether to cache OAuth2 client tokens.
+     */
+    public void setOauth2CacheTokens(boolean oauth2CacheTokens) {
+        this.oauth2CacheTokens = oauth2CacheTokens;
+    }
+
+    public long getOauth2CachedTokensDefaultExpirySeconds() {
+        return oauth2CachedTokensDefaultExpirySeconds;
+    }
+
+    /**
+     * Default expiration time for cached OAuth2 tokens, in seconds. Used if 
token response does not contain
+     * 'expires_in' field.
+     */
+    public void setOauth2CachedTokensDefaultExpirySeconds(long 
oauth2CachedTokensDefaultExpirySeconds) {
+        this.oauth2CachedTokensDefaultExpirySeconds = 
oauth2CachedTokensDefaultExpirySeconds;
+    }
+
+    public long getOauth2CachedTokensExpirationMarginSeconds() {
+        return oauth2CachedTokensExpirationMarginSeconds;
+    }
+
+    /**
+     * 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 over http, in seconds. Set this parameter to 
high value if you OAuth2 Token Endpoint
+     * answers slowly or you tokens expire quickly. If you set this parameter 
to too small value, you can get 4xx http
+     * errors because camel will think that the received token is still valid, 
while in reality the token is expired for
+     * the Authentication server.
+     */
+    public void setOauth2CachedTokensExpirationMarginSeconds(long 
oauth2CachedTokensExpirationMarginSeconds) {
+        this.oauth2CachedTokensExpirationMarginSeconds = 
oauth2CachedTokensExpirationMarginSeconds;
+    }
 }
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 07c2d800785..4b10ca79243 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
@@ -85,6 +85,12 @@ public class HttpEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "lazyStartProducer": 
target.setLazyStartProducer(property(camelContext, boolean.class, value)); 
return true;
         case "maxtotalconnections":
         case "maxTotalConnections": 
target.setMaxTotalConnections(property(camelContext, int.class, value)); return 
true;
+        case "oauth2cachetokens":
+        case "oauth2CacheTokens": 
target.setOauth2CacheTokens(property(camelContext, boolean.class, value)); 
return true;
+        case "oauth2cachedtokensdefaultexpiryseconds":
+        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 "oauth2clientid":
         case "oauth2ClientId": target.setOauth2ClientId(property(camelContext, 
java.lang.String.class, value)); return true;
         case "oauth2clientsecret":
@@ -200,6 +206,12 @@ public class HttpEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "lazyStartProducer": return boolean.class;
         case "maxtotalconnections":
         case "maxTotalConnections": return int.class;
+        case "oauth2cachetokens":
+        case "oauth2CacheTokens": return boolean.class;
+        case "oauth2cachedtokensdefaultexpiryseconds":
+        case "oauth2CachedTokensDefaultExpirySeconds": return long.class;
+        case "oauth2cachedtokensexpirationmarginseconds":
+        case "oauth2CachedTokensExpirationMarginSeconds": return long.class;
         case "oauth2clientid":
         case "oauth2ClientId": return java.lang.String.class;
         case "oauth2clientsecret":
@@ -316,6 +328,12 @@ public class HttpEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "lazyStartProducer": return target.isLazyStartProducer();
         case "maxtotalconnections":
         case "maxTotalConnections": return target.getMaxTotalConnections();
+        case "oauth2cachetokens":
+        case "oauth2CacheTokens": return target.isOauth2CacheTokens();
+        case "oauth2cachedtokensdefaultexpiryseconds":
+        case "oauth2CachedTokensDefaultExpirySeconds": return 
target.getOauth2CachedTokensDefaultExpirySeconds();
+        case "oauth2cachedtokensexpirationmarginseconds":
+        case "oauth2CachedTokensExpirationMarginSeconds": return 
target.getOauth2CachedTokensExpirationMarginSeconds();
         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 76185081941..98e1aa9f5dc 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
@@ -24,7 +24,7 @@ public class HttpEndpointUriFactory extends 
org.apache.camel.support.component.E
     private static final Set<String> SECRET_PROPERTY_NAMES;
     private static final Set<String> MULTI_VALUE_PREFIXES;
     static {
-        Set<String> props = new HashSet<>(55);
+        Set<String> props = new HashSet<>(58);
         props.add("authDomain");
         props.add("authHost");
         props.add("authMethod");
@@ -57,6 +57,9 @@ public class HttpEndpointUriFactory extends 
org.apache.camel.support.component.E
         props.add("ignoreResponseBody");
         props.add("lazyStartProducer");
         props.add("maxTotalConnections");
+        props.add("oauth2CacheTokens");
+        props.add("oauth2CachedTokensDefaultExpirySeconds");
+        props.add("oauth2CachedTokensExpirationMarginSeconds");
         props.add("oauth2ClientId");
         props.add("oauth2ClientSecret");
         props.add("oauth2Scope");
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 caa43c1b699..2e2f0473f08 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
@@ -134,11 +134,14 @@
     "authMethodPriority": { "index": 46, "kind": "parameter", "displayName": 
"Auth Method Priority", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", "enum": [ 
"Basic", "Digest", "NTLM" ], "deprecated": false, "autowired": false, "secret": 
false, "description": "Which authentication method to prioritize to use, either 
as Basic, Digest or NTLM." },
     "authPassword": { "index": 47, "kind": "parameter", "displayName": "Auth 
Password", "group": "security", "label": "producer,security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "description": "Authentication password" },
     "authUsername": { "index": 48, "kind": "parameter", "displayName": "Auth 
Username", "group": "security", "label": "producer,security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "description": "Authentication username" },
-    "oauth2ClientId": { "index": 49, "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, "description": "OAuth2 
client id" },
-    "oauth2ClientSecret": { "index": 50, "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, "description": "OAuth2 
client secret" },
-    "oauth2Scope": { "index": 51, "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": 52, "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 Token endpoint" },
-    "sslContextParameters": { "index": 53, "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": 54, "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, "description": "To 
use a custom X509HostnameVerifier such as DefaultHostnameVerifier or 
NoopHostnameVerifier" }
+    "oauth2CachedTokensDefaultExpirySeconds": { "index": 49, "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": 50, "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": 51, "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": 52, "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, "description": "OAuth2 
client id" },
+    "oauth2ClientSecret": { "index": 53, "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, "description": "OAuth2 
client secret" },
+    "oauth2Scope": { "index": 54, "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": 55, "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 Token endpoint" },
+    "sslContextParameters": { "index": 56, "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": 57, "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, "description": "To 
use a custom X509HostnameVerifier such as DefaultHostnameVerifier or 
NoopHostnameVerifier" }
   }
 }
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 269f80bfe23..d897219586d 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
@@ -134,11 +134,14 @@
     "authMethodPriority": { "index": 46, "kind": "parameter", "displayName": 
"Auth Method Priority", "group": "security", "label": "producer,security", 
"required": false, "type": "string", "javaType": "java.lang.String", "enum": [ 
"Basic", "Digest", "NTLM" ], "deprecated": false, "autowired": false, "secret": 
false, "description": "Which authentication method to prioritize to use, either 
as Basic, Digest or NTLM." },
     "authPassword": { "index": 47, "kind": "parameter", "displayName": "Auth 
Password", "group": "security", "label": "producer,security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "description": "Authentication password" },
     "authUsername": { "index": 48, "kind": "parameter", "displayName": "Auth 
Username", "group": "security", "label": "producer,security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "description": "Authentication username" },
-    "oauth2ClientId": { "index": 49, "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, "description": "OAuth2 
client id" },
-    "oauth2ClientSecret": { "index": 50, "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, "description": "OAuth2 
client secret" },
-    "oauth2Scope": { "index": 51, "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": 52, "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 Token endpoint" },
-    "sslContextParameters": { "index": 53, "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": 54, "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, "description": "To 
use a custom X509HostnameVerifier such as DefaultHostnameVerifier or 
NoopHostnameVerifier" }
+    "oauth2CachedTokensDefaultExpirySeconds": { "index": 49, "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": 50, "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": 51, "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": 52, "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, "description": "OAuth2 
client id" },
+    "oauth2ClientSecret": { "index": 53, "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, "description": "OAuth2 
client secret" },
+    "oauth2Scope": { "index": 54, "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": 55, "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 Token endpoint" },
+    "sslContextParameters": { "index": 56, "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": 57, "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, "description": "To 
use a custom X509HostnameVerifier such as DefaultHostnameVerifier or 
NoopHostnameVerifier" }
   }
 }
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 23c4138dcd5..b715d065716 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
@@ -33,6 +33,7 @@ import 
org.apache.camel.component.extension.ComponentVerifierExtension;
 import org.apache.camel.http.base.HttpHelper;
 import org.apache.camel.http.common.HttpBinding;
 import org.apache.camel.http.common.HttpCommonComponent;
+import org.apache.camel.http.common.HttpConfiguration;
 import org.apache.camel.http.common.HttpRestHeaderFilterStrategy;
 import org.apache.camel.spi.BeanIntrospection;
 import org.apache.camel.spi.HeaderFilterStrategy;
@@ -235,10 +236,33 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
         String clientSecret = getParameter(parameters, "oauth2ClientSecret", 
String.class);
         String tokenEndpoint = getParameter(parameters, "oauth2TokenEndpoint", 
String.class);
         String scope = getParameter(parameters, "oauth2Scope", String.class);
+        HttpConfiguration configDefaults = new HttpConfiguration();
+        boolean cacheTokens = getParameter(
+                parameters,
+                "oauth2CacheTokens",
+                boolean.class,
+                configDefaults.isOauth2CacheTokens());
+        long cachedTokensDefaultExpirySeconds = getParameter(
+                parameters,
+                "oauth2CachedTokensDefaultExpirySeconds",
+                long.class,
+                configDefaults.getOauth2CachedTokensDefaultExpirySeconds());
+        long cachedTokensExpirationMarginSeconds = getParameter(
+                parameters,
+                "oauth2CachedTokensExpirationMarginSeconds",
+                long.class,
+                configDefaults.getOauth2CachedTokensExpirationMarginSeconds());
 
         if (clientId != null && clientSecret != null && tokenEndpoint != null) 
{
             return CompositeHttpConfigurer.combineConfigurers(configurer,
-                    new OAuth2ClientConfigurer(clientId, clientSecret, 
tokenEndpoint, scope));
+                    new OAuth2ClientConfigurer(
+                            clientId,
+                            clientSecret,
+                            tokenEndpoint,
+                            scope,
+                            cacheTokens,
+                            cachedTokensDefaultExpirySeconds,
+                            cachedTokensExpirationMarginSeconds));
         }
         return configurer;
     }
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 0701ba6b654..ee29c73b8c6 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
@@ -16,6 +16,14 @@
  */
 package org.apache.camel.component.http;
 
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
 import org.apache.camel.util.json.DeserializationException;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -37,55 +45,143 @@ public class OAuth2ClientConfigurer implements 
HttpClientConfigurer {
     private final String clientSecret;
     private final String tokenEndpoint;
     private final String scope;
+    private final boolean cacheTokens;
+    private final Long cachedTokensDefaultExpirySeconds;
+    private final Long cachedTokensExpirationMarginSeconds;
+    private final static Map<OAuth2URIAndCredentials, TokenCache> tokenCache = 
new HashMap<>();
 
-    public OAuth2ClientConfigurer(String clientId, String clientSecret, String 
tokenEndpoint, String scope) {
+    public OAuth2ClientConfigurer(String clientId, String clientSecret, String 
tokenEndpoint, String scope, boolean cacheTokens,
+                                  long cachedTokensDefaultExpirySeconds, long 
cachedTokensExpirationMarginSeconds) {
         this.clientId = clientId;
         this.clientSecret = clientSecret;
         this.tokenEndpoint = tokenEndpoint;
         this.scope = scope;
+        this.cacheTokens = cacheTokens;
+        this.cachedTokensDefaultExpirySeconds = 
cachedTokensDefaultExpirySeconds;
+        this.cachedTokensExpirationMarginSeconds = 
cachedTokensExpirationMarginSeconds;
     }
 
     @Override
     public void configureHttpClient(HttpClientBuilder clientBuilder) {
         HttpClient httpClient = clientBuilder.build();
         clientBuilder.addRequestInterceptorFirst((HttpRequest request, 
EntityDetails entity, HttpContext context) -> {
+            URI requestUri = getUriFromRequest(request);
+            OAuth2URIAndCredentials uriAndCredentials = new 
OAuth2URIAndCredentials(requestUri, clientId, clientSecret);
+            if (cacheTokens) {
+                if (tokenCache.containsKey(uriAndCredentials)
+                        && 
!tokenCache.get(uriAndCredentials).isExpiredWithMargin(cachedTokensExpirationMarginSeconds))
 {
+                    request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + 
tokenCache.get(uriAndCredentials).getToken());
+                } else {
+                    JsonObject accessTokenResponse = 
getAccessTokenResponse(httpClient);
+                    String accessToken = 
accessTokenResponse.getString("access_token");
+                    String expiresIn = 
accessTokenResponse.getString("expires_in");
+                    if (expiresIn != null && !expiresIn.isEmpty()) {
+                        tokenCache.put(uriAndCredentials, new 
TokenCache(accessToken, expiresIn));
+                    } else if (cachedTokensDefaultExpirySeconds > 0) {
+                        tokenCache.put(uriAndCredentials, new 
TokenCache(accessToken, cachedTokensDefaultExpirySeconds));
+                    }
+                    request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + 
accessToken);
+                }
+            } else {
+                JsonObject accessTokenResponse = 
getAccessTokenResponse(httpClient);
+                String accessToken = 
accessTokenResponse.getString("access_token");
+                request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + 
accessToken);
+            }
+        });
+    }
 
-            String url = tokenEndpoint;
-            if (scope != null) {
-                String sep = "?";
-                if (url.contains("?")) {
-                    sep = "&";
+    private JsonObject getAccessTokenResponse(HttpClient httpClient) throws 
IOException {
+        String url = tokenEndpoint;
+        if (scope != null) {
+            String sep = "?";
+            if (url.contains("?")) {
+                sep = "&";
+            }
+            url = url + sep + "scope=" + scope;
+        }
+
+        final HttpPost httpPost = new HttpPost(url);
+
+        httpPost.addHeader(HttpHeaders.AUTHORIZATION,
+                HttpCredentialsHelper.generateBasicAuthHeader(clientId, 
clientSecret));
+        httpPost.setEntity(new StringEntity("grant_type=client_credentials", 
ContentType.APPLICATION_FORM_URLENCODED));
+
+        AtomicReference<JsonObject> result = new AtomicReference<>();
+        httpClient.execute(httpPost, response -> {
+            try {
+                String responseString = 
EntityUtils.toString(response.getEntity());
+
+                if (response.getCode() == 200) {
+                    result.set((JsonObject) 
Jsoner.deserialize(responseString));
+                } else {
+                    throw new HttpException(
+                            "Received error response from token request with 
Status Code: " + response.getCode());
                 }
-                url = url + sep + "scope=" + scope;
+            } catch (DeserializationException e) {
+                throw new HttpException("Something went wrong when reading 
token request response", e);
             }
+            return null;
+        });
+        return result.get();
+    }
 
-            final HttpPost httpPost = new HttpPost(url);
+    private URI getUriFromRequest(HttpRequest request) {
+        URI result;
+        try {
+            result = request.getUri();
+        } catch (URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+        return result;
+    }
 
-            httpPost.addHeader(HttpHeaders.AUTHORIZATION,
-                    HttpCredentialsHelper.generateBasicAuthHeader(clientId, 
clientSecret));
-            httpPost.setEntity(new 
StringEntity("grant_type=client_credentials", 
ContentType.APPLICATION_FORM_URLENCODED));
+    private static class TokenCache {
+        private String token;
+        private Instant expirationTime;
 
-            httpClient.execute(httpPost, response -> {
+        public TokenCache() {
+        }
 
-                try {
-                    String responseString = 
EntityUtils.toString(response.getEntity());
+        public TokenCache(String token, String expires_in) {
+            this.token = token;
+            setExpirationTimeSeconds(expires_in);
+        }
 
-                    if (response.getCode() == 200) {
-                        String accessToken = ((JsonObject) 
Jsoner.deserialize(responseString)).getString("access_token");
-                        request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " 
+ accessToken);
-                    } else {
-                        throw new HttpException(
-                                "Received error response from token request 
with Status Code: " + response.getCode());
-                    }
+        public TokenCache(String accessToken, Long seconds) {
+            this.token = accessToken;
+            this.expirationTime = Instant.now().plusSeconds(seconds);
+        }
 
-                } catch (DeserializationException e) {
-                    throw new HttpException("Something went wrong when reading 
token request response", e);
-                }
+        public boolean isExpired() {
+            return Instant.now().isAfter(expirationTime);
+        }
 
-                return null;
-            });
+        public boolean isExpiredWithMargin(Long marginSeconds) {
+            return 
Instant.now().isAfter(expirationTime.minusSeconds(marginSeconds));
+        }
 
-        });
+        public void setExpirationTimeSeconds(String expires_in) {
+            this.expirationTime = 
Instant.now().plusSeconds(Long.parseLong(expires_in));
+        }
+
+        public String getToken() {
+            return token;
+        }
+
+        public void setToken(String token) {
+            this.token = token;
+        }
+
+        public Instant getExpirationTime() {
+            return expirationTime;
+        }
+
+        public void setExpirationTime(Instant expirationTime) {
+            this.expirationTime = expirationTime;
+        }
+    }
+
+    private record OAuth2URIAndCredentials(URI uri, String clientId, String 
clientSecret) {
     }
 
 }
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
new file mode 100644
index 00000000000..fb8cd4c95c2
--- /dev/null
+++ 
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2TokenCachingTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+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.assertNotNull;
+
+public class HttpOAuth2TokenCachingTest extends BaseHttpTest {
+
+    private static final String FAKE_TOKEN = "xxx.yyy.zzz";
+    private static final String clientId = "test-client";
+    private static final String clientSecret = "test-secret";
+    private static final OAuth2TokenRequestHandler handler = new 
OAuth2TokenRequestHandler(FAKE_TOKEN, clientId, clientSecret);
+
+    @Override
+    public void setupResources() throws Exception {
+    }
+
+    @Test
+    public void tokenIsCached() throws Exception {
+        try (var localServer = createLocalServer(); var localOAuth2Server = 
createLocalOAuth2Server()) {
+            String tokenEndpoint = "http://localhost:"; + 
localOAuth2Server.getLocalPort() + "/token";
+            String requestUrl = "http://localhost:"; + 
localServer.getLocalPort() + "/post?httpMethod=POST&oauth2ClientId="
+                                + clientId + "&oauth2ClientSecret=" + 
clientSecret + "&oauth2TokenEndpoint=" + tokenEndpoint +
+                                "&oauth2CacheTokens=" + true;
+
+            template.request(requestUrl,
+                    exchange1 -> {
+                    });
+            localOAuth2Server.close();
+            Exchange exchange
+                    = template.request(requestUrl,
+                            exchange1 -> {
+                            });
+            assertExchange(exchange);
+        }
+    }
+
+    @Test
+    public void tokenIsNotCachedWhenCacheTokensIsFalse() throws Exception {
+        try (var localServer = createLocalServer(); var localOAuth2Server = 
createLocalOAuth2Server()) {
+            String tokenEndpoint = "http://localhost:"; + 
localOAuth2Server.getLocalPort() + "/token";
+            String requestUrl = "http://localhost:"; + 
localServer.getLocalPort() + "/post?httpMethod=POST&oauth2ClientId="
+                                + clientId + "&oauth2ClientSecret=" + 
clientSecret + "&oauth2TokenEndpoint=" + tokenEndpoint +
+                                "&oauth2CacheTokens=" + false;
+
+            template.request(requestUrl,
+                    exchange1 -> {
+                    });
+            localOAuth2Server.close();
+            Exchange exchange
+                    = template.request(requestUrl,
+                            exchange1 -> {
+                            });
+            assertExceptionExchange(exchange);
+        }
+    }
+
+    @Test
+    public void toDTokenIsCached() 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:start")
+                            .setVariable("cid", constant(clientId))
+                            .setVariable("cs", constant(clientSecret))
+                            .toD("http://localhost:"; + 
localServer.getLocalPort()
+                                 + 
"/post?httpMethod=POST&oauth2ClientId=${variable.cid}"
+                                 + 
"&oauth2ClientSecret=${variable:cs}&oauth2TokenEndpoint=" + tokenEndpoint
+                                 + "&oauth2CacheTokens=" + true);
+                }
+            });
+
+            template.send("direct:start", e -> {
+            });
+            localOAuth2Server.close();
+            Exchange exchange = template.send("direct:start", e -> {
+            });
+
+            assertExchange(exchange);
+        }
+    }
+
+    @Test
+    public void toDTokenIsNotCachedWhenCacheTokensIsFalse() 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:start")
+                            .setVariable("cid", constant(clientId))
+                            .setVariable("cs", constant(clientSecret))
+                            .toD("http://localhost:"; + 
localServer.getLocalPort()
+                                 + 
"/post?httpMethod=POST&oauth2ClientId=${variable.cid}"
+                                 + 
"&oauth2ClientSecret=${variable:cs}&oauth2TokenEndpoint=" + tokenEndpoint
+                                 + "&oauth2CacheTokens=" + false);
+                }
+            });
+
+            template.send("direct:start", e -> {
+            });
+            localOAuth2Server.close();
+            Exchange exchange = template.send("direct:start", e -> {
+            });
+
+            assertExceptionExchange(exchange);
+        }
+    }
+
+    protected void assertExceptionExchange(Exchange exchange) {
+        assertNotNull(exchange);
+        assertNotNull(exchange.getException());
+        Exception exception = exchange.getException();
+        assertEquals(HttpHostConnectException.class, exception.getClass());
+    }
+
+    protected void assertHeaders(Map<String, Object> headers) {
+        assertEquals(HttpStatus.SC_OK, 
headers.get(Exchange.HTTP_RESPONSE_CODE));
+    }
+
+    protected String getExpectedContent() {
+        return "";
+    }
+
+    private HttpServer createLocalServer() throws Exception {
+        Map<String, String> expectedHeaders = new HashMap<>();
+        expectedHeaders.put("Authorization", "Bearer " + FAKE_TOKEN);
+
+        var localServer = ServerBootstrap.bootstrap()
+                
.setCanonicalHostName("localhost").setHttpProcessor(getBasicHttpProcessor())
+                
.setConnectionReuseStrategy(getConnectionReuseStrategy()).setResponseFactory(getHttpResponseFactory())
+                .setSslContext(getSSLContext())
+                .register("/post",
+                        new HeaderValidationHandler(
+                                "POST",
+                                null,
+                                null,
+                                null,
+                                expectedHeaders))
+                .create();
+
+        localServer.start();
+        return localServer;
+    }
+
+    private HttpServer createLocalOAuth2Server() throws Exception {
+        var localOAuth2Server = ServerBootstrap.bootstrap()
+                
.setCanonicalHostName("localhost").setHttpProcessor(getBasicHttpProcessor())
+                
.setConnectionReuseStrategy(getConnectionReuseStrategy()).setResponseFactory(getHttpResponseFactory())
+                .setSslContext(getSSLContext())
+                .register("/token", handler)
+                .create();
+
+        localOAuth2Server.start();
+        return localOAuth2Server;
+    }
+}
diff --git 
a/components/camel-http/src/test/java/org/apache/camel/component/http/handler/OAuth2TokenRequestHandler.java
 
b/components/camel-http/src/test/java/org/apache/camel/component/http/handler/OAuth2TokenRequestHandler.java
index aa2253cae0a..cb649223f79 100644
--- 
a/components/camel-http/src/test/java/org/apache/camel/component/http/handler/OAuth2TokenRequestHandler.java
+++ 
b/components/camel-http/src/test/java/org/apache/camel/component/http/handler/OAuth2TokenRequestHandler.java
@@ -49,7 +49,6 @@ public class OAuth2TokenRequestHandler implements 
HttpRequestHandler {
     @Override
     public void handle(ClassicHttpRequest request, ClassicHttpResponse 
response, HttpContext context)
             throws HttpException, IOException {
-
         String requestBody = EntityUtils.toString(request.getEntity());
         WWWFormCodec.parse(requestBody, StandardCharsets.UTF_8).stream()
                 .filter(pair -> pair.getName().equals("grant_type") && 
pair.getValue().equals("client_credentials"))
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 deccba69442..be465a3e345 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
@@ -596,6 +596,112 @@ public interface HttpEndpointBuilderFactory {
             doSetProperty("authUsername", authUsername);
             return this;
         }
+        /**
+         * Default expiration time for cached OAuth2 tokens, in seconds. Used 
if
+         * token response does not contain 'expires_in' field.
+         * 
+         * The option is a: <code>long</code> type.
+         * 
+         * Default: 3600
+         * Group: security
+         * 
+         * @param oauth2CachedTokensDefaultExpirySeconds the value to set
+         * @return the dsl builder
+         */
+        default HttpEndpointBuilder 
oauth2CachedTokensDefaultExpirySeconds(long 
oauth2CachedTokensDefaultExpirySeconds) {
+            doSetProperty("oauth2CachedTokensDefaultExpirySeconds", 
oauth2CachedTokensDefaultExpirySeconds);
+            return this;
+        }
+        /**
+         * Default expiration time for cached OAuth2 tokens, in seconds. Used 
if
+         * token response does not contain 'expires_in' field.
+         * 
+         * The option will be converted to a <code>long</code> type.
+         * 
+         * Default: 3600
+         * Group: security
+         * 
+         * @param oauth2CachedTokensDefaultExpirySeconds the value to set
+         * @return the dsl builder
+         */
+        default HttpEndpointBuilder 
oauth2CachedTokensDefaultExpirySeconds(String 
oauth2CachedTokensDefaultExpirySeconds) {
+            doSetProperty("oauth2CachedTokensDefaultExpirySeconds", 
oauth2CachedTokensDefaultExpirySeconds);
+            return this;
+        }
+        /**
+         * 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 over http, in seconds. Set this parameter to high value if you
+         * OAuth2 Token Endpoint answers slowly or you tokens expire quickly. 
If
+         * you set this parameter to too small value, you can get 4xx http
+         * errors because camel will think that the received token is still
+         * valid, while in reality the token is expired for the Authentication
+         * server.
+         * 
+         * The option is a: <code>long</code> type.
+         * 
+         * Default: 5
+         * Group: security
+         * 
+         * @param oauth2CachedTokensExpirationMarginSeconds the value to set
+         * @return the dsl builder
+         */
+        default HttpEndpointBuilder 
oauth2CachedTokensExpirationMarginSeconds(long 
oauth2CachedTokensExpirationMarginSeconds) {
+            doSetProperty("oauth2CachedTokensExpirationMarginSeconds", 
oauth2CachedTokensExpirationMarginSeconds);
+            return this;
+        }
+        /**
+         * 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 over http, in seconds. Set this parameter to high value if you
+         * OAuth2 Token Endpoint answers slowly or you tokens expire quickly. 
If
+         * you set this parameter to too small value, you can get 4xx http
+         * errors because camel will think that the received token is still
+         * valid, while in reality the token is expired for the Authentication
+         * server.
+         * 
+         * The option will be converted to a <code>long</code> type.
+         * 
+         * Default: 5
+         * Group: security
+         * 
+         * @param oauth2CachedTokensExpirationMarginSeconds the value to set
+         * @return the dsl builder
+         */
+        default HttpEndpointBuilder 
oauth2CachedTokensExpirationMarginSeconds(String 
oauth2CachedTokensExpirationMarginSeconds) {
+            doSetProperty("oauth2CachedTokensExpirationMarginSeconds", 
oauth2CachedTokensExpirationMarginSeconds);
+            return this;
+        }
+        /**
+         * Whether to cache OAuth2 client tokens.
+         * 
+         * The option is a: <code>boolean</code> type.
+         * 
+         * Default: false
+         * Group: security
+         * 
+         * @param oauth2CacheTokens the value to set
+         * @return the dsl builder
+         */
+        default HttpEndpointBuilder oauth2CacheTokens(boolean 
oauth2CacheTokens) {
+            doSetProperty("oauth2CacheTokens", oauth2CacheTokens);
+            return this;
+        }
+        /**
+         * Whether to cache OAuth2 client tokens.
+         * 
+         * The option will be converted to a <code>boolean</code> type.
+         * 
+         * Default: false
+         * Group: security
+         * 
+         * @param oauth2CacheTokens the value to set
+         * @return the dsl builder
+         */
+        default HttpEndpointBuilder oauth2CacheTokens(String 
oauth2CacheTokens) {
+            doSetProperty("oauth2CacheTokens", oauth2CacheTokens);
+            return this;
+        }
         /**
          * OAuth2 client id.
          * 

Reply via email to