Skipping 3D Secure (SCA) challenge handling means your checkout silently fails for European customers (where SCA is legally mandated under PSD2) and for any card issuer that requires additional authentication. More critically, omitting the requires_action status check means your code can mark an order as paid after receiving the initial PaymentIntent — before the card issuer has actually authorized the charge. The result: inventory is decremented, fulfillment is triggered, and the customer receives their order, but the actual charge later fails when the 3DS challenge times out. PCI-DSS 4.0 Req 8.4 covers multi-factor authentication requirements; OWASP A07 (Identification and Authentication Failures) captures incomplete authentication flows.
High because ignoring the `requires_action` state causes your backend to mark orders as paid before authentication completes, enabling fulfillment of unpaid orders on 3DS-required cards.
Use stripe.confirmPayment() with redirect: 'if_required' and always inspect the returned paymentIntent.status before updating your order state.
// Client component — full SCA-aware confirmation
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/checkout/complete`,
},
redirect: 'if_required',
})
if (error) {
setError(error.message ?? 'Payment failed')
} else if (paymentIntent?.status === 'succeeded') {
await confirmOrder(paymentIntent.id) // only now mark the order as paid
} else if (paymentIntent?.status === 'requires_action') {
// User is being redirected for 3DS — confirmOrder happens after return_url redirect
}
Create PaymentIntents server-side with automatic_payment_methods: { enabled: true } so Stripe requests 3DS when the issuer requires it.
ID: ecommerce-payment-security.client-side-handling.three-d-secure-sca
Severity: high
What to look for: Search for the payment confirmation step in client-side checkout code. For Stripe, look for stripe.confirmPayment(), stripe.confirmCardPayment(), or stripe.handleNextAction() calls. Count all payment confirmation call sites and for each, verify that the code awaits and handles the requires_action or requires_source_action status. Verify that the code does not assume payment success immediately after the initial charge call without checking for required actions. Also check that the server-side PaymentIntent creation uses automatic_payment_methods or explicitly sets payment_method_options.card.request_three_d_secure in at least 1 location.
Pass criteria: The payment confirmation flow handles the requires_action state by calling the provider's next-action handler (e.g., stripe.handleNextAction(), stripe.confirmCardPayment() in a full flow). No more than 0 payment confirmation paths should skip the 3DS challenge handling. The code properly awaits authentication before marking a payment as complete.
Fail criteria: The checkout code creates a PaymentIntent and immediately marks the order as paid without awaiting the full confirmation cycle that includes 3DS challenge handling. Or stripe.confirmPayment() is called but errors from it are silently swallowed.
Skip (N/A) when: The project uses Stripe Hosted Checkout or PayPal Standard where SCA is handled entirely by the provider's hosted page, not your code.
Detail on fail: Describe the missing or incorrect implementation. Example: "components/checkout/PaymentForm.tsx calls stripe.createPaymentMethod() and immediately POSTs to /api/confirm without calling stripe.confirmPayment() or handling requires_action status — 3DS challenges will silently fail"
Remediation: Implement the complete Stripe Payment Intents flow with SCA support:
// In your client component
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/checkout/complete`,
},
redirect: 'if_required', // Only redirect for 3DS, stay on page otherwise
})
if (error) {
// Show error to user — e.g., card declined or authentication failed
setError(error.message)
} else if (paymentIntent?.status === 'succeeded') {
// Payment complete — now update your order record
await confirmOrder(paymentIntent.id)
}
For the server side, create the PaymentIntent with SCA-appropriate settings:
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
automatic_payment_methods: { enabled: true },
})