Skip to content

Commit 962163c

Browse files
Prevent Stripe API calls after detecting the API keys are not valid (401 response from API) (#4323)
* Add transient to prevent api calls after getting a 401 response * Update includes/class-wc-stripe-api.php Co-authored-by: daledupreez <dale.du.preez@automattic.com> * Update error response to empty json * Remove invalid transient when saving new keys * Refactored to use get_option/update_option for concistency with how we save similar data for onboarding and webhooks stats * Update code comments * Update account not connected notification message * Show account as disconnected if the account data is not valid * Refactor 401 response to be null (needed for the Configure connection modal) * Clear account cache after detecting a 401 response * Add tests * Simplify option keys in test * Fix success response mock * Add changelo entry * Apply suggestions from code review Co-authored-by: daledupreez <dale.du.preez@automattic.com> --------- Co-authored-by: daledupreez <dale.du.preez@automattic.com>
1 parent 3a1a105 commit 962163c

File tree

7 files changed

+127
-3
lines changed

7 files changed

+127
-3
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Update - Remove BACS from the unsupported 'change payment method for subscription' page.
88
* Fix - Fix payment method title display when new payment settings experience is enabled
99
* Fix - Prevent styles from non-checkout pages affecting the appearance of Stripe element.
10+
* Fix - Prevent further Stripe API calls if API keys are invalid (401 response).
1011

1112
= 9.5.0 - 2025-05-13 =
1213
* Fix - Fixes the listing of payment methods on the classic checkout when the Optimized Checkout is enabled.

client/settings/account-details/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ const AccountDetails = () => {
133133
{ createInterpolateElement(
134134
isTestModeEnabled
135135
? __(
136-
"Seems like the test API keys we've saved for you are no longer valid. If you recently updated them, use the <strong>Configure Connection</strong> button below to reconnect.",
136+
"We couldn't connect to your account, it seems like the test API keys we've saved for you are no longer valid. Please use the <strong>Configure connection</strong> button below to reconnect.",
137137
'woocommerce-gateway-stripe'
138138
)
139139
: __(
140-
"Seems like the live API keys we've saved for you are no longer valid. If you recently updated them, use the <strong>Configure Connection</strong> button below to reconnect.",
140+
"We couldn't connect to your account, it seems like the live API keys we've saved for you are no longer valid. Please use the <strong>Configure connection</strong> button below to reconnect.",
141141
'woocommerce-gateway-stripe'
142142
),
143143
{

client/settings/stripe-auth-account/account-status-panel.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ const getAccountStatus = ( accountKeys, data, testMode ) => {
150150
},
151151
};
152152

153-
if ( ! hasKeys ) {
153+
if ( ! hasKeys || data?.account === null ) {
154154
return accountStatusMap.disconnected;
155155
}
156156

includes/class-wc-stripe-api.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ class WC_Stripe_API {
1616
const ENDPOINT = 'https://api.stripe.com/v1/';
1717
const STRIPE_API_VERSION = '2024-06-20';
1818

19+
/**
20+
* The test mode invalid API keys option key.
21+
*
22+
* @var string
23+
*/
24+
const TEST_MODE_INVALID_API_KEYS_OPTION_KEY = 'wc_stripe_test_invalid_api_keys_detected';
25+
26+
/**
27+
* The live mode invalid API keys option key.
28+
*
29+
* @var string
30+
*/
31+
const LIVE_MODE_INVALID_API_KEYS_OPTION_KEY = 'wc_stripe_live_invalid_api_keys_detected';
32+
1933
/**
2034
* Secret API Key.
2135
*
@@ -231,6 +245,13 @@ public static function request( $request, $api = 'charges', $method = 'POST', $w
231245
* @param string $api
232246
*/
233247
public static function retrieve( $api ) {
248+
// If we have an option flag indicating that the secret key is not valid, we don't attempt the API call and we return an error.
249+
$invalid_api_keys_option_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_INVALID_API_KEYS_OPTION_KEY : self::LIVE_MODE_INVALID_API_KEYS_OPTION_KEY;
250+
$invalid_api_keys_detected = get_option( $invalid_api_keys_option_key );
251+
if ( $invalid_api_keys_detected ) {
252+
return null; // The UI expects this empty response in case of invalid API keys.
253+
}
254+
234255
WC_Stripe_Logger::log( "{$api}" );
235256

236257
$response = wp_safe_remote_get(
@@ -242,6 +263,18 @@ public static function retrieve( $api ) {
242263
]
243264
);
244265

266+
// If we get a 401 error, we know the secret key is not valid.
267+
if ( is_array( $response ) && isset( $response['response'] ) && is_array( $response['response'] ) && isset( $response['response']['code'] ) && 401 === $response['response']['code'] ) {
268+
// We save a flag in the options to avoid making calls until the secret key gets updated.
269+
update_option( $invalid_api_keys_option_key, true );
270+
update_option( $invalid_api_keys_option_key . '_at', time() );
271+
272+
// We delete the transient for the account data to trigger the not-connected UI in the admin dashboard.
273+
delete_transient( WC_Stripe_Mode::is_test() ? WC_Stripe_Account::TEST_ACCOUNT_OPTION : WC_Stripe_Account::LIVE_ACCOUNT_OPTION );
274+
275+
return null; // The UI expects this empty response in case of invalid API keys.
276+
}
277+
245278
if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
246279
WC_Stripe_Logger::log( 'Error Response: ' . print_r( $response, true ) );
247280
return new WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'woocommerce-gateway-stripe' ) );

includes/connect/class-wc-stripe-connect.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ private function save_stripe_keys( $result, $type = 'connect', $mode = 'live' )
183183
update_option( 'wc_stripe_' . $prefix . 'oauth_failed_attempts', 0 );
184184
update_option( 'wc_stripe_' . $prefix . 'oauth_last_failed_at', '' );
185185

186+
// Clear the invalid API keys transient.
187+
$invalid_api_keys_option_key = $is_test ? WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY : WC_Stripe_API::LIVE_MODE_INVALID_API_KEYS_OPTION_KEY;
188+
update_option( $invalid_api_keys_option_key, false );
189+
update_option( $invalid_api_keys_option_key . '_at', time() );
190+
186191
if ( 'app' === $type ) {
187192
// Stripe App OAuth access_tokens expire after 1 hour:
188193
// https://docs.stripe.com/stripe-apps/api-authentication/oauth#refresh-access-token

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
118118
* Update - Remove BACS from the unsupported 'change payment method for subscription' page.
119119
* Fix - Fix payment method title display when new payment settings experience is enabled
120120
* Fix - Prevent styles from non-checkout pages affecting the appearance of Stripe element.
121+
* Fix - Prevent further Stripe API calls if API keys are invalid (401 response).
121122

122123

123124
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).

tests/phpunit/test-class-wc-stripe-api.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,88 @@ public function test_set_secret_key_for_mode_with_parameter() {
9494
WC_Stripe_API::set_secret_key_for_mode( 'invalid' );
9595
$this->assertEquals( self::LIVE_SECRET_KEY, WC_Stripe_API::get_secret_key() );
9696
}
97+
98+
/**
99+
* Test WC_Stripe_API::retrieve() when API returns 401 error.
100+
*/
101+
public function test_retrieve_handles_401_error() {
102+
// Mock a 401 API response
103+
add_filter( 'pre_http_request', [ $this, 'mock_401_response' ] );
104+
105+
// Call the retrieve method
106+
$result = WC_Stripe_API::retrieve( 'test_endpoint' );
107+
108+
// Verify the result is null
109+
$this->assertNull( $result );
110+
111+
// Verify the invalid API keys option was set
112+
$this->assertTrue( get_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY ) );
113+
114+
// Clean up
115+
remove_filter( 'pre_http_request', [ $this, 'mock_401_response' ] );
116+
delete_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY );
117+
}
118+
119+
/**
120+
* Test WC_Stripe_API::retrieve() when API keys are invalid.
121+
*/
122+
public function test_retrieve_returns_null_when_api_keys_are_invalid() {
123+
// Set up the invalid API keys option
124+
update_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY, true );
125+
126+
// Call the retrieve method
127+
$result = WC_Stripe_API::retrieve( 'test_endpoint' );
128+
129+
// Verify the result is null
130+
$this->assertNull( $result );
131+
132+
// Clean up
133+
delete_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY );
134+
}
135+
136+
/**
137+
* Test WC_Stripe_API::retrieve() when API keys are valid.
138+
*/
139+
public function test_retrieve_makes_api_call_when_api_keys_are_valid() {
140+
// Ensure no invalid API keys option exists
141+
delete_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY );
142+
143+
// Mock a successful API response
144+
add_filter( 'pre_http_request', [ $this, 'mock_successful_response' ] );
145+
146+
// Call the retrieve method
147+
$result = WC_Stripe_API::retrieve( 'test_endpoint' );
148+
149+
// Verify the result matches our mock response
150+
$this->assertEquals( 'success', $result );
151+
152+
// Clean up
153+
remove_filter( 'pre_http_request', [ $this, 'mock_successful_response' ] );
154+
}
155+
156+
/**
157+
* Helper method to mock a successful API response.
158+
*/
159+
public function mock_successful_response() {
160+
return [
161+
'response' => [
162+
'code' => 200,
163+
'message' => 'OK',
164+
],
165+
'body' => json_encode( 'success' ),
166+
];
167+
}
168+
169+
/**
170+
* Helper method to mock a 401 API response.
171+
*/
172+
public function mock_401_response() {
173+
return [
174+
'response' => [
175+
'code' => 401,
176+
'message' => 'Unauthorized',
177+
],
178+
'body' => '',
179+
];
180+
}
97181
}

0 commit comments

Comments
 (0)