From 75fcf73f025ea6c9832eedf429576dedee04bb23 Mon Sep 17 00:00:00 2001 From: FlipWarthog Date: Fri, 18 Sep 2026 13:49:10 -0400 Subject: [PATCH 1/2] Fix #108: Implement cient_credentials auth --- .../pardot/api/ConfigurationBuilder.java | 33 ++ .../api/auth/SsoSessionRefreshHandler.java | 21 +- .../api/config/SsoLoginCredentials.java | 25 +- ...otClient_SsoClientCredentialsAuthTest.java | 431 ++++++++++++++++++ 4 files changed, 499 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/darksci/pardot/api/PardotClient_SsoClientCredentialsAuthTest.java diff --git a/src/main/java/com/darksci/pardot/api/ConfigurationBuilder.java b/src/main/java/com/darksci/pardot/api/ConfigurationBuilder.java index 701cf92..3ca699e 100644 --- a/src/main/java/com/darksci/pardot/api/ConfigurationBuilder.java +++ b/src/main/java/com/darksci/pardot/api/ConfigurationBuilder.java @@ -118,6 +118,39 @@ public ConfigurationBuilder withSsoLogin(final String username, final String pas ), authorizationServer)); } + /** + * For configuring authenticating to the Pardot API using the client_credentials OAuth2 authentication flow. + * + * @param clientId Connected Application client or consumer Id. + * @param clientSecret Connected Application client or consumer secret. + * @param businessUnitId Id of the Pardot business unit to connect to. + * @return Builder instance. + */ + public ConfigurationBuilder withSsoLogin(final String clientId, final String clientSecret, final String businessUnitId) { + return withCustomAuthenticationHandler(new SsoSessionRefreshHandler(new SsoLoginCredentials( + Objects.requireNonNull(clientId), + Objects.requireNonNull(clientSecret), + Objects.requireNonNull(businessUnitId) + ), AuthorizationServer.DEFAULT_SALESFORCE)); + } + + /** + * For configuring authenticating to the Pardot API using the client_credentials OAuth2 authentication flow. + * + * @param clientId Connected Application client or consumer Id. + * @param clientSecret Connected Application client or consumer secret. + * @param businessUnitId Id of the Pardot business unit to connect to. + * @param authorizationServer Override the authorization server address. + * @return Builder instance. + */ + public ConfigurationBuilder withSsoLogin(final String clientId, final String clientSecret, final String businessUnitId, final AuthorizationServer authorizationServer) { + return withCustomAuthenticationHandler(new SsoSessionRefreshHandler(new SsoLoginCredentials( + Objects.requireNonNull(clientId), + Objects.requireNonNull(clientSecret), + Objects.requireNonNull(businessUnitId) + ), authorizationServer)); + } + /** * For configuring authenticating to the Pardot API using a previously acquired refresh_token acquired using * the access_code OAuth2 authentication flow. diff --git a/src/main/java/com/darksci/pardot/api/auth/SsoSessionRefreshHandler.java b/src/main/java/com/darksci/pardot/api/auth/SsoSessionRefreshHandler.java index 0d7b33a..56cae4d 100644 --- a/src/main/java/com/darksci/pardot/api/auth/SsoSessionRefreshHandler.java +++ b/src/main/java/com/darksci/pardot/api/auth/SsoSessionRefreshHandler.java @@ -50,12 +50,21 @@ public void clearToken() { @Override public boolean refreshCredentials(final PardotClient client) { - final SsoLoginResponse response = client.login(new SsoLoginRequest(authorizationServer) - .withClientId(credentials.getClientId()) - .withClientSecret(credentials.getClientSecret()) - .withUsername(credentials.getUsername()) - .withPassword(credentials.getPassword()) - ); + final SsoLoginResponse response; + if (credentials.getUsername() != null && credentials.getPassword() != null) { + response = client.login(new SsoLoginRequest(authorizationServer) + .withClientId(credentials.getClientId()) + .withClientSecret(credentials.getClientSecret()) + .withUsername(credentials.getUsername()) + .withPassword(credentials.getPassword()) + ); + } else { + response = client.login(new SsoLoginRequest(authorizationServer) + .withClientId(credentials.getClientId()) + .withClientSecret(credentials.getClientSecret()) + .withGrantType("client_credentials") + ); + } // If we have an API key. if (response.getAccessToken() != null) { diff --git a/src/main/java/com/darksci/pardot/api/config/SsoLoginCredentials.java b/src/main/java/com/darksci/pardot/api/config/SsoLoginCredentials.java index e55fd2c..98869fe 100644 --- a/src/main/java/com/darksci/pardot/api/config/SsoLoginCredentials.java +++ b/src/main/java/com/darksci/pardot/api/config/SsoLoginCredentials.java @@ -23,13 +23,30 @@ * Defines credentials for authenticating to Pardot API using Salesforce SSO. */ public class SsoLoginCredentials { + // Potentially null if using client_credentials auth + private String username; + private String password; // Immutable values. - private final String username; - private final String password; private final String clientId; private final String clientSecret; private final String businessUnitId; + /** + * Constructor. + * @param clientId Connected App client or consumer Id. + * @param clientSecret Connected App client or consumer secret. + * @param businessUnitId Pardot Business Unit Id to connect to. + */ + public SsoLoginCredentials( + final String clientId, + final String clientSecret, + final String businessUnitId) { + + this.clientId = Objects.requireNonNull(clientId); + this.clientSecret = Objects.requireNonNull(clientSecret); + this.businessUnitId = Objects.requireNonNull(businessUnitId); + } + /** * Constructor. * @param username Salesforce username. @@ -45,11 +62,9 @@ public SsoLoginCredentials( final String clientSecret, final String businessUnitId) { + this(clientId, clientSecret, businessUnitId); this.username = Objects.requireNonNull(username); this.password = Objects.requireNonNull(password); - this.clientId = Objects.requireNonNull(clientId); - this.clientSecret = Objects.requireNonNull(clientSecret); - this.businessUnitId = Objects.requireNonNull(businessUnitId); } public String getUsername() { diff --git a/src/test/java/com/darksci/pardot/api/PardotClient_SsoClientCredentialsAuthTest.java b/src/test/java/com/darksci/pardot/api/PardotClient_SsoClientCredentialsAuthTest.java new file mode 100644 index 0000000..852734f --- /dev/null +++ b/src/test/java/com/darksci/pardot/api/PardotClient_SsoClientCredentialsAuthTest.java @@ -0,0 +1,431 @@ +/** + * Copyright 2017, 2018, 2019, 2020 Stephen Powis https://github.com/Crim/pardot-java-client + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.darksci.pardot.api; + +import com.darksci.pardot.api.auth.AuthParameter; +import com.darksci.pardot.api.auth.AuthorizationServer; +import com.darksci.pardot.api.auth.SsoSessionRefreshHandler; +import com.darksci.pardot.api.config.Configuration; +import com.darksci.pardot.api.request.login.SsoLoginRequest; +import com.darksci.pardot.api.request.tag.TagReadRequest; +import com.darksci.pardot.api.request.user.UserReadRequest; +import com.darksci.pardot.api.response.login.SsoLoginResponse; +import com.darksci.pardot.api.response.tag.Tag; +import com.darksci.pardot.api.response.user.User; +import com.darksci.pardot.api.rest.RestClient; +import com.darksci.pardot.api.rest.RestResponse; +import org.junit.Before; +import org.junit.Test; +import util.TestHelper; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Unit testing over PardotClient using Sso Authentication Scheme. + */ +public class PardotClient_SsoClientCredentialsAuthTest { + // Dependencies + private Configuration apiConfig; + private RestClient mockRestClient; + + // Instance under test + private PardotClient pardotClient; + + // Create configuration + private final String clientId = "NotARealClientId"; + private final String clientSecret = "NotARealClientSecret"; + private final String businessId = "ABC-123-DEF"; + + @Before + public void before() { + final ConfigurationBuilder builder = Configuration.newBuilder() + .withSsoLogin(clientId, clientSecret, businessId); + apiConfig = builder.build(); + + // Create mock RestClient + mockRestClient = mock(RestClient.class); + + // Create instance using mock dependencies. + pardotClient = new PardotClient(apiConfig, mockRestClient); + } + + /** + * Smoke test over username & password authentication login requests. + */ + @Test + public void smokeTestDirectLoginRequest() { + // Construct request. + final SsoLoginRequest loginRequest = new SsoLoginRequest() + .withClientId(clientId) + .withClientSecret(clientId); + + // Mock response + when(mockRestClient.submitRequest(loginRequest)) + .thenReturn(createRestResponseFromFile("ssoLoginSuccess.json", 200)); + + // Call method under test + final SsoLoginResponse response = pardotClient.login(loginRequest); + + // Validate + assertNotNull(response); + assertEquals("Bearer", response.getTokenType()); + assertEquals("ACCESS_TOKEN_HERE", response.getAccessToken()); + assertEquals("https://test.my.salesforce.com", response.getInstanceUrl()); + assertEquals("https://login.salesforce.com/id/00DD/005B00", response.getId()); + assertEquals("1600482897925", response.getIssuedAt()); + assertEquals("SIGNATURE-HERE", response.getSignature()); + + // Verify mock interactions + verify(mockRestClient, times(1)) + .submitRequest(loginRequest); + verifyNoMoreRestClientInteractions(); + } + + /** + * Smoke test authorization server override. + */ + @Test + public void smokeTestDirectLoginRequest_alternativeAuthServer() { + // Construct request. + final SsoLoginRequest loginRequest = new SsoLoginRequest() + .withClientId(clientId) + .withClientSecret(clientId) + .withAuthorizationServer(new AuthorizationServer("http://test.server", "/end/point")); + + assertEquals("Invalid Api Hostname", "http://test.server", loginRequest.getApiHostname()); + assertEquals("Invalid End point", "/end/point", loginRequest.getApiEndpoint()); + } + + /** + * Smoke test authorization server override. + */ + @Test + public void smokeTestDirectLoginRequest_defaultAuthServer() { + // Construct request. + final SsoLoginRequest loginRequest = new SsoLoginRequest() + .withClientId(clientId) + .withClientSecret(clientId); + + assertEquals("Invalid Api Hostname", "https://login.salesforce.com", loginRequest.getApiHostname()); + assertEquals("Invalid End point", "/services/oauth2/token", loginRequest.getApiEndpoint()); + } + + /** + * Verifies the behavior of PardotClient when the library has not yet authenticated to Pardot's API. + * + * Expected behavior is the library internally detects that no valid session exists + * and attempts to authenticate automatically, and then makes the original request. + * + * We will execute a request to retrieve a tag by id. This should trigger the library + * to first attempt to authenticate. After that is successful, it should execute our original request. + */ + @Test + public void testIndirectLogin() { + // Sanity test + assertArrayEquals( + "AuthorizationRequestParameters should always be an empty array", + AuthParameter.EMPTY, + apiConfig.getSessionRefreshHandler().getAuthorizationRequestParameters() + ); + assertArrayEquals( + "AuthorizationHeaders should always start empty", + AuthParameter.EMPTY, + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders() + ); + + // Construct request to query a tag + // This exact request isn't really relevant. Just that it will trigger + // the library to authenticate automatically. + final TagReadRequest tagReadRequest = new TagReadRequest() + .selectById(1L); + + // Mock responses from RestClient/Api Server. + when(mockRestClient.submitRequest(isA(SsoLoginRequest.class))) + .thenReturn(createRestResponseFromFile("ssoLoginSuccess.json", 200)); + when(mockRestClient.submitRequest(isA(TagReadRequest.class))) + .thenReturn(createRestResponseFromFile("tagRead.xml", 200)); + + // Call method under test + final Optional responseOptional = pardotClient.tagRead(tagReadRequest); + + // Validate response for tag + assertNotNull(responseOptional); + assertTrue(responseOptional.isPresent()); + + final Tag response = responseOptional.get(); + assertEquals(1L, (long) response.getId()); + assertEquals("Standard Tag", response.getName()); + + // Validate we updated our Authorization Parameters based on the login. + assertArrayEquals( + "AuthorizationRequestParameters should always be an empty array", + AuthParameter.EMPTY, + apiConfig.getSessionRefreshHandler().getAuthorizationRequestParameters() + ); + assertEquals( + "Should have 2 authorization headers after authenticating", + 2, + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders().length + ); + + // Check Authorization Header + assertEquals( + "Should contain appropriate Authorization Header Name", + "Authorization", + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders()[0].getName() + ); + assertEquals( + "Should contain appropriate Authorization Header value", + "Bearer ACCESS_TOKEN_HERE", + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders()[0].getValue() + ); + + // Check Pardot Business Unit Id Header + assertEquals( + "Should contain appropriate Business Unit Header Name", + "Pardot-Business-Unit-Id", + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders()[1].getName() + ); + assertEquals( + "Should contain appropriate Business Unit Header value", + "ABC-123-DEF", + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders()[1].getValue() + ); + + // Verify mock interactions + verify(mockRestClient, times(1)) + .submitRequest(isA(SsoLoginRequest.class)); + verify(mockRestClient, times(1)) + .submitRequest(isA(TagReadRequest.class)); + verifyNoMoreRestClientInteractions(); + } + + /** + * Verifies the behavior of PardotClient when the login session times out. + * + * Expected behavior is the library internally captures the invalid session error response + * and attempts to re-authenticate automatically, and then replays the original request. + * + * We will execute a request to retrieve a tag by id. This should trigger the library + * to first attempt to authenticate. After that is successful, it should execute our original request. + */ + @Test + public void testReAuthenticationOnSessionTimeout() { + // Lets set a dummy Authentication Key to simulate already having a valid session + ((SsoSessionRefreshHandler)(apiConfig.getSessionRefreshHandler())).setApiToken("OriginalDummyKey"); + + // Construct request to query a tag + // This exact request isn't really relevant. Just that it will trigger + // the library to authenticate automatically. + final TagReadRequest tagReadRequest = new TagReadRequest() + .selectById(1L); + + // Mock responses from RestClient/Api Server. + when(mockRestClient.submitRequest(isA(SsoLoginRequest.class))) + .thenReturn(createRestResponseFromFile("ssoLoginSuccess.json", 200)); + + when(mockRestClient.submitRequest(isA(TagReadRequest.class))) + .thenReturn( + // First call should return an invalid API key response. + createRestResponseFromFile("errorInvalidSsoAccessToken.xml", 400), + + // Second call should return the real tag read response. + createRestResponseFromFile("tagRead.xml", 200) + ); + + // Call method under test + final Optional responseOptional = pardotClient.tagRead(tagReadRequest); + + // Validate response for tag + assertNotNull(responseOptional); + assertTrue(responseOptional.isPresent()); + + final Tag response = responseOptional.get(); + assertEquals(1L, (long) response.getId()); + assertEquals("Standard Tag", response.getName()); + + // Validate we updated our ApiConfig based on the login. + // Validate we updated our Authorization Parameters based on the login. + assertArrayEquals( + "AuthorizationRequestParameters should always be an empty array", + AuthParameter.EMPTY, + apiConfig.getSessionRefreshHandler().getAuthorizationRequestParameters() + ); + assertEquals( + "Should have 2 authorization headers after authenticating", + 2, + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders().length + ); + + // Check Authorization Header + assertEquals( + "Should contain appropriate Authorization Header Name", + "Authorization", + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders()[0].getName() + ); + assertEquals( + "Should contain appropriate Authorization Header value", + "Bearer ACCESS_TOKEN_HERE", + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders()[0].getValue() + ); + + // Check Pardot Business Unit Id Header + assertEquals( + "Should contain appropriate Business Unit Header Name", + "Pardot-Business-Unit-Id", + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders()[1].getName() + ); + assertEquals( + "Should contain appropriate Business Unit Header value", + "ABC-123-DEF", + apiConfig.getSessionRefreshHandler().getAuthorizationHeaders()[1].getValue() + ); + + // Verify mock interactions + verify(mockRestClient, times(1)) + .submitRequest(isA(SsoLoginRequest.class)); + verify(mockRestClient, times(2)) + .submitRequest(isA(TagReadRequest.class)); + verifyNoMoreRestClientInteractions(); + } + + /** + * Verifies the behavior of PardotClient when the login session times out, and when we + * attempt to renew the session we get invalid credentials. + * + * First we simulate having a valid ApiUserKey/Session. + * We request reading a tag, and have the API server return an invalid session result. + * The library should automatically attempt to renew the session. We mock + * the servers response to the login with an invalid credentials response. + * + * We expect the library to throw a LoginFailedException. + */ + @Test + public void testReAuthenticationOnSessionTimeout_triggersInvalidCredentials() { + // Lets set a dummy Authentication Key to simulate already having a valid session + ((SsoSessionRefreshHandler)(apiConfig.getSessionRefreshHandler())).setApiToken("OriginalDummyKey"); + + // Construct request to query a tag + // This exact request isn't really relevant. Just that it will trigger + // the library to authenticate automatically. + final TagReadRequest tagReadRequest = new TagReadRequest() + .selectById(1L); + + // Mock responses from RestClient/Api Server. + // When we request for a tag read, we should get an invalid API key response. + when(mockRestClient.submitRequest(isA(TagReadRequest.class))) + .thenReturn( + // First call should return an invalid API key response. + createRestResponseFromFile("errorInvalidSsoAccessToken.xml", 400) + ); + + // When it attempts to renew the session, we should get an invalid credentials response. + when(mockRestClient.submitRequest(isA(SsoLoginRequest.class))) + .thenReturn(createRestResponseFromFile("ssoLoginFailed.json", 400)); + + // Call method under test, this should throw an exception + assertThrows(LoginFailedException.class, () -> { + pardotClient.tagRead(tagReadRequest); + }); + } + + /** + * Verify behavior when attempting to retrieve a user by id, but the API returns an 'invalid user id' error. + * The result should be an empty optional. + */ + @Test + public void userRead_invalidUserId_returnsEmptyOptional() { + // All login to succeed. + mockSuccessfulLogin(); + + // Mock responses from RestClient/Api Server. + // When we request for a user read, we should get a does not exist API error. + // This should force an empty optional to be returned. + when(mockRestClient.submitRequest(isA(UserReadRequest.class))) + .thenReturn( + // First call should return an invalid API key response. + createRestResponseFromFile("userRead_invalidUserId.xml", 200) + ); + + final Optional response = pardotClient.userRead(new UserReadRequest()); + + assertNotNull("Should not be null", response); + assertFalse("Should not be present", response.isPresent()); + } + + /** + * Verify behavior when attempting to retrieve a user by id, but the API returns an 'invalid user id' error. + * The result should be an empty optional. + */ + @Test + public void userRead_validUserId_returnsPopulatedOptional() { + // All login to succeed. + mockSuccessfulLogin(); + + // Mock responses from RestClient/Api Server. + // When we request for a user read, we should get a does not exist API error. + // This should force an empty optional to be returned. + when(mockRestClient.submitRequest(isA(UserReadRequest.class))) + .thenReturn( + // First call should return a valid user response. + createRestResponseFromFile("userRead.xml", 200) + ); + + final Optional response = pardotClient.userRead(new UserReadRequest()); + + assertNotNull("Should not be null", response); + assertTrue("Should be present", response.isPresent()); + } + + private RestResponse createRestResponseFromFile(final String filename, int httpCode) { + try { + return new RestResponse( + TestHelper.readFile("mockResponses/" + filename), + httpCode + ); + } catch (final IOException exception) { + throw new RuntimeException(exception); + } + } + + private void verifyNoMoreRestClientInteractions() { + verify(mockRestClient, times(1)) + .init(apiConfig); + verifyNoMoreInteractions(mockRestClient); + } + + private void mockSuccessfulLogin() { + // Mock successful login response from RestClient/Api Server. + when(mockRestClient.submitRequest(isA(SsoLoginRequest.class))) + .thenReturn(createRestResponseFromFile("ssoLoginSuccess.json", 200)); + } +} From 7e3e26271c1ec8acae7104708b42a1299f04eb42 Mon Sep 17 00:00:00 2001 From: FlipWarthog Date: Fri, 18 Sep 2026 21:07:01 -0400 Subject: [PATCH 2/2] Break out client credentials into its own refresh handler --- .../pardot/api/ConfigurationBuilder.java | 10 +- .../SsoClientCredentialsRefreshHandler.java | 94 +++++++++++++++++++ .../api/auth/SsoSessionRefreshHandler.java | 21 ++--- .../api/config/SsoClientCredentials.java | 67 +++++++++++++ .../api/config/SsoLoginCredentials.java | 27 ++---- ...otClient_SsoClientCredentialsAuthTest.java | 53 ++++++----- 6 files changed, 208 insertions(+), 64 deletions(-) create mode 100644 src/main/java/com/darksci/pardot/api/auth/SsoClientCredentialsRefreshHandler.java create mode 100644 src/main/java/com/darksci/pardot/api/config/SsoClientCredentials.java diff --git a/src/main/java/com/darksci/pardot/api/ConfigurationBuilder.java b/src/main/java/com/darksci/pardot/api/ConfigurationBuilder.java index 3ca699e..d4e696a 100644 --- a/src/main/java/com/darksci/pardot/api/ConfigurationBuilder.java +++ b/src/main/java/com/darksci/pardot/api/ConfigurationBuilder.java @@ -17,23 +17,25 @@ package com.darksci.pardot.api; +import java.util.Objects; + import com.darksci.pardot.api.auth.AuthorizationServer; import com.darksci.pardot.api.auth.PasswordSessionRefreshHandler; import com.darksci.pardot.api.auth.SessionRefreshHandler; import com.darksci.pardot.api.auth.SsoAccessTokenSessionRefreshHandler; +import com.darksci.pardot.api.auth.SsoClientCredentialsRefreshHandler; import com.darksci.pardot.api.auth.SsoRefreshTokenSessionRefreshHandler; import com.darksci.pardot.api.auth.SsoSessionRefreshHandler; import com.darksci.pardot.api.config.Configuration; import com.darksci.pardot.api.config.PasswordLoginCredentials; import com.darksci.pardot.api.config.ProxyConfiguration; import com.darksci.pardot.api.config.SsoAccessTokenCredentials; +import com.darksci.pardot.api.config.SsoClientCredentials; import com.darksci.pardot.api.config.SsoLoginCredentials; import com.darksci.pardot.api.config.SsoRefreshTokenCredentials; import com.darksci.pardot.api.rest.interceptor.NoopRequestInterceptor; import com.darksci.pardot.api.rest.interceptor.RequestInterceptor; -import java.util.Objects; - /** * Pardot API Client Configuration Builder. * Used to construct {@link Configuration} instances. @@ -127,7 +129,7 @@ public ConfigurationBuilder withSsoLogin(final String username, final String pas * @return Builder instance. */ public ConfigurationBuilder withSsoLogin(final String clientId, final String clientSecret, final String businessUnitId) { - return withCustomAuthenticationHandler(new SsoSessionRefreshHandler(new SsoLoginCredentials( + return withCustomAuthenticationHandler(new SsoClientCredentialsRefreshHandler(new SsoClientCredentials( Objects.requireNonNull(clientId), Objects.requireNonNull(clientSecret), Objects.requireNonNull(businessUnitId) @@ -144,7 +146,7 @@ public ConfigurationBuilder withSsoLogin(final String clientId, final String cli * @return Builder instance. */ public ConfigurationBuilder withSsoLogin(final String clientId, final String clientSecret, final String businessUnitId, final AuthorizationServer authorizationServer) { - return withCustomAuthenticationHandler(new SsoSessionRefreshHandler(new SsoLoginCredentials( + return withCustomAuthenticationHandler(new SsoClientCredentialsRefreshHandler(new SsoClientCredentials( Objects.requireNonNull(clientId), Objects.requireNonNull(clientSecret), Objects.requireNonNull(businessUnitId) diff --git a/src/main/java/com/darksci/pardot/api/auth/SsoClientCredentialsRefreshHandler.java b/src/main/java/com/darksci/pardot/api/auth/SsoClientCredentialsRefreshHandler.java new file mode 100644 index 0000000..64f65e6 --- /dev/null +++ b/src/main/java/com/darksci/pardot/api/auth/SsoClientCredentialsRefreshHandler.java @@ -0,0 +1,94 @@ +/** + * Copyright 2017, 2018, 2019, 2020 Stephen Powis https://github.com/Crim/pardot-java-client + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.darksci.pardot.api.auth; + +import com.darksci.pardot.api.PardotClient; +import com.darksci.pardot.api.config.SsoClientCredentials; +import com.darksci.pardot.api.request.login.SsoLoginRequest; +import com.darksci.pardot.api.response.login.SsoLoginResponse; + +import java.util.Objects; + +/** + * Handles refreshing credentials using SSO Login method. + */ +public class SsoClientCredentialsRefreshHandler implements SessionRefreshHandler { + private final SsoClientCredentials credentials; + private final AuthorizationServer authorizationServer; + + private String apiToken = null; + + public SsoClientCredentialsRefreshHandler(final SsoClientCredentials credentials, final AuthorizationServer authorizationServer) { + this.credentials = Objects.requireNonNull(credentials); + this.authorizationServer = Objects.requireNonNull(authorizationServer); + } + + @Override + public boolean isValid() { + return apiToken != null; + } + + @Override + public void clearToken() { + this.apiToken = null; + } + + @Override + public boolean refreshCredentials(final PardotClient client) { + final SsoLoginResponse response = client.login(new SsoLoginRequest(authorizationServer) + .withClientId(credentials.getClientId()) + .withClientSecret(credentials.getClientSecret()) + .withGrantType("client_credentials") + ); + + // If we have an API key. + if (response.getAccessToken() != null) { + // Set it. + setApiToken(response.getAccessToken()); + return true; + } + + return false; + } + + @Override + public AuthParameter[] getAuthorizationHeaders() { + if (!isValid()) { + return AuthParameter.EMPTY; + } + + final String value = "Bearer " + apiToken; + return new AuthParameter[] { + new AuthParameter("Authorization", value), + new AuthParameter("Pardot-Business-Unit-Id", credentials.getBusinessUnitId()) + }; + } + + @Override + public AuthParameter[] getAuthorizationRequestParameters() { + return AuthParameter.EMPTY; + } + + /** + * Used to set ApiToken value. + * @param apiToken value to set. + */ + public void setApiToken(final String apiToken) { + this.apiToken = apiToken; + } +} diff --git a/src/main/java/com/darksci/pardot/api/auth/SsoSessionRefreshHandler.java b/src/main/java/com/darksci/pardot/api/auth/SsoSessionRefreshHandler.java index 56cae4d..0d7b33a 100644 --- a/src/main/java/com/darksci/pardot/api/auth/SsoSessionRefreshHandler.java +++ b/src/main/java/com/darksci/pardot/api/auth/SsoSessionRefreshHandler.java @@ -50,21 +50,12 @@ public void clearToken() { @Override public boolean refreshCredentials(final PardotClient client) { - final SsoLoginResponse response; - if (credentials.getUsername() != null && credentials.getPassword() != null) { - response = client.login(new SsoLoginRequest(authorizationServer) - .withClientId(credentials.getClientId()) - .withClientSecret(credentials.getClientSecret()) - .withUsername(credentials.getUsername()) - .withPassword(credentials.getPassword()) - ); - } else { - response = client.login(new SsoLoginRequest(authorizationServer) - .withClientId(credentials.getClientId()) - .withClientSecret(credentials.getClientSecret()) - .withGrantType("client_credentials") - ); - } + final SsoLoginResponse response = client.login(new SsoLoginRequest(authorizationServer) + .withClientId(credentials.getClientId()) + .withClientSecret(credentials.getClientSecret()) + .withUsername(credentials.getUsername()) + .withPassword(credentials.getPassword()) + ); // If we have an API key. if (response.getAccessToken() != null) { diff --git a/src/main/java/com/darksci/pardot/api/config/SsoClientCredentials.java b/src/main/java/com/darksci/pardot/api/config/SsoClientCredentials.java new file mode 100644 index 0000000..2f5ff9d --- /dev/null +++ b/src/main/java/com/darksci/pardot/api/config/SsoClientCredentials.java @@ -0,0 +1,67 @@ +/** + * Copyright 2017, 2018, 2019, 2020 Stephen Powis https://github.com/Crim/pardot-java-client + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.darksci.pardot.api.config; + +import java.util.Objects; + +/** + * Defines credentials for authenticating to Pardot API using Salesforce SSO Client Credentials flow. + */ +public class SsoClientCredentials { + // Immutable values. + private final String clientId; + private final String clientSecret; + private final String businessUnitId; + + /** + * Constructor. + * @param clientId Connected App client or consumer Id. + * @param clientSecret Connected App client or consumer secret. + * @param businessUnitId Pardot Business Unit Id to connect to. + */ + public SsoClientCredentials( + final String clientId, + final String clientSecret, + final String businessUnitId) { + + this.clientId = Objects.requireNonNull(clientId); + this.clientSecret = Objects.requireNonNull(clientSecret); + this.businessUnitId = Objects.requireNonNull(businessUnitId); + } + + public String getClientId() { + return clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public String getBusinessUnitId() { + return businessUnitId; + } + + @Override + public String toString() { + return "SsoClientCredentials{" + + "clientId='" + clientId + '\'' + + ", clientSecret='XXXXXXXXXX'" + + ", businessUnitId='" + businessUnitId + '\'' + + '}'; + } +} diff --git a/src/main/java/com/darksci/pardot/api/config/SsoLoginCredentials.java b/src/main/java/com/darksci/pardot/api/config/SsoLoginCredentials.java index 98869fe..eac962b 100644 --- a/src/main/java/com/darksci/pardot/api/config/SsoLoginCredentials.java +++ b/src/main/java/com/darksci/pardot/api/config/SsoLoginCredentials.java @@ -20,33 +20,16 @@ import java.util.Objects; /** - * Defines credentials for authenticating to Pardot API using Salesforce SSO. + * Defines credentials for authenticating to Pardot API using Salesforce SSO Username-Password Flow. */ public class SsoLoginCredentials { - // Potentially null if using client_credentials auth - private String username; - private String password; // Immutable values. + private final String username; + private final String password; private final String clientId; private final String clientSecret; private final String businessUnitId; - /** - * Constructor. - * @param clientId Connected App client or consumer Id. - * @param clientSecret Connected App client or consumer secret. - * @param businessUnitId Pardot Business Unit Id to connect to. - */ - public SsoLoginCredentials( - final String clientId, - final String clientSecret, - final String businessUnitId) { - - this.clientId = Objects.requireNonNull(clientId); - this.clientSecret = Objects.requireNonNull(clientSecret); - this.businessUnitId = Objects.requireNonNull(businessUnitId); - } - /** * Constructor. * @param username Salesforce username. @@ -62,9 +45,11 @@ public SsoLoginCredentials( final String clientSecret, final String businessUnitId) { - this(clientId, clientSecret, businessUnitId); this.username = Objects.requireNonNull(username); this.password = Objects.requireNonNull(password); + this.clientId = Objects.requireNonNull(clientId); + this.clientSecret = Objects.requireNonNull(clientSecret); + this.businessUnitId = Objects.requireNonNull(businessUnitId); } public String getUsername() { diff --git a/src/test/java/com/darksci/pardot/api/PardotClient_SsoClientCredentialsAuthTest.java b/src/test/java/com/darksci/pardot/api/PardotClient_SsoClientCredentialsAuthTest.java index 852734f..6bd368c 100644 --- a/src/test/java/com/darksci/pardot/api/PardotClient_SsoClientCredentialsAuthTest.java +++ b/src/test/java/com/darksci/pardot/api/PardotClient_SsoClientCredentialsAuthTest.java @@ -17,25 +17,6 @@ package com.darksci.pardot.api; -import com.darksci.pardot.api.auth.AuthParameter; -import com.darksci.pardot.api.auth.AuthorizationServer; -import com.darksci.pardot.api.auth.SsoSessionRefreshHandler; -import com.darksci.pardot.api.config.Configuration; -import com.darksci.pardot.api.request.login.SsoLoginRequest; -import com.darksci.pardot.api.request.tag.TagReadRequest; -import com.darksci.pardot.api.request.user.UserReadRequest; -import com.darksci.pardot.api.response.login.SsoLoginResponse; -import com.darksci.pardot.api.response.tag.Tag; -import com.darksci.pardot.api.response.user.User; -import com.darksci.pardot.api.rest.RestClient; -import com.darksci.pardot.api.rest.RestResponse; -import org.junit.Before; -import org.junit.Test; -import util.TestHelper; - -import java.io.IOException; -import java.util.Optional; - import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -49,8 +30,29 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import java.io.IOException; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +import com.darksci.pardot.api.auth.AuthParameter; +import com.darksci.pardot.api.auth.AuthorizationServer; +import com.darksci.pardot.api.auth.SsoClientCredentialsRefreshHandler; +import com.darksci.pardot.api.config.Configuration; +import com.darksci.pardot.api.request.login.SsoLoginRequest; +import com.darksci.pardot.api.request.tag.TagReadRequest; +import com.darksci.pardot.api.request.user.UserReadRequest; +import com.darksci.pardot.api.response.login.SsoLoginResponse; +import com.darksci.pardot.api.response.tag.Tag; +import com.darksci.pardot.api.response.user.User; +import com.darksci.pardot.api.rest.RestClient; +import com.darksci.pardot.api.rest.RestResponse; + +import util.TestHelper; + /** - * Unit testing over PardotClient using Sso Authentication Scheme. + * Unit testing over PardotClient using Sso Authentication Scheme with Client Credentials. */ public class PardotClient_SsoClientCredentialsAuthTest { // Dependencies @@ -86,7 +88,8 @@ public void smokeTestDirectLoginRequest() { // Construct request. final SsoLoginRequest loginRequest = new SsoLoginRequest() .withClientId(clientId) - .withClientSecret(clientId); + .withClientSecret(clientId) + .withGrantType("client_credentials"); // Mock response when(mockRestClient.submitRequest(loginRequest)) @@ -119,6 +122,7 @@ public void smokeTestDirectLoginRequest_alternativeAuthServer() { final SsoLoginRequest loginRequest = new SsoLoginRequest() .withClientId(clientId) .withClientSecret(clientId) + .withGrantType("client_credentials") .withAuthorizationServer(new AuthorizationServer("http://test.server", "/end/point")); assertEquals("Invalid Api Hostname", "http://test.server", loginRequest.getApiHostname()); @@ -133,7 +137,8 @@ public void smokeTestDirectLoginRequest_defaultAuthServer() { // Construct request. final SsoLoginRequest loginRequest = new SsoLoginRequest() .withClientId(clientId) - .withClientSecret(clientId); + .withClientSecret(clientId) + .withGrantType("client_credentials"); assertEquals("Invalid Api Hostname", "https://login.salesforce.com", loginRequest.getApiHostname()); assertEquals("Invalid End point", "/services/oauth2/token", loginRequest.getApiEndpoint()); @@ -241,7 +246,7 @@ public void testIndirectLogin() { @Test public void testReAuthenticationOnSessionTimeout() { // Lets set a dummy Authentication Key to simulate already having a valid session - ((SsoSessionRefreshHandler)(apiConfig.getSessionRefreshHandler())).setApiToken("OriginalDummyKey"); + ((SsoClientCredentialsRefreshHandler)(apiConfig.getSessionRefreshHandler())).setApiToken("OriginalDummyKey"); // Construct request to query a tag // This exact request isn't really relevant. Just that it will trigger @@ -332,7 +337,7 @@ public void testReAuthenticationOnSessionTimeout() { @Test public void testReAuthenticationOnSessionTimeout_triggersInvalidCredentials() { // Lets set a dummy Authentication Key to simulate already having a valid session - ((SsoSessionRefreshHandler)(apiConfig.getSessionRefreshHandler())).setApiToken("OriginalDummyKey"); + ((SsoClientCredentialsRefreshHandler)(apiConfig.getSessionRefreshHandler())).setApiToken("OriginalDummyKey"); // Construct request to query a tag // This exact request isn't really relevant. Just that it will trigger