Skip to content

Commit

Permalink
Added fingerprint check validation for card and us bank (#4207)
Browse files Browse the repository at this point in the history
## Summary
<!-- Simple summary of what was changed. -->
Added fingerprint checking logic in deferred validation for card and
us_bank_account if PM ids don't match

## Motivation
<!-- Why are you making this change? If it's for fixing a bug, if
possible, please include a code snippet or example project that
demonstrates the issue. -->
#4195
https://jira.corp.stripe.com/browse/RUN_MOBILESDK-3670

## Testing
<!-- How was the code tested? Be as specific as possible. -->
Unit tests with different pm ids but matching or mismatching fingerprints for card and bank account

## Changelog
<!-- Is this a notable change that affects users? If so, add a line to
`CHANGELOG.md` and prefix the line with one of the following:
    - [Added] for new features.
    - [Changed] for changes in existing functionality.
    - [Deprecated] for soon-to-be removed features.
    - [Removed] for now removed features.
    - [Fixed] for any bug fixes.
    - [Security] in case of vulnerabilities.
-->
  • Loading branch information
joyceqin-stripe authored Nov 1, 2024
1 parent 1869bbe commit a1f5f95
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 18 deletions.
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.

* [Fixed] Fixed deferred intent validation to handle cloned payment methods ([#4195](https://github.com/stripe/stripe-ios/issues/4195)

## 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,94 @@ 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 testPaymentIntentMatchedCardFingerprint() throws {
let testCard = STPPaymentMethod._testCard()
var paymentMethodJson = STPPaymentMethod.paymentMethodJson
paymentMethodJson["id"] = "pm_mismatch_id"
paymentMethodJson["card"] = testCard.card
let testCardPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson)
XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardPi.paymentMethod,
paymentMethod: testCard))
}

func testPaymentIntentMismatchedCardFingerprint() throws {
let testCard = STPPaymentMethod._testCard()
var paymentMethodJson = STPPaymentMethod.paymentMethodJson
paymentMethodJson["id"] = "pm_mismatch_id"
paymentMethodJson["card"] = ["fingerprint": "mismatch_fingerprint"]
let testCardPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson)
guard let intentPaymentMethod = testCardPi.paymentMethod else {
return
}
guard let intentPaymentMethodFingerprint = intentPaymentMethod.card?.fingerprint else {
return
}
guard let testCardFingerprint = testCard.card?.fingerprint else {
return
}
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardPi.paymentMethod,
paymentMethod: testCard)) { error in
XCTAssertEqual("\(error)", """
An error occurred in PaymentSheet. \nThere is a mismatch between the fingerprint of the payment method on your Intent: \(intentPaymentMethodFingerprint) and the fingerprint of the payment method passed into the `confirmHandler`: \(testCardFingerprint).
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 analyticEvent = STPAnalyticsClient.sharedClient._testLogHistory.last
XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodMismatch.rawValue)
XCTAssertNotNil(analyticEvent?["error_code"] as? String)
}

func testPaymentIntentMatchedUSBankAccountFingerprint() throws {
let testUSBankAccount = STPPaymentMethod._testUSBankAccount()
var paymentMethodJson = STPPaymentMethod.usBankAccountJson
paymentMethodJson["id"] = "pm_mismatch_id"
paymentMethodJson["us_bank_account"] = testUSBankAccount.usBankAccount
let testUSBankAccountPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson)
XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testUSBankAccountPi.paymentMethod,
paymentMethod: testUSBankAccount))
}

func testPaymentIntentMismatchedUSBankAccountFingerprint() throws {
let testUSBankAccount = STPPaymentMethod._testUSBankAccount()
var paymentMethodJson = STPPaymentMethod.usBankAccountJson
paymentMethodJson["id"] = "pm_mismatch_id"
paymentMethodJson["us_bank_account"] = ["fingerprint": "mismatch_fingerprint"]
let testUSBankAccountPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson)
guard let intentPaymentMethod = testUSBankAccountPi.paymentMethod else {
return
}
guard let intentPaymentMethodFingerprint = intentPaymentMethod.card?.fingerprint else {
return
}
guard let testUSBankAccountFingerprint = testUSBankAccount.usBankAccount?.fingerprint else {
return
}
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testUSBankAccountPi.paymentMethod,
paymentMethod: testUSBankAccount)) { error in
XCTAssertEqual("\(error)", """
An error occurred in PaymentSheet. \nThere is a mismatch between the fingerprint of the payment method on your Intent: \(intentPaymentMethodFingerprint) and the fingerprint of the payment method passed into the `confirmHandler`: \(testUSBankAccountFingerprint).
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 analyticEvent = STPAnalyticsClient.sharedClient._testLogHistory.last
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 +200,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 +213,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 +224,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
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ extension STPPaymentMethod {
"card": [
"last4": "4242",
"brand": "visa",
"fingerprint": "B8XXs2y2JsVBtB9f",
],
])!
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,31 @@ extension STPPaymentMethod {
"card": [
"last4": "4242",
"brand": "visa",
"fingerprint": "B8XXs2y2JsVBtB9f",
],
]
}

static var usBankAccountJson: [String: Any] {
return [
"id": "pm_123",
"type": "us_bank_account",
"us_bank_account": [
"account_holder_type": "individual",
"account_type": "checking",
"bank_name": "STRIPE TEST BANK",
"fingerprint": "ickfX9sbxIyAlbuh",
"last4": "6789",
"networks": [
"preferred": "ach",
"supported": [
"ach",
],
] as [String: Any],
"routing_number": "110000000",
] as [String: Any],
]
}

static var paymentMethodsJson: [String: Any] = [
"data": [
Expand Down

0 comments on commit a1f5f95

Please sign in to comment.