woocommerce / woocommerce-gateway-paypal-express-checkout

59 stars 64 forks source link

Explicitly set 'same-origin' policy for `fetch()` (fixes compat with old browsers) #778

Closed jorgeatorres closed 4 years ago

jorgeatorres commented 4 years ago

Issue: #773.

Description

TL;DR: The fetch() API we're using in our Smart Payment Buttons JavaScript code to call the "start checkout" AJAX action when the PayPal button is clicked has an implicit value of "same-origin" for its "credentials" setting, which controls whether the request includes cookies with session/auth information as part of it or not. Before Aug 2017, the default value for this setting was "omit" which sent nothing, so browsers released before Aug 2017 or those that weren't updated, perform this request anonymously, breaking nonce validation. The failed nonce check produces an error which stops the PayPal checkout flow.

By making it explicitly that we want to send cookies with the request (by setting "credentials" to "same-origin") we guarantee old browsers behave the same as modern ones.

Detailed Explanation In #773 it was reported that the Smart Payment Buttons were not working on "old" versions of popular browsers such as Safari on iOS 11, Edge 42.x or even desktop Safari 11.x. I initially thought it was due to our use of JavaScript Promises and/or `fetch()`, but after some digging I found that the reason was that [this nonce check](https://github.com/woocommerce/woocommerce-gateway-paypal-express-checkout/blob/2631a8bc6433a9a0c857a73a7b887a0986d27eb7/includes/class-wc-gateway-ppec-cart-handler.php#L161-L163) was failing when on those browsers. I added some code to log the action, user id, token and other variables that are used inside `wp_verify_nonce()` when verifying a nonce. I then compared these values right before and after clicking the PayPal button in a single product page. I noticed that the only value that changed "randomly" when clicking the button was the [`$uid` variable](https://github.com/WordPress/WordPress/blob/a45e10c6dceca1252c9da2457118dc7d7a06c84f/wp-includes/pluggable.php#L2133-L2144) which should be the logged-in user ID or a nonce [generated by WooCommerce](https://github.com/woocommerce/woocommerce/blob/e9f71ac24cf0edcd2ac8b1539d1187e63b16fcc1/includes/class-wc-session-handler.php#L78) to identify the session (when logged-out). This apparent change in User ID means [the nonce we send to the "start checkout"](https://github.com/woocommerce/woocommerce-gateway-paypal-express-checkout/blob/2631a8bc6433a9a0c857a73a7b887a0986d27eb7/includes/class-wc-gateway-ppec-cart-handler.php#L161-L163) AJAX action doesn't match the one generated inside the actual callback given it is now using a "different" User ID. As such, the request returns the "Cheatin', huh?" error message instead of a proper JSON object, thus triggering a JavaScript error which stops the PayPal checkout flow. This request is made via `fetch()` but those browsers affected are supposed to support it. [After some reading](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) on the `fetch()` API I noticed this in reference to the "credentials" option that controls whether auth information and cookies is sent with the remote request: > fetch **won’t send cookies**, unless you set the credentials init option. (Since Aug 25, 2017. The spec changed the default credentials policy to same-origin. […] ) So basically "old" browsers (before 2017 or not updated after that) use a default value of "omit", which results in our "start checkout" call being executed as if it was for an anonymous user and making the nonce verification fail. Modern browsers use the new default of "same-origin" which sends the relevant cookies with the request. The fix? Setting this value explicitly to "same-origin" makes old browsers mimic the behavior of modern ones in this regard and addresses the issue.

Steps to test:

  1. Checkout master.
  2. Enable the PayPal Checkout buttons for at least one context (single product page, cart, checkout, etc.).
  3. Using an "old" browser such as Safari on iOS 11 or Edge 42.x, add a product to the cart or visit a single product page (if the buttons are enabled for the single product page).
  4. Click the PayPal button.
  5. The PayPal window opens and then closes, stopping the checkout flow.
  6. Checkout this branch.
  7. Repeat 3 & 4.
  8. Confirm that the PayPal checkout flow now works.

Closes #773.

mattallan commented 4 years ago

LGTM :+1: Nice debugging as always @jorgeatorres :100: