Increasing payments processing security with Stripe Card authentication and 3D Secure

Dani Shulman
9 min readJul 13, 2023

--

Fraud prevention and regulatory compliance are essential aspects of any online payment system. To address these concerns, card authentication and 3D Secure technology play a crucial role. By implementing these measures, businesses can reduce fraud risks and ensure compliance with regulations, such as the Strong Customer Authentication (SCA) requirements in Europe. This guide will provide you with a comprehensive overview of card authentication and 3D Secure, explaining the necessary steps to implement them effectively. Additionally, we will explore the liability shift rule and customization options to tailor the authentication process to your specific needs.

Card authentication and 3D Secure technology are essential for reducing fraud and providing added security to online payments. 3D Secure 2 (3DS2) is the latest version of this standard, offering improved authentication methods and a better user experience compared to 3D Secure 1.

Stripe supports 3D Secure 2 on its payments APIs, mobile SDKs, and Checkout, allowing businesses to implement these security measures effectively. By leveraging 3D Secure 2, businesses can reduce fraud risks and ensure a smoother checkout process for their customers.

When implementing 3D Secure for card authentication, it’s important to understand the liability shift rule. The liability shift means that if a payment is successfully authenticated using 3D Secure, the liability for fraud disputes shifts from the business to the card issuer. This generally protects businesses from fraudulent chargebacks. However, there are some cases where the liability shift may not apply, such as when the card or issuer doesn’t support 3D Secure or if the payment falls into certain exempted categories. It’s crucial to respond to dispute inquiries promptly to avoid potential financial chargebacks. While rare, there can be instances where liability shift is downgraded or not properly recognized, and in such cases, providing additional evidence and information can help strengthen your position in disputes. It’s essential to familiarize yourself with the dispute process and make informed decisions to manage and minimize disputes effectively.

Enable the rule

Inside the Stripe dashboard there is a UI to enable the 3DS rules, we will use the Test mode to enable the 3DS rules.

Our current Strip payment integration works by:

  1. Making a request from the client to https://api.stripe.com/v1/tokens endpoint, with the card info, and Stripe public token. The response comes back with token
  2. Making a request to our backend with the token to process the card, the backend uses the token and attaches the sale amount, sale details, customer info. Using the Stripe private token we send a request to process the card to https://api.stripe.com/v1/charges

Option 1. Migration to PaymentIntent

Since PaymentIntent flow is require for this integration, it would mean we have to change the way we process cards on our web and app.

The main difference between using PaymentIntent and making requests to the Stripe Tokens and Charges API lies in the workflow and level of control over the payment process.

Workflow: With the Tokens and Charges API approach, you typically create a token object that represents a customer’s payment details (such as card information), and then use that token to create a charge object to process the payment. This two-step process requires separate API calls and handling of multiple objects.

On the other hand, PaymentIntent provides a unified and more streamlined workflow. It combines the steps of creating a payment and processing it into a single object. You create a PaymentIntent and specify the payment amount, currency, and payment method directly. PaymentIntent handles the payment process, including authentication, authorization, and capture, all within a single API call.

Flexibility and Control: PaymentIntent offers more flexibility and control over the payment process compared to the Tokens and Charges API. With PaymentIntent, you have granular control over various aspects of the payment, such as the ability to authorize and capture payments separately or handle partial payments. You can also leverage features like 3D Secure authentication, subscription billing, and Radar fraud detection seamlessly within the PaymentIntent flow.

In contrast, the Tokens and Charges API has a more straightforward approach without as many advanced features or options for customization. It may be suitable for simpler payment scenarios that don’t require complex payment management or additional features.

Future Compatibility: Stripe has been focusing on enhancing and evolving the PaymentIntent API as the primary method for payment processing. While the Tokens and Charges API is still supported, Stripe encourages developers to transition to the PaymentIntent API for new integrations and to take advantage of its advanced features and improvements.

Overall, PaymentIntent provides a more comprehensive and flexible solution for handling payments, offering greater control, advanced features, and a streamlined workflow compared to the Tokens and Charges API. It is the recommended approach for new integrations and more complex payment scenarios.

This guide shows how a Custom payment flow can be integrated https://stripe.com/docs/payments/quickstart?client=react&lang=node

Option 2. Continue to use Tokens and Charges API

It is possible to implement 3D Secure authentication with the Tokens and Charges API approach. Here’s a general outline of how you can incorporate 3D Secure into the process:

  1. Collect Payment Details: Collect the customer’s payment information securely on your website or application. This typically involves collecting the card details (e.g., card number, expiration date, CVC) using a payment form.
  2. Create a Token: Use the collected payment details to create a token using the Stripe.js library or Stripe Elements. This token represents the customer’s payment information without exposing sensitive card details to your server.
  3. Create a Charge: Once you have the token, you can create a charge object using the Charges API. Specify the amount, currency, and the token ID in the charge request. This will initiate the payment process.
  4. Check 3D Secure Support: Before making the charge request, you can check if the customer’s card supports 3D Secure by retrieving the card information from the token. You can use the card’s three_d_secure property to determine if the card is enrolled in 3D Secure.
  5. Handle 3D Secure Flow: If the card supports 3D Secure, you can initiate the authentication process by redirecting the customer to the authentication page hosted by the card issuer. You need to provide the necessary information, such as the charge amount, currency, and a return URL where the customer will be redirected after authentication.
  6. Handle Authentication Result: After the customer completes the 3D Secure authentication, they will be redirected back to your specified return URL. You can receive the result of the authentication in the query parameters or by making an API request to retrieve the charge object. You can then proceed with capturing the payment or handling the result based on the authentication outcome.

Get the three_d_secure from the card token

const stripe = require('stripe')('YOUR_SECRET_KEY');

stripe.tokens.retrieve('TOKEN_ID', function(err, token) {
if (err) {
// Handle error
console.error(err);
} else {
// Access card information from the token object
const card = token.card;
const three_d_secure = card.three_d_secure; // 3D secure

// Do something with the card information
console.log(three_d_secure);
}
});

Redirect if 3D Secure is supported

This step seems to be most complicated since I would have to know where to redirect each type of card, and which URL to construct. Then handle redirection, and then handle response from the card holder website which may be different. For this reason going forward with option 2 isn’t really an option at this point.

Miagration of the PaymentIntent development

  1. Update the GraphQL api to handle the creation of the Payment Intent.
const { secretsByEnv } = require("../Configs.node")
const stripe = require("stripe")(secretsByEnv().SECRET_STRIPE_TOKEN)

async function CreatePaymentIntent({ currency, amount }) {
if (!currency || !amount) {
console.error({ currency, amount })
return {
error: "Missing sale data",
}
}
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: currency,
automatic_payment_methods: {
enabled: true,
},
})
return paymentIntent?.client_secret
}
module.exports = CreatePaymentIntent

2. Now that we have a payment intent let’s update the payment form. Our form is custom built and generally we like it, but perhaps the stripe form is even more optimized for increased conversion and even supports some addtional payment methods we don’t currently have.

 npm install --save @stripe/react-stripe-js @stripe/stripe-js

3. Add the request to the new back-end endpoint to receive the PaymentIntent request and pass is to Elements. Make sure you are using the keys of the correct environments.

 import React, { useState, useEffect } from "react";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";

import CheckoutForm from "./CheckoutForm";
import "./App.css";

// Make sure to call loadStripe outside of a component’s render to avoid
// recreating the Stripe object on every render.
// This is your test publishable API key.
const stripePromise = loadStripe("pk_test_51IcI8sHyolb1mKLat52GHTRxDFhUqtVPmnr2YBomswIlpk9ebyND3fqiYB7cWwk4eA7XGKr4iq6RnZzyrqxTTOGJ00h7UVugZP");

export default function App() {
const [clientSecret, setClientSecret] = useState("");

useEffect(() => {
// Create PaymentIntent as soon as the page loads
fetch("/create-payment-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: [{ id: "xl-tshirt" }] }),
})
.then((res) => res.json())
.then((data) => setClientSecret(data.clientSecret));
}, []);

const appearance = {
theme: 'stripe',
};
const options = {
clientSecret,
appearance,
};

return (
<div className="App">
{clientSecret && (
<Elements options={options} stripe={stripePromise}>
<CheckoutForm />
</Elements>
)}
</div>
);
}

4. Implement the checkout form

import React, { useEffect, useState } from "react";
import {
PaymentElement,
useStripe,
useElements
} from "@stripe/react-stripe-js";

export default function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();

const [message, setMessage] = useState(null);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
if (!stripe) {
return;
}

const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);

if (!clientSecret) {
return;
}

stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
switch (paymentIntent.status) {
case "succeeded":
setMessage("Payment succeeded!");
break;
case "processing":
setMessage("Your payment is processing.");
break;
case "requires_payment_method":
setMessage("Your payment was not successful, please try again.");
break;
default:
setMessage("Something went wrong.");
break;
}
});
}, [stripe]);

const handleSubmit = async (e) => {
e.preventDefault();

if (!stripe || !elements) {
// Stripe.js hasn't yet loaded.
// Make sure to disable form submission until Stripe.js has loaded.
return;
}

setIsLoading(true);

const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: "http://localhost:3000",
},
});

// This point will only be reached if there is an immediate error when
// confirming the payment. Otherwise, your customer will be redirected to
// your `return_url`. For some payment methods like iDEAL, your customer will
// be redirected to an intermediate site first to authorize the payment, then
// redirected to the `return_url`.
if (error.type === "card_error" || error.type === "validation_error") {
setMessage(error.message);
} else {
setMessage("An unexpected error occurred.");
}

setIsLoading(false);
};

const paymentElementOptions = {
layout: "tabs"
}

return (
<form id="payment-form" onSubmit={handleSubmit}>
<PaymentElement id="payment-element" options={paymentElementOptions} />
<button disabled={isLoading || !stripe || !elements} id="submit">
<span id="button-text">
{isLoading ? <div className="spinner" id="spinner"></div> : "Pay now"}
</span>
</button>
{/* Show any error or success messages */}
{message && <div id="payment-message">{message}</div>}
</form>
);
}

5. After a successful payment the client will be redirected to a provided url and append some state

http://localhost:8000/booking/payment-checkout/aNIDM2JnW8XKHTNItDr0K/?payment_intent=pi_3NTkcGHyolb1mKLa0iMWH1R0&payment_intent_client_secret=pi_3NTkcGHyolb1mKLa0iMWH1R0_secret_Mbz6EjGI7gYsYQQEOryAucm8Q&redirect_status=succeeded

6. To effectively handle events in the payment process and ensure a reliable workflow, Stripe recommends using webhooks. Webhooks allow you to receive notifications from Stripe when specific events occur, such as when a payment is successful or when it fails. This enables you to take appropriate actions based on these events, such as sending order confirmations, logging sales, or initiating shipping workflows.

Instead of relying on a callback from the client, which may be unreliable if the customer closes the browser or manipulates the response, using webhooks ensures that you capture and process events consistently. By listening for asynchronous events, you can support various payment methods seamlessly within a single integration.

Stripe specifically recommends handling the following events related to the payment intent:

  1. payment_intent.succeeded: This event indicates that a payment was successfully processed and completed.
  2. payment_intent.processing: This event represents a payment that is being processed and is not yet completed.
  3. payment_intent.payment_failed: This event occurs when a payment attempt fails.

You can set up webhooks either using the Dashboard webhook tool provided by Stripe or by following their webhook guide. By implementing webhooks, you can enhance the functionality and reliability of your payment integration by responding to events in a timely and automated manner.

6. And finally we want to handle the UI to show the correct state when a the redirection after 3D occurs. It will redirect to the provided url with the parameters redirect_status=succeeded or redirect_status=failed.

--

--