Skip to content

Commit

Permalink
Set error message on ThreeDSecureInfo when 3DS challenge fails (brain…
Browse files Browse the repository at this point in the history
…tree#419)

* Set error message on ThreeDSecureInfo when customer fails 3DS authentication
  • Loading branch information
sestevens authored and GitHub Enterprise committed Oct 1, 2019
1 parent e458ab4 commit d309139
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 170 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"paymentMethod": {
"type": "CreditCard",
"nonce": "ceff1c43-0274-010d-6b0e-e15544dd8583",
"description": "ending in 91",
"consumed": false,
"threeDSecureInfo": {
"liabilityShifted": true,
"liabilityShiftPossible": true,
"status": "authenticate_successful",
"enrolled": "Y",
"cavv": "MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=",
"xid": null,
"eci_flag": "05",
"three_d_secure_version": "2.1.0",
"ds_transaction_id": "e1aaed8b-4c2e-4e50-b6a9-8d48779c90b3",
"acs_transaction_id": "e2f621db-b23a-4b0a-983c-91f72b23d16b",
"three_d_secure_server_transaction_id": "d6909f17-6a92-4ea1-9148-5499677dee60",
"pares_status": "Y",
"acsTransactionId": "e2f621db-b23a-4b0a-983c-91f72b23d16b",
"dsTransactionId": "e1aaed8b-4c2e-4e50-b6a9-8d48779c90b3",
"eciFlag": "05",
"paresStatus": "Y",
"threeDSecureServerTransactionId": "d6909f17-6a92-4ea1-9148-5499677dee60",
"threeDSecureVersion": "2.1.0",
"lookup": {
"transStatus": "C",
"transStatusReason": null
},
"authentication": {
"transStatus": "Y",
"transStatusReason": null
}
},
"details": {
"bin": "400000",
"lastTwo": "91",
"lastFour": "1091",
"cardType": "Visa",
"expirationYear": "2022",
"expirationMonth": "01"
},
"bin_data": {
"prepaid": "Unknown",
"healthcare": "Unknown",
"debit": "Unknown",
"durbin_regulated": "Unknown",
"commercial": "Unknown",
"payroll": "Unknown",
"issuing_bank": "Unknown",
"country_of_issuance": "Unknown",
"product_id": "Unknown"
}
},
"threeDSecureInfo": {
"liabilityShifted": true,
"liabilityShiftPossible": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"errors": [
{
"attribute": "three_d_secure_token",
"message": "Failed to authenticate, please try a different form of payment.",
"model": "transaction",
"type": "user",
"code": "81571"
}
],
"threeDSecureInfo": {
"liabilityShifted": false,
"liabilityShiftPossible": true
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"error": {
"message": "Failed to authenticate, please try a different form of payment"
"message": "Failed to authenticate, please try a different form of payment."
},
"fieldErrors":[],
"threeDSecureInfo": {
"liabilityShifted": false,
"liabilityShiftPossible": false
"liabilityShiftPossible": true
},
"success": false
}
61 changes: 33 additions & 28 deletions Braintree/src/main/java/com/braintreepayments/api/ThreeDSecure.java
Original file line number Diff line number Diff line change
Expand Up @@ -334,45 +334,39 @@ public static void initializeChallengeWithLookupResponse(final BraintreeFragment
}
}

protected static void performCardinalAuthentication(final BraintreeFragment fragment, final ThreeDSecureLookup threeDSecureLookup) {
fragment.sendAnalyticsEvent("three-d-secure.verification-flow.started");

Bundle extras = new Bundle();
extras.putParcelable(ThreeDSecureActivity.EXTRA_THREE_D_SECURE_LOOKUP, threeDSecureLookup);

Intent intent = new Intent(fragment.getApplicationContext(), ThreeDSecureActivity.class);
intent.putExtras(extras);

fragment.startActivityForResult(intent, BraintreeRequestCodes.THREE_D_SECURE);
}

protected static void authenticateCardinalJWT(final BraintreeFragment fragment, final ThreeDSecureLookup threeDSecureLookup, final String cardinalJWT) {
final CardNonce cardNonce = threeDSecureLookup.getCardNonce();
static void authenticateCardinalJWT(final BraintreeFragment fragment, final ThreeDSecureLookup threeDSecureLookup, final String cardinalJWT) {
final CardNonce lookupCardNonce = threeDSecureLookup.getCardNonce();

fragment.sendAnalyticsEvent("three-d-secure.verification-flow.upgrade-payment-method.started");

final String nonce = cardNonce.getNonce();
final String lookupNonce = lookupCardNonce.getNonce();
JSONObject body = new JSONObject();
try {
body.put("jwt", cardinalJWT);
body.put("paymentMethodNonce", nonce);
body.put("paymentMethodNonce", lookupNonce);
} catch (JSONException ignored) {
}

fragment.getHttpClient().post(TokenizationClient.versionedPath(
TokenizationClient.PAYMENT_METHOD_ENDPOINT + "/" + nonce +
TokenizationClient.PAYMENT_METHOD_ENDPOINT + "/" + lookupNonce +
"/three_d_secure/authenticate_from_jwt"), body.toString(), new HttpResponseCallback() {
@Override
public void success(String responseBody) {
CardNonce nonceToReturn = ThreeDSecureAuthenticationResponse.getNonceWithAuthenticationDetails(responseBody, cardNonce);
ThreeDSecureAuthenticationResponse authenticationResponse = ThreeDSecureAuthenticationResponse.fromJson(responseBody);

if (nonceToReturn.getThreeDSecureInfo().getThreeDSecureAuthenticationResponse().isSuccess()) {
fragment.sendAnalyticsEvent("three-d-secure.verification-flow.upgrade-payment-method.succeeded");
} else {
// NEXT_MAJOR_VERSION
// Remove this line. Pass back lookupCardNonce + error message if there are errors.
// Otherwise pass back authenticationResponse.getCardNonce().
CardNonce nonce = ThreeDSecureAuthenticationResponse.getNonceWithAuthenticationDetails(responseBody, lookupCardNonce);

if (authenticationResponse.getErrors() != null) {
fragment.sendAnalyticsEvent("three-d-secure.verification-flow.upgrade-payment-method.failure.returned-lookup-nonce");
nonce.getThreeDSecureInfo().setErrorMessage(authenticationResponse.getErrors());
completeVerificationFlowWithNoncePayload(fragment, nonce);
} else {
fragment.sendAnalyticsEvent("three-d-secure.verification-flow.upgrade-payment-method.succeeded");
completeVerificationFlowWithNoncePayload(fragment, nonce);
}

completeVerificationFlowWithNoncePayload(fragment, nonceToReturn);
}

@Override
Expand All @@ -393,15 +387,14 @@ protected static void onActivityResult(BraintreeFragment fragment, int resultCod

if (resultUri != null) {
// V1 flow
ThreeDSecureAuthenticationResponse authenticationResponse = ThreeDSecureAuthenticationResponse
.fromJson(resultUri.getQueryParameter("auth_response"));
String authResponse = resultUri.getQueryParameter("auth_response");
ThreeDSecureAuthenticationResponse authenticationResponse = ThreeDSecureAuthenticationResponse.fromJson(authResponse);

// NEXT_MAJOR_VERSION Make isSuccess package-private so that we have access to it, but merchants do not
if (authenticationResponse.isSuccess()) {
completeVerificationFlowWithNoncePayload(fragment, authenticationResponse.getCardNonce());
} else if (authenticationResponse.getException() != null) {
fragment.postCallback(new BraintreeException(authenticationResponse.getException()));
} else {
fragment.postCallback(new ErrorWithResponse(422, authenticationResponse.getErrors()));
fragment.postCallback(new ErrorWithResponse(422, authResponse));
}
} else {
// V2 flow
Expand Down Expand Up @@ -478,4 +471,16 @@ public void failure(Exception exception) {
}
});
}

private static void performCardinalAuthentication(final BraintreeFragment fragment, final ThreeDSecureLookup threeDSecureLookup) {
fragment.sendAnalyticsEvent("three-d-secure.verification-flow.started");

Bundle extras = new Bundle();
extras.putParcelable(ThreeDSecureActivity.EXTRA_THREE_D_SECURE_LOOKUP, threeDSecureLookup);

Intent intent = new Intent(fragment.getApplicationContext(), ThreeDSecureActivity.class);
intent.putExtras(extras);

fragment.startActivityForResult(intent, BraintreeRequestCodes.THREE_D_SECURE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import android.os.Parcel;
import android.os.Parcelable;

import com.braintreepayments.api.Json;

import org.json.JSONException;
import org.json.JSONObject;

Expand All @@ -11,6 +13,9 @@
*/
public class ThreeDSecureAuthenticationResponse implements Parcelable {

private static final String ERRORS_KEY = "errors";
private static final String ERROR_KEY = "error";
private static final String MESSAGE_KEY = "message";
private static final String PAYMENT_METHOD_KEY = "paymentMethod";
private static final String SUCCESS_KEY = "success";

Expand Down Expand Up @@ -39,16 +44,17 @@ public static ThreeDSecureAuthenticationResponse fromJson(String jsonString) {
authenticationResponse.mCardNonce = cardNonce;
}

// TODO: 3DS 1.0 has a "success" key, but 3DS 2.0 responses dont.
// Waiting for the Gateway to send this success key.
// 3DS 1.0 has a "success" key, but 3DS 2.0 responses do not.
if (json.has(SUCCESS_KEY)) {
if (json.has(ERROR_KEY)) {
authenticationResponse.mErrors = Json.optString(json.getJSONObject(ERROR_KEY), MESSAGE_KEY, null);
}
authenticationResponse.mSuccess = json.getBoolean(SUCCESS_KEY);
} else if (!json.has("errors")) {
authenticationResponse.mSuccess = true;
}

if (!authenticationResponse.mSuccess) {
authenticationResponse.mErrors = jsonString;
} else {
if (json.has(ERRORS_KEY)) {
authenticationResponse.mErrors = Json.optString(json.getJSONArray(ERRORS_KEY).getJSONObject(0), MESSAGE_KEY, null);
}
authenticationResponse.mSuccess = authenticationResponse.mErrors == null;
}
} catch (JSONException e) {
authenticationResponse.mSuccess = false;
Expand All @@ -57,6 +63,7 @@ public static ThreeDSecureAuthenticationResponse fromJson(String jsonString) {
return authenticationResponse;
}

@Deprecated
public static CardNonce getNonceWithAuthenticationDetails(String jsonString, CardNonce lookupCardNonce) {
ThreeDSecureAuthenticationResponse authenticationResponse = new ThreeDSecureAuthenticationResponse();
CardNonce nonceToReturn = lookupCardNonce;
Expand All @@ -66,7 +73,7 @@ public static CardNonce getNonceWithAuthenticationDetails(String jsonString, Car

if (json.has(SUCCESS_KEY)) {
authenticationResponse.mSuccess = json.getBoolean(SUCCESS_KEY);
} else if (!json.has("errors")) {
} else if (!json.has(ERRORS_KEY)) {
authenticationResponse.mSuccess = true;
}

Expand Down Expand Up @@ -103,8 +110,11 @@ public static ThreeDSecureAuthenticationResponse fromException(String exception)
}

/**
* @deprecated Use {@link ThreeDSecureInfo#isLiabilityShifted()} and
* {@link ThreeDSecureInfo#isLiabilityShiftPossible()} to determine state of 3D Secure authentication
* @return If the authentication was completed
*/
@Deprecated
public boolean isSuccess() {
return mSuccess;
}
Expand All @@ -118,9 +128,12 @@ public CardNonce getCardNonce() {
}

/**
* @deprecated Use {@link ThreeDSecureInfo#getErrorMessage()}
* @return Possible errors that occurred during the authentication
*/
@Deprecated
public String getErrors() {
// NEXT_MAJOR_VERSION make this a private method
return mErrors;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class ThreeDSecureInfo implements Parcelable {
private String mAuthenticationTransactionStatusReason;
private String mLookupTransactionStatus;
private String mLookupTransactionStatusReason;
private String mErrorMessage;

protected static ThreeDSecureInfo fromJson(JSONObject json) {
if (json == null) {
Expand Down Expand Up @@ -86,6 +87,10 @@ protected void setThreeDSecureAuthenticationResponse(ThreeDSecureAuthenticationR
mThreeDSecureAuthenticationResponse = authResponse;
}

public void setErrorMessage(String errorMessage) {
mErrorMessage = errorMessage;
}

/**
* @return Cardholder authentication verification value or "CAVV" is the main encrypted message issuers and card networks use to verify authentication has occured. Mastercard uses an "AVV" message which will also be returned in the cavv parameter.
*/
Expand Down Expand Up @@ -189,6 +194,8 @@ public String getParesStatus() {
}

/**
* @deprecated Use {@link #getErrorMessage()}, {@link #isLiabilityShifted()}, and {@link #isLiabilityShiftPossible()}
* to determine the result of the challenge authentication
* @return The {@link ThreeDSecureAuthenticationResponse} if one is associated with a nonce.
*/
public ThreeDSecureAuthenticationResponse getThreeDSecureAuthenticationResponse() {
Expand Down Expand Up @@ -223,6 +230,13 @@ public String getLookupTransactionStatusReason() {
return mLookupTransactionStatusReason;
}

/**
* @return The error message, if present, that occurred during a 3D Secure challenge attempt
*/
public String getErrorMessage() {
return mErrorMessage;
}

public ThreeDSecureInfo() {}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,20 +302,25 @@ public void onActivityResult_whenSuccessful_sendAnalyticsEvents() throws Excepti
}

@Test
public void onActivityResult_whenFailure_postsException() throws Exception {
public void onActivityResult_whenFailure_postsErrorWithResponse() throws Exception {
JSONObject json = new JSONObject();
json.put("success", false);

JSONObject errorJson = new JSONObject();
errorJson.put("message", "Failed to authenticate, please try a different form of payment.");
json.put("error", errorJson);

Uri uri = Uri.parse("https://.com?auth_response=" + json.toString());
Intent data = new Intent();
data.setData(uri);

ThreeDSecure.onActivityResult(mFragment, RESULT_OK, data);

ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
ArgumentCaptor<ErrorWithResponse> captor = ArgumentCaptor.forClass(ErrorWithResponse.class);
verify(mFragment).postCallback(captor.capture());

ErrorWithResponse error = (ErrorWithResponse) captor.getValue();
ErrorWithResponse error = captor.getValue();
assertEquals(422, error.getStatusCode());
assertEquals("Failed to authenticate, please try a different form of payment.", error.getMessage());
}
}
Loading

0 comments on commit d309139

Please sign in to comment.