Skip to content

Commit 4a40c9b

Browse files
authored
Merge pull request #132 from Kit/revoke-remove-tokens-disconnect
Revoke and Remove Tokens on Disconnect
2 parents c93f79d + 000b258 commit 4a40c9b

11 files changed

Lines changed: 376 additions & 40 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ jobs:
5959
'EndToEnd/forms',
6060
'EndToEnd/general',
6161
'EndToEnd/recommendations',
62+
'EndToEnd/uninstall',
6263
'Integration'
6364
]
6465

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "project",
55
"license": "GPLv3",
66
"require": {
7-
"convertkit/convertkit-wordpress-libraries": "2.1.3"
7+
"convertkit/convertkit-wordpress-libraries": "2.1.5"
88
},
99
"require-dev": {
1010
"php-webdriver/webdriver": "^1.0",

includes/class-integrate-convertkit-wpforms.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,31 @@ public function delete_resource_cache() {
962962
// Get API instance.
963963
$api = $this->get_api_instance( $account_id );
964964

965+
// Check that we're using the Kit WordPress Libraries 2.1.4 or higher.
966+
// If another Kit Plugin is active and out of date, its libraries might
967+
// be loaded that don't have this method.
968+
if ( ! method_exists( $api, 'revoke_tokens' ) ) { // @phpstan-ignore-line Older WordPress Libraries won't have this function.
969+
wp_send_json_error(
970+
array(
971+
'error' => __( 'The Kit WordPress Libraries is missing the `revoke_tokens` method. Please update all Kit WordPress Plugins to their latest versions, and click Disconnect again.', 'integrate-convertkit-wpforms' ),
972+
)
973+
);
974+
}
975+
976+
// Revoke Access and Refresh Tokens.
977+
// See integrate_convertkit_wpforms_delete_credentials() method in functions.php, which is called
978+
// by the `convertkit_api_revoke_tokens` action and deletes credentials from the Plugin's settings.
979+
$result = $api->revoke_tokens();
980+
981+
// Bail if an error occurred.
982+
if ( is_wp_error( $result ) ) {
983+
wp_send_json_error(
984+
array(
985+
'error' => $result->get_error_message(),
986+
)
987+
);
988+
}
989+
965990
// Delete cached resources.
966991
$resource_forms = new Integrate_ConvertKit_WPForms_Resource_Forms( $api, $account_id );
967992
$resource_sequences = new Integrate_ConvertKit_WPForms_Resource_Sequences( $api, $account_id );

tests/EndToEnd/forms/FormCest.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -812,11 +812,9 @@ private function _wpFormsCompleteAndSubmitForm(EndToEndTester $I, int $pageID, s
812812
// Check that a review request was created.
813813
$I->reviewRequestExists($I);
814814

815-
// Disconnect the account.
816-
$I->disconnectAccount($I, $this->accountID);
817-
818-
// Check that the resources are no longer cached under the given account ID.
819-
$I->dontSeeCachedResourcesInDatabase($I, $this->accountID);
815+
// Remove the provider connection.
816+
// We don't disconnect the account, as this would now revoke the tokens and cause later tests to fail.
817+
$I->removeProviderConnection($I, $this->accountID);
820818
}
821819

822820
/**

tests/EndToEnd/general/IntegrationsCest.php

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,6 @@ public function testAddIntegrationWithValidCredentials(EndToEndTester $I)
9191
),
9292
$reconnectURL
9393
);
94-
95-
// Confirm that the connection can be disconnected.
96-
$I->click('Disconnect');
97-
98-
// Confirm that we want to disconnect.
99-
$I->waitForElementVisible('.jconfirm-box');
100-
$I->click('.jconfirm-box button.btn-confirm');
101-
102-
// Confirm no connection is listed.
103-
$I->wait(3);
104-
$I->dontSee('Connected on:');
10594
}
10695

10796
/**
@@ -138,8 +127,8 @@ public function testInvalidCredentials(EndToEndTester $I)
138127
// Define connection with invalid API credentials.
139128
$I->setupWPFormsIntegration(
140129
$I,
141-
'fakeAccessToken',
142-
'fakeRefreshToken'
130+
accessToken: 'fakeAccessToken',
131+
refreshToken: 'fakeRefreshToken'
143132
);
144133

145134
// Setup WPForms Form and configuration for this test.
@@ -178,6 +167,57 @@ public function testInvalidCredentials(EndToEndTester $I)
178167
$I->seeErrorNotice($I, 'Kit for WPForms: Authorization failed. Please reconnect your Kit account.');
179168
}
180169

170+
/**
171+
* Test that the credentials and resources are deleted on disconnect.
172+
*
173+
* @since 1.9.2
174+
*
175+
* @param EndToEndTester $I Tester.
176+
*/
177+
public function testCredentialsAndResourcesAreDeletedOnDisconnect(EndToEndTester $I)
178+
{
179+
// Define a random account ID.
180+
$accountID = 'kit-' . wp_generate_password( 10, false );
181+
182+
// Fake the API Key, Access and Refresh Tokens; if we revoke the tokens used for tests, future tests will fail.
183+
$I->setupWPFormsIntegration(
184+
$I,
185+
accessToken: 'fakeAccessToken',
186+
refreshToken: 'fakeRefreshToken',
187+
apiKey: 'fakeAPIKey',
188+
apiSecret: 'fakeAPISecret',
189+
accountID: $accountID
190+
);
191+
192+
// Load WPForms > Settings > Integrations.
193+
$I->amOnAdminPage('admin.php?page=wpforms-settings&view=integrations');
194+
195+
// Expand Kit integration section.
196+
$I->click('#wpforms-integration-convertkit');
197+
198+
// Disconnect the connection to Kit.
199+
$I->waitForElementVisible('a[data-provider="convertkit"]');
200+
$I->click('Disconnect');
201+
202+
// Confirm that we want to disconnect.
203+
$I->waitForElementVisible('.jconfirm-box');
204+
$I->click('.jconfirm-box button.btn-confirm');
205+
206+
// Confirm no connection is listed.
207+
$I->wait(3);
208+
$I->dontSee('Connected on:');
209+
210+
// Check connection is removed from the settings.
211+
// Clicking 'Disconnect' in WPForms removes the connection from the settings,
212+
// including any credentials within that connection.
213+
$providers = $I->grabOptionFromDatabase('wpforms_providers');
214+
$I->assertArrayHasKey('convertkit', $providers);
215+
$I->assertCount(0, $providers['convertkit']);
216+
217+
// Check cached resources are removed from the database on disconnection.
218+
$I->dontSeeCachedResourcesInDatabase($I, $accountID);
219+
}
220+
181221
/**
182222
* Deactivate and reset Plugin(s) after each test, if the test passes.
183223
* We don't use _after, as this would provide a screenshot of the Plugin
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
namespace Tests\EndToEnd;
4+
5+
use Tests\Support\EndToEndTester;
6+
7+
/**
8+
* Tests Plugin uninstallation.
9+
*
10+
* @since 1.9.2
11+
*/
12+
class UninstallCest
13+
{
14+
/**
15+
* Test that the Plugin's access and refresh tokens are revoked, and all v4 and v3
16+
* API credentials are removed from the Plugin's settings when the Plugin is deleted.
17+
*
18+
* @since 1.9.2
19+
*
20+
* @param EndToEndTester $I Tester.
21+
*/
22+
public function testPluginDeletionRevokesAndRemovesTokens(EndToEndTester $I)
23+
{
24+
// Activate this Plugin.
25+
$I->activateConvertKitPlugin($I);
26+
27+
// Generate an access token and refresh token by API key and secret.
28+
// We don't use the tokens from the environment, as revoking those
29+
// would result in later tests failing.
30+
$result = wp_remote_post(
31+
'https://api.kit.com/wordpress/accounts/oauth_access_token',
32+
[
33+
'headers' => [
34+
'Content-Type' => 'application/json',
35+
],
36+
'body' => wp_json_encode(
37+
[
38+
'api_key' => $_ENV['CONVERTKIT_API_KEY'],
39+
'api_secret' => $_ENV['CONVERTKIT_API_SECRET'],
40+
'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
41+
'tenant_name' => wp_generate_password( 10, false ), // Random tenant name to produce a token for this request only.
42+
]
43+
),
44+
]
45+
);
46+
$tokens = json_decode(wp_remote_retrieve_body($result), true)['oauth'];
47+
48+
// Store the tokens and API keys in the Plugin's settings.
49+
$I->setupWPFormsIntegration(
50+
$I,
51+
accessToken: $tokens['access_token'],
52+
refreshToken: $tokens['refresh_token'],
53+
apiKey: $_ENV['CONVERTKIT_API_KEY'],
54+
apiSecret: $_ENV['CONVERTKIT_API_SECRET']
55+
);
56+
57+
// Deactivate the Plugin.
58+
$I->deactivateConvertKitPlugin($I);
59+
60+
// Delete the Plugin.
61+
$I->deleteKitPlugin($I);
62+
63+
// Confirm the credentials have been removed from the Plugin's settings.
64+
$I->wait(3);
65+
$settings = $I->grabOptionFromDatabase('wpforms_providers');
66+
$connection = reset($settings['convertkit']);
67+
$I->assertEmpty($connection['access_token']);
68+
$I->assertEmpty($connection['refresh_token']);
69+
$I->assertEmpty($connection['api_key']);
70+
$I->assertEmpty($connection['api_secret']);
71+
72+
// Confirm attempting to use the revoked access token no longer works.
73+
$result = wp_remote_get(
74+
'https://api.kit.com/v4/account',
75+
[
76+
'headers' => [
77+
'Authorization' => 'Bearer ' . $tokens['access_token'],
78+
],
79+
]
80+
);
81+
$data = json_decode(wp_remote_retrieve_body($result), true);
82+
$I->assertArrayHasKey( 'errors', $data );
83+
$I->assertEquals( 'The access token was revoked', $data['errors'][0] );
84+
85+
// Confirm attempting to use the revoked refresh token no longer works.
86+
$result = wp_remote_post(
87+
'https://api.kit.com/v4/oauth/token',
88+
[
89+
'headers' => [
90+
'Authorization' => 'Bearer ' . $tokens['access_token'],
91+
],
92+
'body' => [
93+
'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
94+
'grant_type' => 'refresh_token',
95+
'refresh_token' => $tokens['refresh_token'],
96+
],
97+
]
98+
);
99+
$data = json_decode(wp_remote_retrieve_body($result), true);
100+
$I->assertArrayHasKey( 'error', $data );
101+
$I->assertEquals( 'invalid_grant', $data['error'] );
102+
}
103+
}

tests/Integration/APITest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,58 @@ public function testCronEventCreatedWhenTokenRefreshed()
200200
$this->assertGreaterThanOrEqual( $nextScheduledTimestamp, time() + 10000 );
201201
}
202202

203+
/**
204+
* Test that the access token and refresh token are deleted from the Plugin's settings
205+
* when the access token is revoked.
206+
*
207+
* @since 1.9.2
208+
*/
209+
public function testCredentialsDeletedAndInvalidWhenRevoked()
210+
{
211+
// Initialize the API without an access token or refresh token.
212+
$api = new \Integrate_ConvertKit_WPForms_API(
213+
$_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
214+
$_ENV['KIT_OAUTH_REDIRECT_URI']
215+
);
216+
217+
// Generate an access token by API key and secret.
218+
$result = $api->get_access_token_by_api_key_and_secret(
219+
$_ENV['CONVERTKIT_API_KEY'],
220+
$_ENV['CONVERTKIT_API_SECRET'],
221+
wp_generate_password( 10, false ) // Random tenant name to produce a token for this request only.
222+
);
223+
224+
// Initialize the API with the access token and refresh token.
225+
$api = new \Integrate_ConvertKit_WPForms_API(
226+
$_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
227+
$_ENV['KIT_OAUTH_REDIRECT_URI'],
228+
$result['oauth']['access_token'],
229+
$result['oauth']['refresh_token']
230+
);
231+
232+
// Confirm the token works when making an authenticated request.
233+
$this->assertNotInstanceOf( 'WP_Error', $api->get_account() );
234+
235+
// Revoke the access and refresh tokens.
236+
$api->revoke_tokens();
237+
238+
// Initialize the API with the (now revoked) access token and refresh token.
239+
// revoke_tokens() will have removed the access token and refresh token from the API class, so we need to provide them again
240+
// to test they're revoked.
241+
$api = new \Integrate_ConvertKit_WPForms_API(
242+
$_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
243+
$_ENV['KIT_OAUTH_REDIRECT_URI'],
244+
$result['oauth']['access_token'],
245+
$result['oauth']['refresh_token']
246+
);
247+
248+
// Confirm attempting to use the revoked access token no longer works.
249+
$this->assertInstanceOf( 'WP_Error', $api->get_account() );
250+
251+
// Confirm attempting to use the revoked refresh token no longer works.
252+
$this->assertInstanceOf( 'WP_Error', $api->refresh_token() );
253+
}
254+
203255
/**
204256
* Mocks an API response as if the Access Token expired.
205257
*

tests/Support/Helper/Plugin.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ public function deactivateConvertKitPlugin($I)
3535
$I->deactivateThirdPartyPlugin($I, 'integrate-convertkit-wpforms');
3636
}
3737

38+
/**
39+
* Helper method to delete the Kit Plugin.
40+
*
41+
* @since 1.9.2
42+
*
43+
* @param EndToEndTester $I EndToEndTester.
44+
*/
45+
public function deleteKitPlugin($I)
46+
{
47+
$I->deleteThirdPartyPlugin($I, 'integrate-convertkit-wpforms');
48+
}
49+
3850
/**
3951
* Helper method to determine that the order of the Form resources in the given
4052
* select element are in the expected alphabetical order.

tests/Support/Helper/ThirdPartyPlugin.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,39 @@ public function deactivateThirdPartyPlugin($I, $name)
7474
$I->waitForElementVisible('table.plugins tr[data-slug=' . $name . '].inactive');
7575
}
7676

77+
/**
78+
* Helper method to delete a third party Plugin, checking
79+
* it deleted and no errors were output.
80+
*
81+
* @since 1.9.2
82+
*
83+
* @param EndToEndTester $I EndToEnd Tester.
84+
* @param string $name Plugin Slug.
85+
*/
86+
public function deleteThirdPartyPlugin($I, $name)
87+
{
88+
// Login as the Administrator, if we're not already logged in.
89+
if ( ! $this->amLoggedInAsAdmin($I) ) {
90+
$this->doLoginAsAdmin($I);
91+
}
92+
93+
// Go to the Plugins screen in the WordPress Administration interface.
94+
$I->amOnPluginsPage();
95+
96+
// Wait for the Plugins page to load.
97+
$I->waitForElementVisible('body.plugins-php');
98+
99+
// Delete the Plugin.
100+
$I->waitForElementVisible('a#delete-' . $name);
101+
$I->click('a#delete-' . $name);
102+
103+
// Click the confirmation dialog.
104+
$I->acceptPopup();
105+
106+
// Wait for the Plugin to be marked as deleted.
107+
$I->waitForElementNotVisible('table.plugins tr.deleted[data-slug=' . $name . ']');
108+
}
109+
77110
/**
78111
* Helper method to check if the Administrator is logged in.
79112
*

0 commit comments

Comments
 (0)