From c3e11bd66e4fc0463d9479ed5d479f3faa089ca0 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 7 Apr 2026 12:51:43 +0800 Subject: [PATCH 1/7] Revoke Access Token on Disconnect --- ...class-convertkit-admin-section-general.php | 17 +++++++++++- composer.json | 2 +- includes/functions.php | 26 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/admin/section/class-convertkit-admin-section-general.php b/admin/section/class-convertkit-admin-section-general.php index 11efb8301..6a55acf8f 100644 --- a/admin/section/class-convertkit-admin-section-general.php +++ b/admin/section/class-convertkit-admin-section-general.php @@ -193,7 +193,22 @@ private function maybe_disconnect() { return; } - // Delete Access Token. + // Revoke Access Token. + $api = new ConvertKit_API_V4( + CONVERTKIT_OAUTH_CLIENT_ID, + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, + $this->settings->get_access_token(), + $this->settings->get_refresh_token(), + $this->settings->debug_enabled(), + 'settings' + ); + $result = $api->revoke_token(); + if ( is_wp_error( $result ) ) { + $this->output_error( $result->get_error_message() ); + return; + } + + // Delete Access and Refresh Tokens. $settings = new ConvertKit_Settings(); $settings->delete_credentials(); diff --git a/composer.json b/composer.json index 800f2ab5c..29d1ede8e 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "project", "license": "GPLv3", "require": { - "convertkit/convertkit-wordpress-libraries": "2.1.3" + "convertkit/convertkit-wordpress-libraries": "dev-add-revoke-token-method" }, "require-dev": { "php-webdriver/webdriver": "^1.0", diff --git a/includes/functions.php b/includes/functions.php index f4f214ec1..b09f3b092 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -792,6 +792,29 @@ function convertkit_maybe_update_credentials( $result, $client_id ) { } +/** + * Deletes the stored access token, refresh token and its expiry from the Plugin settings, + * and clears any existing scheduled WordPress Cron event to refresh the token on expiry, + * when the user revokes the access token. + * + * @since 3.2.4 + * + * @param string $client_id OAuth Client ID used for the Access and Refresh Tokens. + */ +function convertkit_revoke_credentials( $client_id ) { + + // Don't delete these credentials if they're not for this Client ID. + // They're for another Kit Plugin that uses OAuth. + if ( $client_id !== CONVERTKIT_OAUTH_CLIENT_ID ) { + return; + } + + // Delete Access and Refresh Tokens. + $settings = new ConvertKit_Settings(); + $settings->delete_credentials(); + +} + /** * Deletes the stored access token, refresh token and its expiry from the Plugin settings, * and clears any existing scheduled WordPress Cron event to refresh the token on expiry, @@ -830,6 +853,9 @@ function convertkit_maybe_delete_credentials( $result, $client_id ) { add_action( 'convertkit_api_get_access_token', 'convertkit_maybe_update_credentials', 10, 2 ); add_action( 'convertkit_api_refresh_token', 'convertkit_maybe_update_credentials', 10, 2 ); +// Delete credentials when the user revokes the access token. +add_action( 'convertkit_api_revoke_token', 'convertkit_revoke_credentials', 10, 1 ); + // Delete credentials if the API class uses a invalid access token. // This prevents the Plugin making repetitive API requests that will 401. add_action( 'convertkit_api_access_token_invalid', 'convertkit_maybe_delete_credentials', 10, 2 ); From 392ad1700fb5144bb43509225a2dbb7f82f705c2 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 7 Apr 2026 13:22:14 +0800 Subject: [PATCH 2/7] Added integration test --- ...class-convertkit-admin-section-general.php | 13 +++-- tests/Integration/APITest.php | 54 +++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/admin/section/class-convertkit-admin-section-general.php b/admin/section/class-convertkit-admin-section-general.php index 6a55acf8f..13429b106 100644 --- a/admin/section/class-convertkit-admin-section-general.php +++ b/admin/section/class-convertkit-admin-section-general.php @@ -193,13 +193,16 @@ private function maybe_disconnect() { return; } + // Get Settings class. + $settings = new ConvertKit_Settings(); + // Revoke Access Token. $api = new ConvertKit_API_V4( CONVERTKIT_OAUTH_CLIENT_ID, CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, - $this->settings->get_access_token(), - $this->settings->get_refresh_token(), - $this->settings->debug_enabled(), + $settings->get_access_token(), + $settings->get_refresh_token(), + $settings->debug_enabled(), 'settings' ); $result = $api->revoke_token(); @@ -208,10 +211,6 @@ private function maybe_disconnect() { return; } - // Delete Access and Refresh Tokens. - $settings = new ConvertKit_Settings(); - $settings->delete_credentials(); - // Delete cached resources. $creator_network = new ConvertKit_Resource_Creator_Network_Recommendations(); $custom_fields = new ConvertKit_Resource_Custom_Fields(); diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index c601522d1..011256020 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -176,6 +176,60 @@ public function testCronEventCreatedWhenTokenRefreshed() $this->assertGreaterThanOrEqual( $nextScheduledTimestamp, time() + 10000 ); } + /** + * Test that the access token and refresh token are deleted from the Plugin's settings + * when the access token is revoked. + * + * @since 3.2.4 + */ + public function testCredentialsDeletedAndInvalidWhenRevoked() + { + // Initialize the API without an access token or refresh token. + $api = new \ConvertKit_API_V4( + $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + $_ENV['KIT_OAUTH_REDIRECT_URI'] + ); + + // Generate an access token by API key and secret. + $result = $api->get_access_token_by_api_key_and_secret( + $_ENV['CONVERTKIT_API_KEY'], + $_ENV['CONVERTKIT_API_SECRET'], + wp_generate_password( 10, false ) // Random tenant name to produce a token for this request only. + ); + + // Store the access token in the Plugin's settings. + $settings = new \ConvertKit_Settings(); + $settings->save( + array( + 'access_token' => $result['oauth']['access_token'], + 'refresh_token' => $result['oauth']['refresh_token'], + 'token_expires' => $result['oauth']['expires_at'], + ) + ); + + // Initialize the API with the access token and refresh token. + $api = new \ConvertKit_API_V4( + $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + $_ENV['KIT_OAUTH_REDIRECT_URI'], + $settings->get_access_token(), + $settings->get_refresh_token() + ); + + // Confirm the token works when making an authenticated request. + $this->assertNotInstanceOf( 'WP_Error', $api->get_account() ); + + // Revoke the access token. + $api->revoke_token(); + + // Confirm the access token and refresh token are deleted from the Plugin's settings. + $this->assertEmpty( $settings->get_access_token() ); + $this->assertEmpty( $settings->get_refresh_token() ); + $this->assertEmpty( $settings->get_token_expiry() ); + + // Confirm the token no longer works when making an authenticated request. + $this->assertInstanceOf( 'WP_Error', $api->get_account() ); + } + /** * Mocks an API response as if the Access Token expired. * From 01548e5f9c49de5d8737783e6ffbce6f912ebdfd Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 8 Apr 2026 10:02:53 +0800 Subject: [PATCH 3/7] Update to reflect use of `revoke_tokens` method; check method is available --- .../class-convertkit-admin-section-general.php | 18 +++++++++++++++--- includes/functions.php | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/admin/section/class-convertkit-admin-section-general.php b/admin/section/class-convertkit-admin-section-general.php index 13429b106..b9e531e43 100644 --- a/admin/section/class-convertkit-admin-section-general.php +++ b/admin/section/class-convertkit-admin-section-general.php @@ -196,8 +196,8 @@ private function maybe_disconnect() { // Get Settings class. $settings = new ConvertKit_Settings(); - // Revoke Access Token. - $api = new ConvertKit_API_V4( + // Setup API. + $api = new ConvertKit_API_V4( CONVERTKIT_OAUTH_CLIENT_ID, CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, $settings->get_access_token(), @@ -205,7 +205,19 @@ private function maybe_disconnect() { $settings->debug_enabled(), 'settings' ); - $result = $api->revoke_token(); + + // Check that we're using the Kit WordPress Libraries 2.1.4 or higher. + // If another Kit Plugin is active and out of date, its libraries might + // be loaded that don't have this method. + if ( ! method_exists( $api, 'revoke_tokens' ) ) { // @phpstan-ignore-line Older WordPress Libraries won't have this function. + $this->output_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.', 'convertkit' ) ); + return; + } + + // Revoke Access and Refresh Tokens. + // See convertkit_revoke_credentials() method in functions.php, which is called + // by the `convertkit_api_revoke_tokens` action and deletes credentials from the Plugin's settings. + $result = $api->revoke_tokens(); if ( is_wp_error( $result ) ) { $this->output_error( $result->get_error_message() ); return; diff --git a/includes/functions.php b/includes/functions.php index b09f3b092..a1f56b396 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -853,8 +853,8 @@ function convertkit_maybe_delete_credentials( $result, $client_id ) { add_action( 'convertkit_api_get_access_token', 'convertkit_maybe_update_credentials', 10, 2 ); add_action( 'convertkit_api_refresh_token', 'convertkit_maybe_update_credentials', 10, 2 ); -// Delete credentials when the user revokes the access token. -add_action( 'convertkit_api_revoke_token', 'convertkit_revoke_credentials', 10, 1 ); +// Delete credentials when the user revokes the access and refresh tokens. +add_action( 'convertkit_api_revoke_tokens', 'convertkit_revoke_credentials', 10, 1 ); // Delete credentials if the API class uses a invalid access token. // This prevents the Plugin making repetitive API requests that will 401. From c08776bfea772f0a2027a27c68db45296681bb85 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 8 Apr 2026 10:41:16 +0800 Subject: [PATCH 4/7] Delete all credentials from settings --- includes/class-convertkit-settings.php | 5 +++++ includes/functions.php | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/includes/class-convertkit-settings.php b/includes/class-convertkit-settings.php index 56d799077..13c5234b2 100644 --- a/includes/class-convertkit-settings.php +++ b/includes/class-convertkit-settings.php @@ -667,9 +667,14 @@ public function delete_credentials() { $this->save( array( + // OAuth. 'access_token' => '', 'refresh_token' => '', 'token_expires' => '', + + // API Key. + 'api_key' => '', + 'api_secret' => '', ) ); diff --git a/includes/functions.php b/includes/functions.php index a1f56b396..da941a77c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -801,7 +801,7 @@ function convertkit_maybe_update_credentials( $result, $client_id ) { * * @param string $client_id OAuth Client ID used for the Access and Refresh Tokens. */ -function convertkit_revoke_credentials( $client_id ) { +function convertkit_delete_credentials( $client_id ) { // Don't delete these credentials if they're not for this Client ID. // They're for another Kit Plugin that uses OAuth. @@ -854,7 +854,7 @@ function convertkit_maybe_delete_credentials( $result, $client_id ) { add_action( 'convertkit_api_refresh_token', 'convertkit_maybe_update_credentials', 10, 2 ); // Delete credentials when the user revokes the access and refresh tokens. -add_action( 'convertkit_api_revoke_tokens', 'convertkit_revoke_credentials', 10, 1 ); +add_action( 'convertkit_api_revoke_tokens', 'convertkit_delete_credentials', 10, 1 ); // Delete credentials if the API class uses a invalid access token. // This prevents the Plugin making repetitive API requests that will 401. From 4e0c10ab2abbe61d812e26cf2d018aea2d022061 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 8 Apr 2026 10:41:24 +0800 Subject: [PATCH 5/7] Added test to confirm credentials deleted on disconnection --- .../PluginSettingsGeneralCest.php | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsGeneralCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsGeneralCest.php index eda010b26..c938f0bc8 100644 --- a/tests/EndToEnd/general/plugin-screens/PluginSettingsGeneralCest.php +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsGeneralCest.php @@ -161,13 +161,47 @@ public function testValidCredentials(EndToEndTester $I) // Check that no notice is displayed that the API credentials are invalid. $I->dontSeeErrorNotice($I, 'Kit: Authorization failed. Please connect your Kit account.'); + } + + /** + * Test that the credentials and resources are deleted on disconnect. + * + * @since 3.2.4 + * + * @param EndToEndTester $I Tester. + */ + public function testCredentialsAndResourcesAreDeletedOnDisconnect(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); // Go to the Plugin's Settings Screen. $I->loadKitSettingsGeneralScreen($I); + // Fake the API Key, API Secret, Access and Refresh Tokens; if we revoke the tokens used for tests, future tests will fail. + $I->setupKitPlugin( + $I, + [ + 'access_token' => 'fakeAccessToken', + 'refresh_token' => 'fakeRefreshToken', + 'token_expires' => time() + 3600, + 'api_key' => 'fakeAPIKey', + 'api_secret' => 'fakeAPISecret', + ] + ); + // Disconnect the Plugin connection to Kit. $I->click('Disconnect'); + // Check credentials are removed from the settings. + $settings = $I->grabOptionFromDatabase('_wp_convertkit_settings'); + $I->assertEmpty($settings['access_token']); + $I->assertEmpty($settings['refresh_token']); + $I->assertEmpty($settings['token_expires']); + $I->assertEmpty($settings['api_key']); + $I->assertEmpty($settings['api_secret']); + // Check cached resources are removed from the database on disconnection. $I->dontSeeOptionInDatabase('convertkit_creator_network_recommendations'); $I->dontSeeOptionInDatabase('convertkit_custom_fields'); @@ -182,14 +216,6 @@ public function testValidCredentials(EndToEndTester $I) $I->see('Connect'); $I->dontSee('Disconnect'); $I->dontSeeElementInDOM('input#submit'); - - // Check that the option table no longer contains cached resources. - $I->dontSeeOptionInDatabase('convertkit_creator_network_recommendations'); - $I->dontSeeOptionInDatabase('convertkit_forms'); - $I->dontSeeOptionInDatabase('convertkit_landing_pages'); - $I->dontSeeOptionInDatabase('convertkit_posts'); - $I->dontSeeOptionInDatabase('convertkit_products'); - $I->dontSeeOptionInDatabase('convertkit_tags'); } /** From d9274825d179d0e6222a1784bc4c36ba55ff4afb Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 8 Apr 2026 10:45:43 +0800 Subject: [PATCH 6/7] Fix code comment --- admin/section/class-convertkit-admin-section-general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/section/class-convertkit-admin-section-general.php b/admin/section/class-convertkit-admin-section-general.php index b9e531e43..dc10bbb5b 100644 --- a/admin/section/class-convertkit-admin-section-general.php +++ b/admin/section/class-convertkit-admin-section-general.php @@ -215,7 +215,7 @@ private function maybe_disconnect() { } // Revoke Access and Refresh Tokens. - // See convertkit_revoke_credentials() method in functions.php, which is called + // See convertkit_delete_credentials() method in functions.php, which is called // by the `convertkit_api_revoke_tokens` action and deletes credentials from the Plugin's settings. $result = $api->revoke_tokens(); if ( is_wp_error( $result ) ) { From 3595841e78e867d1c94a5e7c0c70de09a43bc6fd Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 8 Apr 2026 14:45:41 +0800 Subject: [PATCH 7/7] Updated integration test --- tests/Integration/APITest.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index 011256020..0ff6b67bb 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -218,16 +218,29 @@ public function testCredentialsDeletedAndInvalidWhenRevoked() // Confirm the token works when making an authenticated request. $this->assertNotInstanceOf( 'WP_Error', $api->get_account() ); - // Revoke the access token. - $api->revoke_token(); + // Revoke the access and refresh tokens. + $api->revoke_tokens(); // Confirm the access token and refresh token are deleted from the Plugin's settings. $this->assertEmpty( $settings->get_access_token() ); $this->assertEmpty( $settings->get_refresh_token() ); $this->assertEmpty( $settings->get_token_expiry() ); - // Confirm the token no longer works when making an authenticated request. + // Initialize the API with the (now revoked) access token and refresh token. + // revoke_tokens() will have removed the access token and refresh token from the API class, so we need to provide them again + // to test they're revoked. + $api = new \ConvertKit_API_V4( + $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], + $result['oauth']['access_token'], + $result['oauth']['refresh_token'] + ); + + // Confirm attempting to use the revoked access token no longer works. $this->assertInstanceOf( 'WP_Error', $api->get_account() ); + + // Confirm attempting to use the revoked refresh token no longer works. + $this->assertInstanceOf( 'WP_Error', $api->refresh_token() ); } /**