I recently noticed this warning while scanning the Stripe documentation:
You can use the client secret to complete the payment process with the amount specified on the PaymentIntent. Don’t log it, embed it in URLs, or expose it to anyone other than the customer. Make sure that you have TLS on any page that includes the client secret.
A web app I run has been appending the client secret along with the payment intent ID to the payment confirmation page (i.e. redirect URL) ever since I first integrated it, without any modification from me as far as I can remember, so I've always assumed the integration was designed to be this way until I read this warning.
Is it safe for the client secret to be in the redirect URL?
I decided to answer this for myself because I was positive that I hadn't changed this behaviour from the documentation that I originally followed for the integration, and sure enough, despite the overzealous downvotes and comments from those who evidently have had no experience with the Stripe integration in question, this approach was then and still is the official way recommended by the main integration section of the Stripe documentation:
Make sure the return_url corresponds to a page on your website that provides the status of the payment. When Stripe redirects the customer to the return_url, we provide the following URL query parameters:
payment_intent: the unique identifier for the PaymentIntent
payment_intent_client_secret: the client secret of the PaymentIntent object...
Use one of the query parameters to retrieve the PaymentIntent. Inspect the status of the PaymentIntent to decide what to show your customers. You can also append your own query parameters when providing the return_url, which persist through the redirect process.
This approach is then demonstrated with the following code:
// Initialize Stripe.js using your publishable key
const stripe = Stripe('pk_test_***');
// Retrieve the "payment_intent_client_secret" query parameter appended to
// your return_url by Stripe.js
const clientSecret = new URLSearchParams(window.location.search).get(
'payment_intent_client_secret'
);
// Retrieve the PaymentIntent
stripe.retrievePaymentIntent(clientSecret).then(({paymentIntent}) => {
const message = document.querySelector('#message')
// Inspect the PaymentIntent `status` to indicate the status of the payment
// to your customer.
//
// Some payment methods will [immediately succeed or fail][0] upon
// confirmation, while others will first enter a `processing` state.
//
// [0]: https://stripe.com/docs/payments/payment-methods#payment-notification
switch (paymentIntent.status) {
case 'succeeded':
message.innerText = 'Success! Payment received.';
break;
case 'processing':
message.innerText = "Payment processing. We'll update you when payment is received.";
break;
case 'requires_payment_method':
message.innerText = 'Payment failed. Please try another payment method.';
// Redirect your user back to your payment page to attempt collecting
// payment again
break;
default:
message.innerText = 'Something went wrong.';
break;
}
This confirms my suspicion that the client_secret
is, as the name suggests, is intended to be seen on the client-side (i.e. by the user who's just paid), and that the general language about embedding it in URLs does not apply to the return URL.