Skip to content

Commit 477a3c4

Browse files
authored
Merge pull request #839 from Kit/automatically-refresh-token
OAuth: Automatically Refresh Token on Expiry
2 parents e9b1a02 + 62a01de commit 477a3c4

3 files changed

Lines changed: 119 additions & 8 deletions

File tree

includes/class-convertkit-settings.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function __construct() {
4646
}
4747

4848
// Update Access Token when refreshed by the API class.
49+
add_action( 'convertkit_api_get_access_token', array( $this, 'update_credentials' ), 10, 2 );
4950
add_action( 'convertkit_api_refresh_token', array( $this, 'update_credentials' ), 10, 2 );
5051

5152
}
@@ -500,18 +501,18 @@ public function get_defaults() {
500501
}
501502

502503
/**
503-
* Saves the new access token, refresh token and its expiry when the API
504-
* class automatically refreshes an outdated access token.
504+
* Saves the new access token, refresh token and its expiry, and schedules
505+
* a WordPress Cron event to refresh the token on expiry.
505506
*
506-
* @since 2.5.0
507+
* @since 2.8.3
507508
*
508509
* @param array $result New Access Token, Refresh Token and Expiry.
509510
* @param string $client_id OAuth Client ID used for the Access and Refresh Tokens.
510511
*/
511512
public function update_credentials( $result, $client_id ) {
512513

513514
// Don't save these credentials if they're not for this Client ID.
514-
// They're for another ConvertKit Plugin that uses OAuth.
515+
// They're for another Kit Plugin that uses OAuth.
515516
if ( $client_id !== CONVERTKIT_OAUTH_CLIENT_ID ) {
516517
return;
517518
}
@@ -524,6 +525,12 @@ public function update_credentials( $result, $client_id ) {
524525
)
525526
);
526527

528+
// Clear any existing scheduled WordPress Cron event.
529+
wp_clear_scheduled_hook( 'convertkit_refresh_token' );
530+
531+
// Schedule a WordPress Cron event to refresh the token on expiry.
532+
wp_schedule_single_event( ( $result['created_at'] + $result['expires_in'] ), 'convertkit_refresh_token' );
533+
527534
}
528535

529536
/**

includes/cron-functions.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,57 @@
66
* @author ConvertKit
77
*/
88

9+
/**
10+
* Refresh the OAuth access token, triggered by WordPress' Cron.
11+
*
12+
* @since 2.8.3
13+
*/
14+
function convertkit_refresh_token() {
15+
16+
// Get Settings and Log classes.
17+
$settings = new ConvertKit_Settings();
18+
19+
// Bail if no existing access and refresh token exists.
20+
if ( ! $settings->has_access_token() ) {
21+
return;
22+
}
23+
if ( ! $settings->has_refresh_token() ) {
24+
return;
25+
}
26+
27+
// Initialize the API.
28+
$api = new ConvertKit_API_V4(
29+
CONVERTKIT_OAUTH_CLIENT_ID,
30+
CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
31+
$settings->get_access_token(),
32+
$settings->get_refresh_token(),
33+
$settings->debug_enabled(),
34+
'cron_refresh_token'
35+
);
36+
37+
// Refresh the token.
38+
$result = $api->refresh_token();
39+
40+
// If an error occured, don't save the new tokens.
41+
// Logging is handled by the ConvertKit_API_V4 class.
42+
if ( is_wp_error( $result ) ) {
43+
return;
44+
}
45+
46+
$settings->save(
47+
array(
48+
'access_token' => $result['access_token'],
49+
'refresh_token' => $result['refresh_token'],
50+
'token_expires' => ( $result['created_at'] + $result['expires_in'] ),
51+
)
52+
);
53+
54+
}
55+
56+
// Register action to run above function; this action is created by WordPress' wp_schedule_event() function
57+
// in update_credentials() in the ConvertKit_Settings class.
58+
add_action( 'convertkit_refresh_token', 'convertkit_refresh_token' );
59+
960
/**
1061
* Refresh the Posts Resource cache, triggered by WordPress' Cron.
1162
*

tests/Integration/APITest.php

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ class APITest extends WPTestCase
2727
*/
2828
private $api;
2929

30+
/**
31+
* Holds the current timestamp, defined in setUp to fix
32+
* it for all tests.
33+
*
34+
* @since 2.8.3
35+
*
36+
* @var int
37+
*/
38+
private $now = 0;
39+
3040
/**
3141
* Performs actions before each test.
3242
*
@@ -36,6 +46,9 @@ public function setUp(): void
3646
{
3747
parent::setUp();
3848

49+
// Set the current timestamp to the start of the test.
50+
$this->now = strtotime( 'now' );
51+
3952
// Activate Plugin, to include the Plugin's constants in tests.
4053
activate_plugins('convertkit/wp-convertkit.php');
4154

@@ -77,7 +90,7 @@ public function testAccessTokenRefreshedAndSavedWhenExpired()
7790

7891
// Filter requests to mock the token expiry and refreshing the token.
7992
add_filter( 'pre_http_request', array( $this, 'mockAccessTokenExpiredResponse' ), 10, 3 );
80-
add_filter( 'pre_http_request', array( $this, 'mockRefreshTokenResponse' ), 10, 3 );
93+
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );
8194

8295
// Run request, which will trigger the above filters as if the token expired and refreshes automatically.
8396
$result = $this->api->get_account();
@@ -88,6 +101,46 @@ public function testAccessTokenRefreshedAndSavedWhenExpired()
88101
$this->assertEquals( $settings->get_refresh_token(), $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'] );
89102
}
90103

104+
/**
105+
* Test that a WordPress Cron event is created when an access token is obtained.
106+
*
107+
* @since 2.8.3
108+
*/
109+
public function testCronEventCreatedWhenAccessTokenObtained()
110+
{
111+
// Mock request as if the API returned an access and refresh token when a request
112+
// was made to refresh the token.
113+
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );
114+
115+
// Run request, as if the access token was obtained successfully.
116+
$result = $this->api->get_access_token( 'mockAuthCode' );
117+
118+
// Confirm the Cron event to refresh the access token was created, and the timestamp to
119+
// run the refresh token call matches the expiry of the access token.
120+
$nextScheduledTimestamp = wp_next_scheduled( 'convertkit_refresh_token' );
121+
$this->assertEquals( $nextScheduledTimestamp, $this->now + 10000 );
122+
}
123+
124+
/**
125+
* Test that a WordPress Cron event is created when an access token is refreshed.
126+
*
127+
* @since 2.8.3
128+
*/
129+
public function testCronEventCreatedWhenTokenRefreshed()
130+
{
131+
// Mock request as if the API returned an access and refresh token when a request
132+
// was made to refresh the token.
133+
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );
134+
135+
// Run request, as if the token was refreshed.
136+
$result = $this->api->refresh_token();
137+
138+
// Confirm the Cron event to refresh the access token was created, and the timestamp to
139+
// run the refresh token call matches the expiry of the access token.
140+
$nextScheduledTimestamp = wp_next_scheduled( 'convertkit_refresh_token' );
141+
$this->assertEquals( $nextScheduledTimestamp, $this->now + 10000 );
142+
}
143+
91144
/**
92145
* Mocks an API response as if the Access Token expired.
93146
*
@@ -138,15 +191,15 @@ public function mockAccessTokenExpiredResponse( $response, $parsed_args, $url )
138191
* @param string $url Request URL.
139192
* @return mixed
140193
*/
141-
public function mockRefreshTokenResponse( $response, $parsed_args, $url )
194+
public function mockTokenResponse( $response, $parsed_args, $url )
142195
{
143196
// Only mock requests made to the /token endpoint.
144197
if ( strpos( $url, 'https://api.kit.com/oauth/token' ) === false ) {
145198
return $response;
146199
}
147200

148201
// Remove this filter, so we don't end up in a loop when retrying the request.
149-
remove_filter( 'pre_http_request', array( $this, 'mockRefreshTokenResponse' ) );
202+
remove_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ) );
150203

151204
// Return a mock access and refresh token for this API request, as calling
152205
// refresh_token results in a new access and refresh token being provided,
@@ -158,7 +211,7 @@ public function mockRefreshTokenResponse( $response, $parsed_args, $url )
158211
'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
159212
'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'],
160213
'token_type' => 'bearer',
161-
'created_at' => strtotime( 'now' ),
214+
'created_at' => $this->now,
162215
'expires_in' => 10000,
163216
'scope' => 'public',
164217
)

0 commit comments

Comments
 (0)