From 50172ecd64012b6e2493263ba1b75763c21943db Mon Sep 17 00:00:00 2001 From: Nidhi Date: Thu, 21 Jul 2022 16:10:36 +0530 Subject: [PATCH] fix: Added support for self signed certificate during OAuth2 flows (#14719) * Added support for self signed certificate during OAuth2 flows, server side changes * fix: authentication.useSelfSignedCert key added * Merging from release * Fixed issue with dependencies, plus bug with using ssl * Cypress test attempt 1 * Cypress test attempt 2 * Cypress test attempt 3 * Clean up * Review comments * Enabled compression again Co-authored-by: Aman Agarwal --- .../Datasources/RestApiDatasource_spec.js | 1 + .../cypress/locators/DatasourcesEditor.json | 4 ++ app/client/cypress/support/commands.js | 18 ++++++++- .../formControls/CheckboxControl.tsx | 1 + .../src/entities/Datasource/RestAPIForm.ts | 1 + .../RestAPIDatasourceForm.tsx | 39 ++++++++++++++++++- .../RestAPIDatasourceFormTransformer.ts | 1 + app/server/appsmith-interfaces/pom.xml | 5 +++ .../appsmith/external/helpers/SSLHelper.java | 27 +++++++++++++ .../com/appsmith/external/models/OAuth2.java | 2 + .../external/connections/APIConnection.java | 15 +++++++ .../connections/APIConnectionFactory.java | 28 ++++++------- .../connections/OAuth2AuthorizationCode.java | 17 ++++++-- .../connections/OAuth2ClientCredentials.java | 18 ++++++--- .../com/external/plugins/RestApiPlugin.java | 29 ++------------ .../OAuth2ClientCredentialsTest.java | 17 ++++++-- .../ce/AuthenticationServiceCEImpl.java | 13 ++++++- .../ExamplesWorkspaceClonerTests.java | 4 +- 18 files changed, 183 insertions(+), 57 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/RestApiDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/RestApiDatasource_spec.js index 5f80129525..7cfa392c25 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/RestApiDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/RestApiDatasource_spec.js @@ -17,6 +17,7 @@ describe("Create a rest datasource", function() { .trigger("click") .wait(1000); agHelper.ValidateToastMessage("datasource created"); //verifying there is no error toast, Bug 14566 + cy.testSelfSignedCertificateSettingsInREST(false); cy.saveDatasource(); cy.contains(".datasource-highlight", "https://mock-api.appsmith.com"); cy.SaveAndRunAPI(); diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json index 74ea16cf4d..69bcd20d5a 100644 --- a/app/client/cypress/locators/DatasourcesEditor.json +++ b/app/client/cypress/locators/DatasourcesEditor.json @@ -65,5 +65,9 @@ "mongoUriDropdown": "//p[text()='Use Mongo Connection String URI']/following-sibling::div", "mongoUriYes": "//div[text()='Yes']", "mongoUriInput":"//p[text()='Connection String URI']/following-sibling::div//input", + "advancedSettings": "[data-cy='section-Advanced Settings']", + "useSelfSignedCert": ".t--connection\\.ssl\\.authType", + "useCertInAuth": "[data-cy='authentication.useSelfSignedCert'] input", + "certificateDetails": "[data-cy='section-Certificate Details']", "saveBtn": ".t--save-datasource" } diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index d2cdc5d0e6..76196117d8 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -15,7 +15,6 @@ const loginPage = require("../locators/LoginPage.json"); const signupPage = require("../locators/SignupPage.json"); import homePage from "../locators/HomePage"; const pages = require("../locators/Pages.json"); -const datasourceEditor = require("../locators/DatasourcesEditor.json"); const datasourceFormData = require("../fixtures/datasources.json"); const commonlocators = require("../locators/commonlocators.json"); const queryEditor = require("../locators/QueryEditor.json"); @@ -144,6 +143,7 @@ Cypress.Commands.add( ); expect(firstTxt).to.equal(expectedvalue); }); + cy.testSelfSignedCertificateSettingsInREST(true); }, ); @@ -161,9 +161,25 @@ Cypress.Commands.add( cy.xpath("//span[text()='Send client credentials in body']").should( "be.visible", ); + cy.testSelfSignedCertificateSettingsInREST(true); }, ); +Cypress.Commands.add("testSelfSignedCertificateSettingsInREST", (isOAuth2) => { + cy.get(datasource.advancedSettings).click(); + cy.get(datasource.useCertInAuth).should("not.exist"); + cy.get(datasource.certificateDetails).should("not.exist"); + cy.TargetDropdownAndSelectOption(datasource.useSelfSignedCert, "Yes"); + if (isOAuth2) { + cy.get(datasource.useCertInAuth).should("exist"); + } else { + cy.get(datasource.useCertInAuth).should("not.exist"); + } + cy.get(datasource.certificateDetails).should("exist"); + cy.TargetDropdownAndSelectOption(datasource.useSelfSignedCert, "No"); + cy.get(datasource.advancedSettings).click(); +}); + Cypress.Commands.add("addBasicProfileDetails", (username, password) => { cy.get(datasource.authType).click(); cy.xpath(datasource.basic).click(); diff --git a/app/client/src/components/formControls/CheckboxControl.tsx b/app/client/src/components/formControls/CheckboxControl.tsx index 7216afac20..45396f0b6b 100644 --- a/app/client/src/components/formControls/CheckboxControl.tsx +++ b/app/client/src/components/formControls/CheckboxControl.tsx @@ -39,6 +39,7 @@ function renderComponent(props: renderComponentProps) { return ( { - const { authentication, authType } = this.props.formData; + const { authentication, authType, connection } = this.props.formData; const isGrantTypeAuthorizationCode = _.get(authentication, "grantType") === GrantType.AuthorizationCode; const isAuthenticationTypeOAuth2 = authType === AuthType.OAuth2; + const isConnectSelfSigned = + _.get(connection, "ssl.authType") === SSLType.SELF_SIGNED_CERTIFICATE; + return ( <> {isAuthenticationTypeOAuth2 && isGrantTypeAuthorizationCode && ( @@ -919,6 +922,16 @@ class DatasourceRestAPIEditor extends React.Component< "DEFAULT", )} + {isAuthenticationTypeOAuth2 && isConnectSelfSigned && ( + + {this.renderCheckboxViaFormControl( + "authentication.useSelfSignedCert", + "Use Self-Signed Certificate for Authorization requests", + "", + false, + )} + + )} ); }; @@ -1127,6 +1140,30 @@ class DatasourceRestAPIEditor extends React.Component< /> ); } + + renderCheckboxViaFormControl( + configProperty: string, + label: string, + placeholderText: string, + isRequired: boolean, + ) { + return ( + + ); + } } const mapStateToProps = (state: AppState, props: any) => { diff --git a/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts b/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts index 7d3b2d1e97..b7987a5ec0 100644 --- a/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts +++ b/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts @@ -104,6 +104,7 @@ const formToDatasourceAuthentication = ( sendScopeWithRefreshToken: authentication.sendScopeWithRefreshToken, refreshTokenClientCredentialsLocation: authentication.refreshTokenClientCredentialsLocation, + useSelfSignedCert: authentication.useSelfSignedCert, }; if (isClientCredentials(authType, authentication)) { return { diff --git a/app/server/appsmith-interfaces/pom.xml b/app/server/appsmith-interfaces/pom.xml index 91b03ffa89..82be696650 100644 --- a/app/server/appsmith-interfaces/pom.xml +++ b/app/server/appsmith-interfaces/pom.xml @@ -129,6 +129,11 @@ org.springframework spring-webflux + + io.projectreactor.netty + reactor-netty-core + provided + diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java index 586926aa24..b01116c96d 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java @@ -1,6 +1,10 @@ package com.appsmith.external.helpers; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.SSLDetails; import com.appsmith.external.models.UploadedFile; +import reactor.netty.tcp.DefaultSslContextSpec; +import reactor.netty.tcp.SslProvider; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; @@ -14,6 +18,7 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.function.Consumer; public class SSLHelper { @@ -50,4 +55,26 @@ public class SSLHelper { return trustManagerFactory; } + + public static Consumer sslCheckForHttpClient(DatasourceConfiguration datasourceConfiguration) { + + return (sslContextSpec) -> { + final DefaultSslContextSpec sslContextSpec1 = DefaultSslContextSpec.forClient(); + + if (datasourceConfiguration.getConnection() != null && + datasourceConfiguration.getConnection().getSsl() != null && + datasourceConfiguration.getConnection().getSsl().getAuthType() == SSLDetails.AuthType.SELF_SIGNED_CERTIFICATE) { + + sslContextSpec1.configure(sslContextBuilder -> { + try { + final UploadedFile certificateFile = datasourceConfiguration.getConnection().getSsl().getCertificateFile(); + sslContextBuilder.trustManager(SSLHelper.getSslTrustManagerFactory(certificateFile)); + } catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException e) { + e.printStackTrace(); + } + }); + } + sslContextSpec.sslContext(sslContextSpec1); + }; + } } diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java index 505581fdda..6b285a4f50 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java @@ -76,6 +76,8 @@ public class OAuth2 extends AuthenticationDTO { String resource; + boolean useSelfSignedCert = false; + public String getScopeString() { if (scopeString != null && !scopeString.isBlank()) { return scopeString; diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnection.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnection.java index b04e6c1bd5..fb931f688e 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnection.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnection.java @@ -1,7 +1,22 @@ package com.external.connections; +import com.appsmith.external.helpers.SSLHelper; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.OAuth2; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import reactor.netty.http.client.HttpClient; // Parent type for all API connections that need to be created during datasource create method. public abstract class APIConnection implements ExchangeFilterFunction { + + HttpClient getSecuredHttpClient(DatasourceConfiguration datasourceConfiguration) { + final OAuth2 oAuth2 = (OAuth2) datasourceConfiguration.getAuthentication(); + HttpClient httpClient = HttpClient.create(); + + if (oAuth2.isUseSelfSignedCert()) { + httpClient = httpClient.secure(SSLHelper.sslCheckForHttpClient(datasourceConfiguration)); + } + + return httpClient; + } } \ No newline at end of file diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java index eaca2e5bc1..ca209e5732 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java @@ -4,6 +4,7 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.BasicAuth; +import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.OAuth2; import com.appsmith.external.models.ApiKeyAuth; import com.appsmith.external.models.BearerTokenAuth; @@ -12,25 +13,26 @@ import reactor.core.publisher.Mono; public class APIConnectionFactory { - public static Mono createConnection(AuthenticationDTO authenticationType) { - if (authenticationType instanceof OAuth2) { - if (OAuth2.Type.CLIENT_CREDENTIALS.equals(((OAuth2) authenticationType).getGrantType())) { - return Mono.from(OAuth2ClientCredentials.create((OAuth2) authenticationType)); - } else if (OAuth2.Type.AUTHORIZATION_CODE.equals(((OAuth2) authenticationType).getGrantType())) { - if (!Boolean.TRUE.equals(authenticationType.getIsAuthorized())) { + public static Mono createConnection(DatasourceConfiguration datasourceConfiguration) { + final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + if (authentication instanceof OAuth2) { + if (OAuth2.Type.CLIENT_CREDENTIALS.equals(((OAuth2) authentication).getGrantType())) { + return Mono.from(OAuth2ClientCredentials.create(datasourceConfiguration)); + } else if (OAuth2.Type.AUTHORIZATION_CODE.equals(((OAuth2) authentication).getGrantType())) { + if (!Boolean.TRUE.equals(authentication.getIsAuthorized())) { return Mono.error(new AppsmithPluginException( AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "Please authorize datasource")); } - return Mono.from(OAuth2AuthorizationCode.create((OAuth2) authenticationType)); + return Mono.from(OAuth2AuthorizationCode.create(datasourceConfiguration)); } else { return Mono.empty(); } - } else if (authenticationType instanceof BasicAuth) { - return Mono.from(BasicAuthentication.create((BasicAuth) authenticationType)); - } else if (authenticationType instanceof ApiKeyAuth) { - return Mono.from(ApiKeyAuthentication.create((ApiKeyAuth) authenticationType)); - } else if (authenticationType instanceof BearerTokenAuth) { - return Mono.from(BearerTokenAuthentication.create((BearerTokenAuth) authenticationType)); + } else if (authentication instanceof BasicAuth) { + return Mono.from(BasicAuthentication.create((BasicAuth) authentication)); + } else if (authentication instanceof ApiKeyAuth) { + return Mono.from(ApiKeyAuthentication.create((ApiKeyAuth) authentication)); + } else if (authentication instanceof BearerTokenAuth) { + return Mono.from(BearerTokenAuthentication.create((BearerTokenAuth) authentication)); } else { return Mono.empty(); } diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java index 4c2de06b2e..434367a361 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java @@ -4,6 +4,7 @@ import com.appsmith.external.constants.Authentication; import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.AuthenticationResponse; +import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.OAuth2; import com.appsmith.external.models.UpdatableConnection; import lombok.AccessLevel; @@ -14,6 +15,7 @@ import org.bson.internal.Base64; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.BodyExtractors; @@ -25,6 +27,7 @@ import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; import java.net.URI; import java.time.Clock; @@ -69,15 +72,16 @@ public class OAuth2AuthorizationCode extends APIConnection implements UpdatableC return true; } - public static Mono create(OAuth2 oAuth2) { - if (oAuth2 == null) { + public static Mono create(DatasourceConfiguration datasourceConfiguration) { + if (datasourceConfiguration == null || datasourceConfiguration.getAuthentication() == null) { return Mono.empty(); } + final OAuth2 oAuth2 = (OAuth2) datasourceConfiguration.getAuthentication(); // Create OAuth2Connection OAuth2AuthorizationCode connection = new OAuth2AuthorizationCode(); if (!isAuthenticationResponseValid(oAuth2)) { - return connection.generateOAuth2Token(oAuth2) + return connection.generateOAuth2Token(datasourceConfiguration) .flatMap(token -> { updateConnection(connection, token); return Mono.just(connection); @@ -100,8 +104,13 @@ public class OAuth2AuthorizationCode extends APIConnection implements UpdatableC return now.isAfter(expiresAt.minus(Duration.ofMinutes(1))); } - private Mono generateOAuth2Token(OAuth2 oAuth2) { + private Mono generateOAuth2Token(DatasourceConfiguration datasourceConfiguration) { + final OAuth2 oAuth2 = (OAuth2) datasourceConfiguration.getAuthentication(); + final HttpClient securedHttpClient = this.getSecuredHttpClient(datasourceConfiguration); + + // Webclient WebClient.Builder webClientBuilder = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(securedHttpClient)) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .exchangeStrategies(ExchangeStrategies .builder() diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java index 3d4428863f..710c1bb7c4 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java @@ -4,6 +4,7 @@ import com.appsmith.external.constants.Authentication; import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.AuthenticationResponse; +import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.OAuth2; import com.appsmith.external.models.UpdatableConnection; import lombok.AccessLevel; @@ -14,6 +15,7 @@ import org.bson.internal.Base64; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.BodyExtractors; @@ -25,6 +27,7 @@ import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; import java.net.URI; import java.time.Clock; @@ -45,10 +48,11 @@ public class OAuth2ClientCredentials extends APIConnection implements UpdatableC private Object tokenResponse; private static final int MAX_IN_MEMORY_SIZE = 10 * 1024 * 1024; // 10 MB - public static Mono create(OAuth2 oAuth2) { - if (oAuth2 == null) { + public static Mono create(DatasourceConfiguration datasourceConfiguration) { + if (datasourceConfiguration == null) { return Mono.empty(); } + final OAuth2 oAuth2 = (OAuth2) datasourceConfiguration.getAuthentication(); // Create OAuth2Connection OAuth2ClientCredentials connection = new OAuth2ClientCredentials(); @@ -65,7 +69,7 @@ public class OAuth2ClientCredentials extends APIConnection implements UpdatableC return now.isBefore(expiresAt.minus(Duration.ofMinutes(1))); }) // If invalid, regenerate token - .switchIfEmpty(connection.generateOAuth2Token(oAuth2)) + .switchIfEmpty(connection.generateOAuth2Token(datasourceConfiguration)) // Store valid token .flatMap(token -> { connection.setToken(token.getAuthenticationResponse().getToken()); @@ -77,9 +81,13 @@ public class OAuth2ClientCredentials extends APIConnection implements UpdatableC }); } - private Mono generateOAuth2Token(OAuth2 oAuth2) { + private Mono generateOAuth2Token(DatasourceConfiguration datasourceConfiguration) { + final OAuth2 oAuth2 = (OAuth2) datasourceConfiguration.getAuthentication(); + final HttpClient securedHttpClient = this.getSecuredHttpClient(datasourceConfiguration); + // Webclient - WebClient.Builder webClientBuilder = WebClient.builder() + final WebClient.Builder webClientBuilder = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(securedHttpClient)) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .exchangeStrategies(ExchangeStrategies .builder() diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index 250be8ed12..c19b1d801a 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -16,8 +16,6 @@ import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.PaginationField; import com.appsmith.external.models.PaginationType; import com.appsmith.external.models.Property; -import com.appsmith.external.models.SSLDetails; -import com.appsmith.external.models.UploadedFile; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.external.plugins.SmartSubstitutionInterface; @@ -56,7 +54,6 @@ import reactor.core.Exceptions; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; -import reactor.netty.tcp.DefaultSslContextSpec; import reactor.util.function.Tuple2; import javax.crypto.SecretKey; @@ -68,9 +65,6 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -302,25 +296,8 @@ public class RestApiPlugin extends BasePlugin { .build(); HttpClient httpClient = HttpClient.create(provider) - .secure(sslContextSpec -> { - - final DefaultSslContextSpec sslContextSpec1 = DefaultSslContextSpec.forClient(); - - if (datasourceConfiguration.getConnection() != null && - datasourceConfiguration.getConnection().getSsl() != null && - datasourceConfiguration.getConnection().getSsl().getAuthType() == SSLDetails.AuthType.SELF_SIGNED_CERTIFICATE) { - - sslContextSpec1.configure(sslContextBuilder -> { - try { - final UploadedFile certificateFile = datasourceConfiguration.getConnection().getSsl().getCertificateFile(); - sslContextBuilder.trustManager(SSLHelper.getSslTrustManagerFactory(certificateFile)); - } catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException e) { - e.printStackTrace(); - } - }); - } - sslContextSpec.sslContext(sslContextSpec1); - }).compress(true); + .secure(SSLHelper.sslCheckForHttpClient(datasourceConfiguration)) + .compress(true); if ("true".equals(System.getProperty("java.net.useSystemProxies")) && (!System.getProperty("http.proxyHost", "").isEmpty() || !System.getProperty("https.proxyHost", "").isEmpty())) { @@ -620,7 +597,7 @@ public class RestApiPlugin extends BasePlugin { @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { - return APIConnectionFactory.createConnection(datasourceConfiguration.getAuthentication()); + return APIConnectionFactory.createConnection(datasourceConfiguration); } @Override diff --git a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/OAuth2ClientCredentialsTest.java b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/OAuth2ClientCredentialsTest.java index a91a9d9584..2d7926878d 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/OAuth2ClientCredentialsTest.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/OAuth2ClientCredentialsTest.java @@ -2,6 +2,7 @@ package com.external.connections; import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.models.AuthenticationResponse; +import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.OAuth2; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -56,13 +57,15 @@ public class OAuth2ClientCredentialsTest { @Test public void testValidConnection() { + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); OAuth2 oAuth2 = new OAuth2(); + datasourceConfiguration.setAuthentication(oAuth2); AuthenticationResponse authenticationResponse = new AuthenticationResponse(); oAuth2.setIsTokenHeader(true); authenticationResponse.setToken("SomeToken"); authenticationResponse.setExpiresAt(Instant.now().plusSeconds(1200)); oAuth2.setAuthenticationResponse(authenticationResponse); - OAuth2ClientCredentials connection = OAuth2ClientCredentials.create(oAuth2).block(Duration.ofMillis(100)); + OAuth2ClientCredentials connection = OAuth2ClientCredentials.create(datasourceConfiguration).block(Duration.ofMillis(100)); assertThat(connection).isNotNull(); assertThat(connection.getExpiresAt()).isEqualTo(authenticationResponse.getExpiresAt()); assertThat(connection.getToken()).isEqualTo("SomeToken"); @@ -70,13 +73,15 @@ public class OAuth2ClientCredentialsTest { @Test public void testStaleFilter() { + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); OAuth2 oAuth2 = new OAuth2(); + datasourceConfiguration.setAuthentication(oAuth2); AuthenticationResponse authenticationResponse = new AuthenticationResponse(); oAuth2.setIsTokenHeader(true); authenticationResponse.setToken("SomeToken"); authenticationResponse.setExpiresAt(Instant.now().plusSeconds(1200)); oAuth2.setAuthenticationResponse(authenticationResponse); - OAuth2ClientCredentials connection = OAuth2ClientCredentials.create(oAuth2).block(Duration.ofMillis(100)); + OAuth2ClientCredentials connection = OAuth2ClientCredentials.create(datasourceConfiguration).block(Duration.ofMillis(100)); connection.setExpiresAt(Instant.now()); Mono response = connection.filter(Mockito.mock(ClientRequest.class), Mockito.mock(ExchangeFunction.class)); @@ -89,7 +94,9 @@ public class OAuth2ClientCredentialsTest { public void testCreate_withIsAuthorizationHeaderTrue_sendsCredentialsInHeader() throws InterruptedException { String baseUrl = String.format("http://localhost:%s", mockEndpoint.getPort()); + final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); OAuth2 oAuth2 = new OAuth2(); + datasourceConfiguration.setAuthentication(oAuth2); oAuth2.setIsAuthorizationHeader(true); oAuth2.setGrantType(OAuth2.Type.CLIENT_CREDENTIALS); oAuth2.setAccessTokenUrl(baseUrl); @@ -101,7 +108,7 @@ public class OAuth2ClientCredentialsTest { .setBody("{}") .addHeader("Content-Type", "application/json")); - final OAuth2ClientCredentials response = OAuth2ClientCredentials.create(oAuth2).block(); + final OAuth2ClientCredentials response = OAuth2ClientCredentials.create(datasourceConfiguration).block(); final RecordedRequest recordedRequest = mockEndpoint.takeRequest(30, TimeUnit.SECONDS); final String authorizationHeader = recordedRequest.getHeader("Authorization"); @@ -113,7 +120,9 @@ public class OAuth2ClientCredentialsTest { public void testCreate_withIsAuthorizationHeaderFalse_sendsCredentialsInBody() throws InterruptedException, EOFException { String baseUrl = String.format("http://localhost:%s", mockEndpoint.getPort()); + final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); OAuth2 oAuth2 = new OAuth2(); + datasourceConfiguration.setAuthentication(oAuth2); oAuth2.setGrantType(OAuth2.Type.CLIENT_CREDENTIALS); oAuth2.setAccessTokenUrl(baseUrl); oAuth2.setClientId("testId"); @@ -124,7 +133,7 @@ public class OAuth2ClientCredentialsTest { .setBody("{}") .addHeader("Content-Type", "application/json")); - final OAuth2ClientCredentials response = OAuth2ClientCredentials.create(oAuth2).block(); + final OAuth2ClientCredentials response = OAuth2ClientCredentials.create(datasourceConfiguration).block(); final RecordedRequest recordedRequest = mockEndpoint.takeRequest(30, TimeUnit.SECONDS); final String authorizationHeader = recordedRequest.getHeader("Authorization"); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCEImpl.java index c69441dbe6..bc4a6e9540 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCEImpl.java @@ -3,6 +3,7 @@ package com.appsmith.server.solutions.ce; import com.appsmith.external.constants.Authentication; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.helpers.SSLHelper; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.AuthenticationResponse; import com.appsmith.external.models.Datasource; @@ -30,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; import org.bson.internal.Base64; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -38,6 +40,7 @@ import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; import java.net.ConnectException; import java.net.URI; @@ -164,7 +167,15 @@ public class AuthenticationServiceCEImpl implements AuthenticationServiceCE { .flatMap(datasourceService::getById) .flatMap(datasource -> { OAuth2 oAuth2 = (OAuth2) datasource.getDatasourceConfiguration().getAuthentication(); - WebClient.Builder builder = WebClient.builder().baseUrl(oAuth2.getAccessTokenUrl()); + final HttpClient httpClient = HttpClient.create(); + + if (oAuth2.isUseSelfSignedCert()) { + httpClient.secure(SSLHelper.sslCheckForHttpClient(datasource.getDatasourceConfiguration())); + } + + WebClient.Builder builder = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .baseUrl(oAuth2.getAccessTokenUrl()); MultiValueMap map = new LinkedMultiValueMap<>(); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesWorkspaceClonerTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesWorkspaceClonerTests.java index 488bbcff52..cc107da120 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesWorkspaceClonerTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesWorkspaceClonerTests.java @@ -854,7 +854,6 @@ public class ExamplesWorkspaceClonerTests { DatasourceConfiguration dc2 = new DatasourceConfiguration(); ds2.setDatasourceConfiguration(dc2); dc2.setAuthentication(new OAuth2( - OAuth2.Type.CLIENT_CREDENTIALS, true, true, @@ -872,7 +871,8 @@ public class ExamplesWorkspaceClonerTests { new Property("custom token param 2", "custom token param value 2") ), null, - null + null, + false )); final Datasource ds3 = new Datasource();