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 95b4c615f81 CAMEL-20118: Support JWT Authentication for Camel Salesforce Maven Plugin (#17329) 95b4c615f81 is described below commit 95b4c615f817cc754c001495793d3a191bf30fd6 Author: Kai Grassnick <7880861+kaigrassn...@users.noreply.github.com> AuthorDate: Mon Mar 3 12:46:07 2025 +0100 CAMEL-20118: Support JWT Authentication for Camel Salesforce Maven Plugin (#17329) * CAMEL-20118: Support JWT Authentication for Camel Salesforce Maven Plugin * adjust MojoExecutionException to reflect property names --------- Co-authored-by: Kai Grassnick <kgrassn...@integrationmatters.com> --- .../codegen/AbstractSalesforceExecution.java | 32 ++++++++- .../camel-salesforce-maven-plugin/README.md | 77 ++++++++++++++++++-- .../apache/camel/maven/AbstractSalesforceMojo.java | 82 ++++++++++++++++++++++ .../camel/maven/AbstractSalesforceMojoTest.java | 54 ++++++++++++++ 4 files changed, 240 insertions(+), 5 deletions(-) diff --git a/components/camel-salesforce/camel-salesforce-codegen/src/main/java/org/apache/camel/component/salesforce/codegen/AbstractSalesforceExecution.java b/components/camel-salesforce/camel-salesforce-codegen/src/main/java/org/apache/camel/component/salesforce/codegen/AbstractSalesforceExecution.java index 015b0cb3a3e..d770ba9fa66 100644 --- a/components/camel-salesforce/camel-salesforce-codegen/src/main/java/org/apache/camel/component/salesforce/codegen/AbstractSalesforceExecution.java +++ b/components/camel-salesforce/camel-salesforce-codegen/src/main/java/org/apache/camel/component/salesforce/codegen/AbstractSalesforceExecution.java @@ -34,6 +34,7 @@ import org.apache.camel.component.salesforce.internal.client.PubSubApiClient; import org.apache.camel.component.salesforce.internal.client.RestClient; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.support.PropertyBindingSupport; +import org.apache.camel.support.jsse.KeyStoreParameters; import org.apache.camel.support.jsse.SSLContextParameters; import org.apache.camel.support.service.ServiceHelper; import org.apache.camel.util.StringHelper; @@ -130,6 +131,16 @@ public abstract class AbstractSalesforceExecution { */ String loginUrl; + /** + * Salesforce JWT Audience. + */ + String jwtAudience; + + /** + * Salesforce KeystoreParameters. + */ + KeyStoreParameters keyStoreParameters; + /** * Salesforce password. */ @@ -294,7 +305,7 @@ public abstract class AbstractSalesforceExecution { // set session before calling start() final SalesforceSession session = new SalesforceSession( new DefaultCamelContext(), httpClient, httpClient.getTimeout(), - new SalesforceLoginConfig(loginUrl, clientId, clientSecret, userName, password, false)); + getSalesforceLoginSession()); httpClient.setSession(session); try { @@ -306,6 +317,17 @@ public abstract class AbstractSalesforceExecution { return httpClient; } + private SalesforceLoginConfig getSalesforceLoginSession() { + if (keyStoreParameters != null) { + SalesforceLoginConfig salesforceLoginConfig + = new SalesforceLoginConfig(loginUrl, clientId, userName, keyStoreParameters, false); + salesforceLoginConfig.setJwtAudience(jwtAudience); + + return salesforceLoginConfig; + } + return new SalesforceLoginConfig(loginUrl, clientId, clientSecret, userName, password, false); + } + private void disconnectFromSalesforce(final RestClient restClient) { if (restClient == null) { return; @@ -383,6 +405,14 @@ public abstract class AbstractSalesforceExecution { this.password = password; } + public void setJwtAudience(String jwtAudience) { + this.jwtAudience = jwtAudience; + } + + public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters) { + this.keyStoreParameters = keyStoreParameters; + } + public void setUserName(String userName) { this.userName = userName; } diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/README.md b/components/camel-salesforce/camel-salesforce-maven-plugin/README.md index 30de0b5c3e4..d06a2c1ea8a 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/README.md +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/README.md @@ -14,8 +14,12 @@ The plugin configuration has the following properties. * clientId - Salesforce client Id for Remote API access * clientSecret - Salesforce client secret for Remote API access -* userName - Salesforce account user name +* userName - Salesforce account username * password - Salesforce account password (including secret token) +* jwtAudience - Salesforce JWT audience (defaults to "https://login.salesforce.com") +* keystoreResource - Path to keystore file for JWT authentication +* keystorePassword - Password for keystore file +* keystoreType - Type of keystore file (defaults to "JKS") * loginUrl - Salesforce loginUrl (defaults to "https://login.salesforce.com") * version - Salesforce Rest API version, defaults to 25.0 * outputDirectory - Directory where to place generated DTOs, defaults to ${project.build.directory}/generated-sources/camel-salesforce @@ -41,7 +45,7 @@ Property name format: `SObject.FieldName.PicklistValue`. Property value is the d </enumerationOverrideProperties> ``` -Additonal properties to provide proxy information, if behind a firewall. +Additional properties to provide proxy information, if behind a firewall. * httpProxyHost * httpProxyPort @@ -53,7 +57,10 @@ Additonal properties to provide proxy information, if behind a firewall. * httpProxyIncludedAddresses * httpProxyExcludedAddresses -Sample pom.xml +There are two authentication methods supported by the plugin: Username-Password and JWT. +The plugin will use the Username-Password method if the `clientSecret` is specified and will use the JWT method if the `keystoreResource` is specified. + +Sample pom.xml using Username-Password authentication: ``` <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" @@ -104,13 +111,75 @@ Sample pom.xml </build> </project> - ``` For obvious security reasons it is recommended that the clientId, clientSecret, userName and password fields be not set in the pom.xml. The plugin should be configured for the rest of the properties, and can be executed using the following command: mvn camel-salesforce:generate -DcamelSalesforce.clientId=<clientid> -DcamelSalesforce.clientSecret=<clientsecret> -DcamelSalesforce.userName=<username> -DcamelSalesforce.password=<password> +Sample pom.xml using JWT authentication: +``` +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <properties> + + <camelSalesforce.clientId>5MVG9uudbyLbNPZOFutIHJpIb2nchnCiNE_NqeYcewMCPPT8_6VV_LQF_CJ813456GxzhxZdxlGwbYI_yzHmz</camelSalesforce.clientId> + <camelSalesforce.userName>f...@bar.com</camelSalesforce.userName> + <camelSalesforce.keystore.resource>src/main/resources/salesforce.jks</camelSalesforce.keystore.resource> + <camelSalesforce.keystore.password>foopasswordCbe5V27JxD0JXYFGJIdIEWB7p</camelSalesforce.keystore.password> + <camelSalesforce.keystore.type>JKS</camelSalesforce.keystore.type> + + <camelSalesforce.jwtAudience>https://login.salesforce.com</camelSalesforce.jwtAudience> + + <camelSalesforce.loginUrl>https://myDomain.my.salesforce.com</camelSalesforce.loginUrl> + + <camelSalesforce.httpProxyHost>foo.bar.com</camelSalesforce.httpProxyHost> + <camelSalesforce.httpProxyPort>8090</camelSalesforce.httpProxyPort> + + </properties> + + ... + + <build> + ... + <plugins> + ... + + <!-- camel maven saleforce for creating salesforce objects --> + <plugin> + <groupId>org.apache.camel.maven</groupId> + <artifactId>camel-salesforce-maven-plugin</artifactId> + <version>2.17.1</version> + <configuration> + <clientId>${camelSalesforce.clientId}</clientId> + <userName>${camelSalesforce.userName}</userName> + <keystoreResource>${camelSalesforce.keystore.resource}</keystoreResource> + <keystorePassword>${camelSalesforce.keystore.password}</keystorePassword> + <keystoreType default-value="JKS">${camelSalesforce.keystore.type}</keystoreType> + <jwtAudience default-value="https://login.salesforce.com">${camelSalesforce.jwtAudience}</jwtAudience> + <loginUrl default-value="https://login.salesforce.com">${camelSalesforce.loginUrl}</loginUrl> + <includes> + <include>Account</include> + <include>Contacts</include> + </includes> + <httpProxyHost>${camelSalesforce.httpProxyHost}</httpProxyHost> + <httpProxyPort>${camelSalesforce.httpProxyPort}</httpProxyPort> + </configuration> + </plugin> + + </plugins> + </build> + +</project> +``` +For obvious security reasons it is recommended that the clientId, userName, keystoreResource, keystorePassword, keystoreType and jwtAudience fields be not set in the pom.xml. +The plugin should be configured for the rest of the properties, and can be executed using the following command: + + mvn camel-salesforce:generate -DcamelSalesforce.clientId=<clientid> -DcamelSalesforce.userName=<username> -DcamelSalesforce.keystore.resource=<keystoreResource> -DcamelSalesforce.keystore.password=<keystorePassword> -DcamelSalesforce.keystore.type=<keystoreType> -DcamelSalesforce.jwtAudience=<jwtAudience> + + The generated DTOs use Jackson. All Salesforce field types are supported. Date and time fields are mapped to java.time.ZonedDateTime, and picklist fields are mapped to generated Java Enumerations. Relationship fields, e.g. `Contact.Account`, will be strongly typed if the referenced SObject type is listed in `includes`. Otherwise, the type of the reference object will be `AbstractDescribedSObjectBase`. Some useful but non-obvious SObjects to include are `RecordType`, `User`, `Group`, and `Name`. diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/main/java/org/apache/camel/maven/AbstractSalesforceMojo.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/main/java/org/apache/camel/maven/AbstractSalesforceMojo.java index 7c9b8d9fabd..b94b06154c2 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/src/main/java/org/apache/camel/maven/AbstractSalesforceMojo.java +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/main/java/org/apache/camel/maven/AbstractSalesforceMojo.java @@ -16,12 +16,18 @@ */ package org.apache.camel.maven; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; import java.util.Map; import java.util.Set; import org.apache.camel.component.salesforce.SalesforceEndpointConfig; import org.apache.camel.component.salesforce.SalesforceLoginConfig; import org.apache.camel.component.salesforce.codegen.AbstractSalesforceExecution; +import org.apache.camel.support.jsse.KeyStoreParameters; import org.apache.camel.support.jsse.SSLContextParameters; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; @@ -141,6 +147,30 @@ public abstract class AbstractSalesforceMojo extends AbstractMojo { @Parameter(property = "camelSalesforce.userName", required = true) String userName; + /** + * Salesforce JWT Audience. + */ + @Parameter(property = "camelSalesforce.jwtAudience", defaultValue = "https://login.salesforce.com") + String jwtAudience; + + /** + * Salesforce Keystore Path. + */ + @Parameter(property = "camelSalesforce.keystore.resource") + String keystoreResource; + + /** + * Salesforce Keystore Password. + */ + @Parameter(property = "camelSalesforce.keystore.password") + String keystorePassword; + + /** + * Salesforce Keystore Type. + */ + @Parameter(property = "camelSalesforce.keystore.type", defaultValue = "jks") + String keystoreType; + /** * Salesforce API version. */ @@ -152,6 +182,7 @@ public abstract class AbstractSalesforceMojo extends AbstractMojo { @Override public final void execute() throws MojoExecutionException, MojoFailureException { try { + validateAuthenticationParameters(); setup(); execution.execute(); } catch (Exception e) { @@ -184,5 +215,56 @@ public abstract class AbstractSalesforceMojo extends AbstractMojo { execution.setPassword(password); execution.setVersion(version); execution.setSslContextParameters(sslContextParameters); + execution.setJwtAudience(jwtAudience); + execution.setKeyStoreParameters(generateKeyStoreParameters()); + } + + private void validateAuthenticationParameters() throws MojoExecutionException { + if (clientSecret == null && keystoreResource == null) { + throw new MojoExecutionException( + "Either property: clientSecret or property: keystoreResource must be provided."); + } else if (clientSecret != null && keystoreResource != null) { + throw new MojoExecutionException( + "Property: clientSecret or property: keystoreResource must be provided."); + } + + if (clientSecret != null) { + if (password == null) { + throw new MojoExecutionException( + generateRequiredErrorMessage("password", "clientSecret")); + } + } + + if (keystoreResource != null) { + if (keystorePassword == null) { + throw new MojoExecutionException( + generateRequiredErrorMessage("keystorePassword", "keystoreResource")); + } + } + } + + private String generateRequiredErrorMessage(String parameter1, String parameter2) { + return String.format("Property: %s must be provided when property: %s was provided.", parameter1, parameter2); + } + + private KeyStoreParameters generateKeyStoreParameters() { + if (keystoreResource == null) { + return null; + } + + KeyStoreParameters keyStoreParameters = new KeyStoreParameters(); + keyStoreParameters.setResource(keystoreResource); + keyStoreParameters.setPassword(keystorePassword); + keyStoreParameters.setType(keystoreType); + + try (InputStream is = new FileInputStream(keystoreResource)) { + KeyStore ks = KeyStore.getInstance(keystoreType); + ks.load(is, keystorePassword.toCharArray()); + keyStoreParameters.setKeyStore(ks); + } catch (IOException | GeneralSecurityException e) { + throw new RuntimeException(e); + } + + return keyStoreParameters; } } diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/AbstractSalesforceMojoTest.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/AbstractSalesforceMojoTest.java index f65ae5433df..f0f177af315 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/AbstractSalesforceMojoTest.java +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/AbstractSalesforceMojoTest.java @@ -71,6 +71,36 @@ public abstract class AbstractSalesforceMojoTest { mojo.execute(); } + @Test + public void shouldLoginWithJwtAndProvideRestClient() throws IOException, MojoExecutionException, MojoFailureException { + final AbstractSalesforceMojo mojo = new AbstractSalesforceMojo() { + final Logger logger = LoggerFactory.getLogger(AbstractSalesforceExecution.class.getName()); + + @Override + protected AbstractSalesforceExecution getSalesforceExecution() { + return new AbstractSalesforceExecution() { + @Override + protected void executeWithClient() { + assertThat(getRestClient()).isNotNull(); + + getRestClient().getGlobalObjects(NO_HEADERS, (response, headers, exception) -> { + assertThat(exception).isNull(); + }); + } + + @Override + protected Logger getLog() { + return logger; + } + }; + } + }; + + setupJwt(mojo); + + mojo.execute(); + } + static void setup(final AbstractSalesforceMojo mojo) throws IOException { // load test-salesforce-login properties try (final InputStream stream = new FileInputStream(TEST_LOGIN_PROPERTIES)) { @@ -93,4 +123,28 @@ public abstract class AbstractSalesforceMojoTest { throw exception; } } + + static void setupJwt(final AbstractSalesforceMojo mojo) throws IOException { + // load test-salesforce-login properties + try (final InputStream stream = new FileInputStream(TEST_LOGIN_PROPERTIES)) { + final Properties properties = new Properties(); + properties.load(stream); + mojo.clientId = properties.getProperty("salesforce.client.id"); + mojo.userName = properties.getProperty("salesforce.username"); + mojo.loginUrl = properties.getProperty("salesforce.login.url"); + mojo.keystoreResource = properties.getProperty("salesforce.keystore.resource"); + mojo.keystorePassword = properties.getProperty("salesforce.keystore.password"); + mojo.keystoreType = properties.getProperty("salesforce.keystore.type"); + mojo.version = SalesforceEndpointConfig.DEFAULT_VERSION; + } catch (final FileNotFoundException e) { + final FileNotFoundException exception + = new FileNotFoundException( + "Create a properties file named " + TEST_LOGIN_PROPERTIES + + " with clientId, userName, keyStoreResource, keyStorePassword, keyStoreType" + + " for a Salesforce account with Merchandise and Invoice objects from Salesforce Guides."); + exception.initCause(e); + + throw exception; + } + } }