Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added fingerprint check validation for card and us bank #4207

Merged
merged 11 commits into from
Nov 1, 2024
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* [Fixed] Fixed an animation glitch when dismissing PaymentSheet in React Native.
* [Fixed] Fixed an issue with FlowController in vertical layout where the payment method could incorrectly be preserved across a call to `update` when it's no longer valid.
* [Fixed] Fixed a potential deadlock when `paymentOption` is accessed from Swift concurrency.

* [Added] Added a check to `PaymentSheetDeferredValidator.validatePaymentMethod` for mismatched card or US bank account fingerprints
joyceqin-stripe marked this conversation as resolved.
Show resolved Hide resolved

## 23.32.0 2024-10-21
### PaymentSheet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ import Foundation
case paymentSheetFormInteracted = "mc_form_interacted"
case paymentSheetFormCompleted = "mc_form_completed"
case paymentSheetCardNumberCompleted = "mc_card_number_completed"
case paymentSheetDeferredIntentPaymentMethodIdMismatch = "mc_deferred_intent_payment_method_id_mismatch"
case paymentSheetDeferredIntentPaymentMethodMismatch = "mc_deferred_intent_payment_method_mismatch"

// MARK: - v1/elements/session
case paymentSheetElementsSessionLoadFailed = "mc_elements_session_load_failed"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ extension PaymentSheet {
}
} else {
// 4b. Server-side confirmation
try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: paymentIntent.paymentMethod, paymentMethod: paymentMethod)
try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: paymentIntent.paymentMethod, paymentMethod: paymentMethod)
paymentHandler.handleNextAction(
for: paymentIntent,
with: authenticationContext,
Expand Down Expand Up @@ -110,7 +110,7 @@ extension PaymentSheet {
}
} else {
// 4b. Server-side confirmation
try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod)
try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod)
paymentHandler.handleNextAction(
for: setupIntent,
with: authenticationContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation
import StripePayments
@_spi(STP) import StripeCore
@_spi(STP) import StripePayments

struct PaymentSheetDeferredValidator {
/// Note: We don't validate amount (for any payment method) because there are use cases where the amount can change slightly between PM collection and confirmation.
Expand All @@ -27,7 +28,7 @@ struct PaymentSheetDeferredValidator {
guard paymentIntent.captureMethod == captureMethod else {
throw PaymentSheetError.deferredIntentValidationFailed(message: "Your PaymentIntent captureMethod (\(paymentIntent.captureMethod)) does not match the PaymentSheet.IntentConfiguration amount (\(captureMethod)).")
}
try validatePaymentMethodId(intentPaymentMethod: paymentIntent.paymentMethod, paymentMethod: paymentMethod)
try validatePaymentMethod(intentPaymentMethod: paymentIntent.paymentMethod, paymentMethod: paymentMethod)
/*
Manual confirmation is only available using FlowController because merchants own the final step of confirmation.
Showing a successful payment in the complete flow may be misleading when merchants still need to do a final confirmation which could fail e.g., bad network
Expand All @@ -36,7 +37,7 @@ struct PaymentSheetDeferredValidator {
throw PaymentSheetError.deferredIntentValidationFailed(message: "Your PaymentIntent confirmationMethod (\(paymentIntent.confirmationMethod)) can only be used with PaymentSheet.FlowController.")
}
}

static func validate(setupIntent: STPSetupIntent,
intentConfiguration: PaymentSheet.IntentConfiguration,
paymentMethod: STPPaymentMethod) throws {
Expand All @@ -46,24 +47,55 @@ struct PaymentSheetDeferredValidator {
guard setupIntent.usage == setupFutureUsage else {
throw PaymentSheetError.deferredIntentValidationFailed(message: "Your SetupIntent usage (\(setupIntent.usage)) does not match the PaymentSheet.IntentConfiguration setupFutureUsage (\(String(describing: setupFutureUsage))).")
}
try validatePaymentMethodId(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod)
try validatePaymentMethod(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod)
}
static func validatePaymentMethodId(intentPaymentMethod: STPPaymentMethod?, paymentMethod: STPPaymentMethod) throws {

static func validatePaymentMethod(intentPaymentMethod: STPPaymentMethod?, paymentMethod: STPPaymentMethod) throws {
guard let intentPaymentMethod = intentPaymentMethod else { return }
guard intentPaymentMethod.stripeId == paymentMethod.stripeId else {
if intentPaymentMethod.type == paymentMethod.type {
// Payment methods of type card and us_bank_account can be cloned, leading to mismatched pm ids, but their fingerprints should still match
switch paymentMethod.type {
case .card:
try validateFingerprint(intentFingerprint: intentPaymentMethod.card?.fingerprint, fingerprint: paymentMethod.card?.fingerprint)
return
case .USBankAccount:
try validateFingerprint(intentFingerprint: intentPaymentMethod.usBankAccount?.fingerprint, fingerprint: paymentMethod.usBankAccount?.fingerprint)
return
default:
break
}
}
let errorMessage = """
\nThere is a mismatch between the payment method ID on your Intent: \(intentPaymentMethod.stripeId) and the payment method passed into the `confirmHandler`: \(paymentMethod.stripeId).

To resolve this issue, you can:
1. Create a new Intent each time before you call the `confirmHandler`, or
2. Update the existing Intent with the desired `paymentMethod` before calling the `confirmHandler`.
"""
let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage), additionalNonPIIParams: ["field": "payment method ID"])
STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic)
throw PaymentSheetError.deferredIntentValidationFailed(message: errorMessage)
}
}

static func validateFingerprint(intentFingerprint: String?, fingerprint: String?) throws {
guard let intentFingerprint = intentFingerprint else { return }
guard let fingerprint = fingerprint else { return }
guard intentFingerprint == fingerprint else {
let errorMessage = """
\nThere is a mismatch between the fingerprint of the payment method on your Intent: \(intentFingerprint) and the fingerprint of the payment method passed into the `confirmHandler`: \(fingerprint).

To resolve this issue, you can:
1. Create a new Intent each time before you call the `confirmHandler`, or
2. Update the existing Intent with the desired `paymentMethod` before calling the `confirmHandler`.
"""
let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodIdMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage))
let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage), additionalNonPIIParams: ["field": "fingerprint"])
STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic)
throw PaymentSheetError.deferredIntentValidationFailed(message: errorMessage)
}
}

}

// MARK: - Validation helpers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
paymentMethodJson["id"] = testCard.stripeId
let testCardPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson)

XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: testCardPi.paymentMethod,
XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardPi.paymentMethod,
paymentMethod: testCard))
}

Expand All @@ -92,7 +92,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
guard let intentPaymentMethod = testCardPi.paymentMethod else {
return
}
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: testCardPi.paymentMethod,
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardPi.paymentMethod,
paymentMethod: testUSBankAccount)) { error in
XCTAssertEqual("\(error)", """
An error occurred in PaymentSheet. \nThere is a mismatch between the payment method ID on your Intent: \(intentPaymentMethod.stripeId) and the payment method passed into the `confirmHandler`: \(testUSBankAccount.stripeId).
Expand All @@ -103,14 +103,14 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
""")
}
let analyticEvent = STPAnalyticsClient.sharedClient._testLogHistory.last
XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodIdMismatch.rawValue)
XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodMismatch.rawValue)
XCTAssertNotNil(analyticEvent?["error_code"] as? String)
}

func testPaymentIntentNilPaymentMethod() throws {
let testCard = STPPaymentMethod._testCard()
let nilPaymentMethodPi = STPFixtures.makePaymentIntent()
XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: nilPaymentMethodPi.paymentMethod,
XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: nilPaymentMethodPi.paymentMethod,
paymentMethod: testCard))
}

Expand All @@ -120,7 +120,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
paymentMethodJson["id"] = testCard.stripeId
let testCardSi = STPFixtures.makeSetupIntent(paymentMethodJson: paymentMethodJson)

XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: testCardSi.paymentMethod,
XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardSi.paymentMethod,
paymentMethod: testCard))
}

Expand All @@ -133,7 +133,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
guard let intentPaymentMethod = testCardSi.paymentMethod else {
return
}
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: testCardSi.paymentMethod,
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardSi.paymentMethod,
paymentMethod: testUSBankAccount)) { error in
XCTAssertEqual("\(error)", """
An error occurred in PaymentSheet. \nThere is a mismatch between the payment method ID on your Intent: \(intentPaymentMethod.stripeId) and the payment method passed into the `confirmHandler`: \(testUSBankAccount.stripeId).
Expand All @@ -144,14 +144,14 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
""")
}
let analyticEvent = STPAnalyticsClient.sharedClient._testLogHistory.last
XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodIdMismatch.rawValue)
XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodMismatch.rawValue)
XCTAssertNotNil(analyticEvent?["error_code"] as? String)
}

func testSetupIntentNilPaymentMethod() throws {
let testCard = STPPaymentMethod._testCard()
let nilPaymentMethodSi = STPFixtures.makeSetupIntent()
XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: nilPaymentMethodSi.paymentMethod,
XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: nilPaymentMethodSi.paymentMethod,
paymentMethod: testCard))
}

Expand Down
Loading