Search code examples
c#.net-standardbraintree.net-standard-2.0braintree-sandbox

Is it possible to make 3D Secure payments through a Braintree sandbox environment with C#?


I have written code in .NET Standard 2.0 to make payments through Braintree. The code uses the Braintree 5.2.0 NuGet package. I intend to exclusively make 3D Secure payments when the code is used against a Braintree production account. I have written a integration test that creates a customer, creates a payment method for that customer, then makes a payment against that payment method using the Token that was generated.

The code to create a customer is:

public async Task<string> SeedCustomer(IBraintreeConfiguration braintreeConfiguration)
{
    BraintreeGateway _braintreeGateway = new BraintreeGateway(
        braintreeConfiguration.Environment,
        braintreeConfiguration.MerchantId,
        braintreeConfiguration.PublicKey,
        braintreeConfiguration.PrivateKey
    );

    CustomerRequest request = new CustomerRequest
    {
        Email = BraintreeTestConstants.Email,
        CustomFields = new Dictionary<string, string>
        {
            {"account_id", BraintreeTestConstants.AccountId}
        }
    };

    string braintreeCustomerId = 
        (await _braintreeGateway.Customer.CreateAsync(request)).Target.Id;

    return braintreeCustomerId;
}

The code to create a payment method is:

PaymentMethodRequest paymentMethodRequest = new PaymentMethodRequest
{
    PaymentMethodNonce = nonce,
    CustomerId = customerId,
    BillingAddress = new PaymentMethodAddressRequest
    {
        FirstName = BraintreeTestConstants.BillingName,
        Locality = BraintreeTestConstants.City,
        Company = BraintreeTestConstants.CompanyName,
        CountryCodeAlpha2 = BraintreeTestConstants.Country,
        ExtendedAddress = BraintreeTestConstants.ExtendedAddress,
        Region = BraintreeTestConstants.State,
        StreetAddress = BraintreeTestConstants.StreetAddress,
        PostalCode = BraintreeTestConstants.Zip,
    },
    Options = new PaymentMethodOptionsRequest
    {
        VerifyCard = true
    }
};

Result<PaymentMethod> result = 
    await _braintreeGateway.PaymentMethod.CreateAsync(paymentMethodRequest);

return _mapper.Map<AddPaymentMethodResultModel>((CreditCard)result.Target);

CreditCard.Token is mapped to AddPaymentMethodResultModel.CardId.

The code to make a payment is:

bool useThreeDSecure = true;

TransactionRequest transactionRequest = new TransactionRequest
{
    Amount = Amount,
    PaymentMethodToken = CardId,
    MerchantAccountId = string.IsNullOrWhiteSpace(MerchantAccountId) ? null : MerchantAccountId,
    TransactionSource = string.IsNullOrWhiteSpace(TransactionSource) ? null : TransactionSource,
    Options = new TransactionOptionsRequest
    {
        SubmitForSettlement = true,
        ThreeDSecure = new TransactionOptionsThreeDSecureRequest
        {
            Required = useThreeDSecure
        }
    }
};

if (Address != null)
{
    transactionRequest.BillingAddress = new AddressRequest
    {
        Company = Address.CompanyName,
        FirstName = Address.BillingName,
        StreetAddress = Address.StreetAddress,
        ExtendedAddress = Address.ExtendedAddress,
        Locality = Address.City,
        Region = Address.State,
        PostalCode = Address.Zip,
        CountryCodeAlpha2 = Address.Country
    };
}

Result<Transaction> result =
    await _braintreeGateway.Transaction.SaleAsync(transactionRequest);

When I execute the test against a Braintree sandbox environment with TransactionRequest.Options.ThreeDSecure.Required set to false, a payment is successful.

When I execute the test against a Braintree sandbox environment TransactionRequest.Options.ThreeDSecure.Required set to true, a payment fails with a Result.Message of Gateway Rejected: three_d_secure.

I was wondering whether it is possible to make a successful payment through a Braintree sandbox environment with TransactionRequest.Options.ThreeDSecure.Required set to true. I have tried unsuccessfully using a payment method created through the NuGet package with a PaymentMethodNonce of fake-three-d-secure-visa-full-authentication-nonce.

I would like to prove that a 3D Secure payment can be made through a Braintree sandbox environment to have confidence that the code will work against a Braintree production environment.


