Adyen / adyen-android

Adyen Android Drop-in and Components
https://docs.adyen.com/checkout/android
MIT License
126 stars 66 forks source link

Android Drop-in v5.x Unable to Complete 3DS2 Authentication when challengeToken is requested after fingerprint #1618

Closed timatpam closed 5 months ago

timatpam commented 6 months ago

Description: We have encountered a critical issue with the 3DS2 authentication process in our Android application after upgrading from Drop-in v4.x to Drop-in v5.x. The problem arises specifically during the 3DS2 fingerprint flow when a challengeToken is required after the fingerprint step. Notably, the same backend configuration works without issues on iOS and with the Android Drop-in v4.x, suggesting that the problem is isolated to the Drop-in v5.x implementation.

Steps to Reproduce:

  1. Initiate a payment using the following Mastercard details:
        Card Number: 5454 5454 5454 5454
        Expiry Date: 03/2030
        CVV: 737
  2. Observe that the Drop-in fails and logs the following error message in Logcat: "Failed to make challenge, missing reference to initial transaction."

Technical Details:

During payment initialization in DropInService's onSubmit, we send a request to our backend and receive a fingerprintToken and paymentData, convert it to DropInServiceResult.Action(action) with a Threeds2FingerprintAction:

Threeds2FingerprintAction(
type=threeDS2Fingerprint,
paymentData=...,
paymentMethodType=scheme,
token=...)

Drop-in processes this action and triggers again onAdditionalDetails in the service, where we forward the fingerprintResult from Drop-in to our backend. Our backend responds with a 3ds2 challengeToken, which we convert to a Threeds2ChallengeAction and pass to DropInServiceResult.Action(action) with

Threeds2ChallengeAction(
type=threeDS2Challenge,
paymentData=...,
paymentMethodType=scheme,
token=...)

At this point, Drop-in fails to handle this action and logs the error "Failed to make challenge, missing reference to initial transaction." Debugging reveals that Drop-in recreates the DefaultAdyen3DS2Delegate upon receiving the second action with null currentTransaction reference.

The log output from Drop-in is:

CO.DropInActivity                    D  requestPaymentsCall
CO.DropInViewModel                   D  Payment amount already set: Amount(currency=EUR, value=15895)
CO.PaymentDropInService              D  requestPaymentsCall
CO.PaymentDropInService              D  dispatching DropInServiceResult
CO.DropInActivity                    D  handleDropInServiceResult - Action
CO.DropInActivity                    D  showActionDialog
CO.CardComponent                     D  onCleared
CO.ObserverContainer                 D  cleaning up existing observer
CO.DefaultGenericActionDelegate      D  onCleared
CO.ObserverContainer                 D  cleaning up existing observer
CO.ActionComponentDialogFragment     D  onCreate
CO.ActionComponentDialogFragment     D  onViewCreated
CO.DefaultGenericActionDelegate      D  initialize
CO.AdyenComponentView                I  Component view type is null, ignoring.
CO.DefaultGenericActionDelegate      D  Created delegate of type DefaultAdyen3DS2Delegate
CO.DefaultGenericActionDelegate      D  Observing details
CO.DefaultGenericActionDelegate      D  Observing exceptions
CO.DefaultGenericActionDelegate      D  Observing view flow
CO.DefaultAdyen3DS2Delegate          D  identifyShopper - submitFingerprintAutomatically: false
CO.StandaloneCoroutine               D  initialize 3DS2 SDK
CO.DefaultAdyen3DS2Delegate          D  create transaction
CO.ActionComponentDialogFragment     D  onActionComponentDataChanged
CO.DropInActivity                    D  requestDetailsCall
CO.PaymentDropInService              D  requestDetailsCall
CO.PaymentDropInService              D  dispatching DropInServiceResult
CO.DropInActivity                    D  handleDropInServiceResult - Action
CO.DropInActivity                    D  showActionDialog
CO.GenericActionComponent            D  onCleared
CO.DefaultGenericActionDelegate      D  onCleared
CO.ObserverContainer                 D  cleaning up existing observer
CO.ActionComponentDialogFragment     D  onCreate
CO.ActionComponentDialogFragment     D  onViewCreated
CO.DefaultGenericActionDelegate      D  initialize
CO.AdyenComponentView                I  Component view type is null, ignoring.
CO.DefaultGenericActionDelegate      D  Created delegate of type DefaultAdyen3DS2Delegate
CO.DefaultGenericActionDelegate      D  Observing details
CO.DefaultGenericActionDelegate      D  Observing exceptions
CO.DefaultGenericActionDelegate      D  Observing view flow
CO.DefaultAdyen3DS2Delegate          D  challengeShopper
CO.ActionComponentDialogFragment     D  onError
CO.ActionComponentDialogFragment     E  Failed to make challenge, missing reference to initial transaction.
CO.DropInActivity                    D  showError - message: There was an error while processing your payment. Please try again later.

It appears that there is a problem with state retention between the fingerprint and challenge phases in the Drop-in v5.x, which prevents the successful completion of the 3DS2 authentication process.

We would greatly appreciate your assistance in investigating and fixing this issue. Thank you for your support.

jreij commented 6 months ago

Hi @timatpam, thanks for reaching out and providing logs. I need a few more details to investigate this:

timatpam commented 6 months ago

Hi @jreij.

    override fun onSubmit(state: PaymentComponentState<*>) {
        launch(Dispatchers.IO) {
            sendResult(
                interactor.onPaymentsCallRequested(state)
            )
        }
    }

    override fun onAdditionalDetails(actionComponentData: ActionComponentData) {
        launch(Dispatchers.IO) {
            sendResult(
                interactor.makeDetailsCall(actionComponentData)
            )
        }
    }

In the interactor we perform a blocking request to our backend and map the response to DropInServiceResult:

fun map(param: Payment3dsResponse): Action =
        when {
            param.fingerprintToken?.isNotBlank().orFalse() ->
                Threeds2FingerprintAction(
                    type = Threeds2FingerprintAction.ACTION_TYPE,
                    paymentData = param.paymentData.orEmpty(),
                    paymentMethodType = "scheme",
                    token = param.fingerprintToken.orEmpty(),
                )

            param.challengeToken?.isNotBlank().orFalse() ->
                Threeds2ChallengeAction(
                    type = Threeds2ChallengeAction.ACTION_TYPE,
                    paymentData = param.paymentData.orEmpty(),
                    paymentMethodType = "scheme",
                    token = param.challengeToken.orEmpty(),
                )

            param.issuerUrl?.isNotBlank().orFalse() ->
                RedirectAction(
                    type = RedirectAction.ACTION_TYPE,
                    paymentData = param.paymentData.orEmpty(),
                    paymentMethodType = "scheme",
                    url = param.issuerUrl.orEmpty(),
                    method = null,
                )

            else ->
                throw IllegalArgumentException("Unable to determine action type")
        }
jreij commented 6 months ago

@timatpam thanks for providing the answers. I noticed that you are using the Threeds2FingerprintAction and Threeds2ChallengeAction in combination with API v71. Generally these actions are used with API v66 and below, with API v71 we use Threeds2Action.

Can you check the response received on your backend? Specifically the action object in the responses of /payments and /payments/details.

timatpam commented 5 months ago

The issue was solved by switching of the mapping of both fingerprintToken and challengeToken to Threeds2Action.