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. *