Solution

  • There is a client-side piece that must be implemented to validate a vaulted card before it can be used to make a 3D Secure payment.

    There are two approaches that can be taken to achieve this: drop-in UI or hosted fields. Drop-in UI provides a form with some styling, while hosted field provides more control over the appearance of your form. I did not require the fields to be visible, as I could manage the data through JavaScript alone, so I used hosted fields.

    First, one must generate a client token, as well as a nonce and bin representing the vaulted card on the server-side. The code for achieving this was:

    public async Task<string> GetBraintreeClientToken()
    {
        string clientToken =
            await _braintreeChargeService.GetClientToken();
    
        return clientToken;
    }
    
    public async Task<BraintreeValidationDataModel> 
        GetValidationDataForVaultedToken(string paymentMethodToken)
    {
        Result<PaymentMethodNonce> result = await 
            _braintreeGateway.PaymentMethodNonce.CreateAsync(paymentMethodToken);
    
        PaymentMethodNonce paymentMethodNonce =
            result.Target;
    
        BraintreeValidationDataModel validationData = new 
            BraintreeValidationDataModel
        {
            Nonce = paymentMethodNonce.Nonce,
            Bin = paymentMethodNonce.Details.Bin
        };
    
        return validationData;
    }
    

    Next, one must use this information on the client-side to generate a nonce that is valid for a 3D Secure payment. To instantiate the Braintree components:

    function setupComponents(clientToken) {
        return Promise.all([
            braintree.threeDSecure.create({
                authorization: clientToken,
                version: 2
            }),
            braintree.dataCollector.create({
                authorization: clientToken
            })
        ]);
    }
    

    To generate a 3D Secure nonce:

    return setupComponents(threeDsValidation.clientToken).then(function (result) {
        var threeDSecure =
            result[0];
    
        if (threeDSecure === null ||
            threeDSecure === undefined) {
            return null;
        }
    
        var dataCollector =
            result[1];
    
        if (dataCollector === null ||
            dataCollector === undefined ||
            dataCollector.deviceData == null) {
            return null;
        }
    
        threeDsValidationNew.deviceData =
            dataCollector.deviceData;
    
        var threeDSecureParameters =
            getThreeDsSecureParameters(threeDsValidationNew);
    
        return threeDSecure.verifyCard(threeDSecureParameters);
    })
    .then(function (verifyCardResult) {
        if (verifyCardResult === null ||
            verifyCardResult === undefined ||
            verifyCardResult.nonce == null) {
            return null;
        }
    
        threeDsValidationNew.nonceSecure =
            verifyCardResult.nonce;
    
        return threeDsValidationNew;
    });
    

    Where getThreeDsSecureParameters is:

    function getThreeDsSecureParameters(threeDsValidation) {
        return {
            amount: threeDsValidation.amount.toString(),
            nonce: threeDsValidation.paymentMethodNonce,
            bin: threeDsValidation.bin,
            email: threeDsValidation.braintreeCharge.email,
            billingAddress: {
                givenName: threeDsValidation.braintreeCharge.billingName,
                phoneNumber: threeDsValidation.braintreeCharge.phone,
                streetAddress: threeDsValidation.braintreeCharge.streetAddress,
                extendedAddress: threeDsValidation.braintreeCharge.extendedAddress,
                locality: threeDsValidation.braintreeCharge.city,
                region: threeDsValidation.braintreeCharge.state,
                postalCode: threeDsValidation.braintreeCharge.zip,
                countryCodeAlpha2: threeDsValidation.braintreeCharge.country
            },
            additionalInformation: {
                shippingGivenName: threeDsValidation.braintreeCharge.shippingName,
                shippingPhone: threeDsValidation.braintreeCharge.shippingPhone,
                shippingAddress: {
                    streetAddress: threeDsValidation.braintreeCharge.shippingStreetAddress,
                    extendedAddress: threeDsValidation.braintreeCharge.shippingExtendedAddress,
                    locality: threeDsValidation.braintreeCharge.shippingCity,
                    region: threeDsValidation.braintreeCharge.shippingState,
                    postalCode: threeDsValidation.braintreeCharge.shippingZip,
                    countryCodeAlpha2: threeDsValidation.braintreeCharge.shippingCountry
                }
            },
            onLookupComplete: function (data, next) {
                next();
            }
        };
    }
    

    The secure nonce returned by verifyCard can then be used as the PaymentMethodNonce on the server side to complete a 3D Secure transaction. An additional field of DeviceData can be added to the TransactionRequest on the server-side, which is equal to the deviceData field of the dataCollector object that was created in the setupComponents method.

    TransactionRequest transactionRequest = new TransactionRequest
    {
        Amount = Amount,
        PaymentMethodNonce = PaymentMethodNonce,
        DeviceData = string.IsNullOrWhiteSpace(DeviceData) ? null : DeviceData,
        MerchantAccountId = string.IsNullOrWhiteSpace(MerchantAccountId) ? null : MerchantAccountId,
        TransactionSource = string.IsNullOrWhiteSpace(TransactionSource) ? null : TransactionSource,
        CustomerId = string.IsNullOrWhiteSpace(CustomerId) ? null : CustomerId,
        Options = new TransactionOptionsRequest
        {
            SubmitForSettlement = true,
            ThreeDSecure = new TransactionOptionsThreeDSecureRequest
            {
                Required = chargeOptions.UseThreeDSecure
            }    
        }
    };
    
    if (Address != null)
    {
        transactionRequest.BillingAddress = new AddressRequest
        {
            Company = string.IsNullOrWhiteSpace(Address.CompanyName) ? null : Address.CompanyName,
            FirstName = string.IsNullOrWhiteSpace(Address.BillingName) ? null : Address.BillingName,
            StreetAddress = string.IsNullOrWhiteSpace(Address.StreetAddress) ? null : Address.StreetAddress,
            ExtendedAddress = string.IsNullOrWhiteSpace(Address.ExtendedAddress) ? null : Address.ExtendedAddress,
            Locality = string.IsNullOrWhiteSpace(Address.City) ? null : Address.City,
            Region = string.IsNullOrWhiteSpace(Address.State) ? null : Address.State,
            PostalCode = string.IsNullOrWhiteSpace(Address.Zip) ? null : Address.Zip,
            CountryCodeAlpha2 = string.IsNullOrWhiteSpace(Address.Country) ? null : Address.Country
        };
    }
    
    IBraintreeChargeReturnModel result = await Pay(transactionRequest,
        braintreeGateway,
        chargeOptions.UseThreeDSecure);
    

    Be aware that the client-side code may display a window asking for the customer to complete additional validation when used with a production Braintree account.