Skip to content

Commit

Permalink
Fix PayPal Request class parceling
Browse files Browse the repository at this point in the history
Addresses #101
  • Loading branch information
braintreeps authored and lkorth committed Jun 3, 2016
1 parent 1382d82 commit e6cf46b
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"paymentResource":{
"paymentToken":"token",
"intent":"authorize",
"redirectUrl":"https://checkout.paypal.com/one-touch-login-sandbox/index.html?action=create_payment_resource\u0026authorization_fingerprint=63cc461306c35080ce674a3372bffe1580b4130c7fd33d33968aa76bb93cdd06%7Ccreated_at%3D2015-10-13T18%3A49%3A48.371382792%2B0000%26merchant_id%3Ddcpspy2brwdjr3qn%26public_key%3D9wwrzqk3vr3t4nc8\u0026cancel_url=com.braintreepayments.api.test.braintree%3A%2F%2Fonetouch%2Fv1%2Fcancel\u0026controller=client_api%2Fpaypal_hermes\u0026experience_profile%5Baddress_override%5D=false\u0026experience_profile%5Bno_shipping%5D=false\u0026merchant_id=dcpspy2brwdjr3qn\u0026return_url=com.braintreepayments.api.test.braintree%3A%2F%2Fonetouch%2Fv1%2Fsuccess\u0026token=EC-HERMES-SANDBOX-EC-TOKEN\u0026version=1"
"redirectUrl":"https://checkout.paypal.com/one-touch-login-sandbox/index.html?action=create_payment_resource\u0026authorization_fingerprint=63cc461306c35080ce674a3372bffe1580b4130c7fd33d33968aa76bb93cdd06%7Ccreated_at%3D2015-10-13T18%3A49%3A48.371382792%2B0000%26merchant_id%3Ddcpspy2brwdjr3qn%26public_key%3D9wwrzqk3vr3t4nc8\u0026cancel_url=com.braintreepayments.api.test.braintree%3A%2F%2Fonetouch%2Fv1%2Fcancel\u0026controller=client_api%2Fpaypal_hermes\u0026experience_profile%5Baddress_override%5D=false\u0026experience_profile%5Bno_shipping%5D=false\u0026merchant_id=dcpspy2brwdjr3qn\u0026return_url=com.braintreepayments.api.test.braintree%3A%2F%2Fonetouch%2Fv1%2Fsuccess\u0026ba_token=EC-HERMES-SANDBOX-EC-TOKEN\u0026version=1"
}
}
50 changes: 35 additions & 15 deletions Braintree/src/test/java/com/braintreepayments/api/PayPalTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import com.braintreepayments.api.models.PaymentMethodBuilder;
import com.braintreepayments.api.models.PostalAddress;
import com.paypal.android.sdk.onetouch.core.AuthorizationRequest;
import com.paypal.android.sdk.onetouch.core.base.ContextInspector;
import com.paypal.android.sdk.onetouch.core.config.Recipe;

import org.json.JSONException;
Expand All @@ -34,6 +33,7 @@
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;

import static com.braintreepayments.testutils.FixturesHelper.stringFromFixture;
import static junit.framework.Assert.assertEquals;
Expand Down Expand Up @@ -136,13 +136,23 @@ public void authorizeAccount_startsBrowser() {

@Test
public void authorizeAccount_isSuccessful() throws Exception {
spy(AuthorizationRequest.class);
doReturn(true).when(AuthorizationRequest.class, "isValidResponse", any(ContextInspector.class), anyString());
doAnswer(new Answer<AuthorizationRequest>() {
@Override
public AuthorizationRequest answer(InvocationOnMock invocation) throws Throwable {
AuthorizationRequest request = spy(new AuthorizationRequest(RuntimeEnvironment.application));
request.environment("test");
request.successUrl("com.braintreepayments.api.test.braintree", "success");

doReturn(true).when(request, "isValidResponse", anyString());

JSONObject decryptedPayload = mock(JSONObject.class);
when(decryptedPayload.getString("payment_code")).thenReturn("code");
when(decryptedPayload.getString("email")).thenReturn("[email protected]");
doReturn(decryptedPayload).when(request, "getDecryptedPayload", anyString());

JSONObject decryptedPayload = mock(JSONObject.class);
when(decryptedPayload.getString("payment_code")).thenReturn("code");
when(decryptedPayload.getString("email")).thenReturn("[email protected]");
doReturn(decryptedPayload).when(AuthorizationRequest.class, "getDecryptedPayload", anyString(), anyString());
return request;
}
}).when(PayPal.class, "getAuthorizationRequest", any(Context.class), any(PayPalConfiguration.class), anyString());

final BraintreeFragment fragment = mMockFragmentBuilder.build();
doAnswer(new Answer() {
Expand Down Expand Up @@ -173,13 +183,23 @@ public Void answer(InvocationOnMock invocation) throws Throwable {

@Test
public void authorizeAccount_doesNotCallCancelListenerWhenSuccessful() throws Exception {
spy(AuthorizationRequest.class);
doReturn(true).when(AuthorizationRequest.class, "isValidResponse", any(ContextInspector.class), anyString());
doAnswer(new Answer<AuthorizationRequest>() {
@Override
public AuthorizationRequest answer(InvocationOnMock invocation) throws Throwable {
AuthorizationRequest request = spy(new AuthorizationRequest(RuntimeEnvironment.application));
request.environment("test");
request.successUrl("com.braintreepayments.api.test.braintree", "success");

doReturn(true).when(request, "isValidResponse", anyString());

JSONObject decryptedPayload = mock(JSONObject.class);
when(decryptedPayload.getString("payment_code")).thenReturn("code");
when(decryptedPayload.getString("email")).thenReturn("[email protected]");
doReturn(decryptedPayload).when(AuthorizationRequest.class, "getDecryptedPayload", anyString(), anyString());
JSONObject decryptedPayload = mock(JSONObject.class);
when(decryptedPayload.getString("payment_code")).thenReturn("code");
when(decryptedPayload.getString("email")).thenReturn("[email protected]");
doReturn(decryptedPayload).when(request, "getDecryptedPayload", anyString());

return request;
}
}).when(PayPal.class, "getAuthorizationRequest", any(Context.class), any(PayPalConfiguration.class), anyString());

final BraintreeFragment fragment = mMockFragmentBuilder.build();
doAnswer(new Answer() {
Expand Down Expand Up @@ -269,13 +289,13 @@ public void requestBillingAgreement_startsBrowser() throws InvalidArgumentExcept
@Test
public void requestBillingAgreement_isSuccessful() {
final BraintreeFragment fragment = mMockFragmentBuilder
.successResponse(stringFromFixture("paypal_hermes_response.json"))
.successResponse(stringFromFixture("paypal_hermes_billing_agreement_response.json"))
.build();
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Intent intent = new Intent()
.setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN"));
.setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&ba_token=EC-HERMES-SANDBOX-EC-TOKEN"));
PayPal.onActivityResult(fragment, Activity.RESULT_OK, intent);
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@

public class AuthorizationRequest extends Request<AuthorizationRequest> implements Parcelable {

private static final String PREFS_ENCRYPTION_KEY = "com.paypal.otc.key";
private static final String PREFS_MSG_GUID = "com.paypal.otc.msg_guid";

private final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s");
private final OtcCrypto mOtcCrypto = new OtcCrypto();
private final HashSet<String> mScopes;
Expand All @@ -82,6 +79,10 @@ public AuthorizationRequest withAdditionalPayloadAttribute(String key, String va
return this;
}

protected Map<String, String> getAdditionalPayloadAttributes() {
return new HashMap<>(mAdditionalPayloadAttributes);
}

public AuthorizationRequest withScopeValue(String scopeValue) {
Matcher matcher = WHITESPACE_PATTERN.matcher(scopeValue);
boolean found = matcher.find();
Expand Down Expand Up @@ -140,18 +141,8 @@ public Recipe getBrowserSwitchRecipe(OtcConfiguration config) {
return config.getBrowserOauth2Config(getScopes());
}

@Override
public void persistRequiredFields(ContextInspector contextInspector) {
contextInspector.setPreference(PREFS_MSG_GUID, mMsgGuid);
contextInspector.setPreference(PREFS_ENCRYPTION_KEY, EncryptionUtils.byteArrayToHexString(mEncryptionKey));
}

private static boolean isValidResponse(ContextInspector contextInspector, String msgGUID) {
String prefsMsgGUID = contextInspector.getStringPreference(AuthorizationRequest.PREFS_MSG_GUID);
String prefsSymmetricKey = getStoredSymmetricKey(contextInspector);

return (!TextUtils.isEmpty(prefsMsgGUID) && prefsMsgGUID.equals(msgGUID) &&
!TextUtils.isEmpty(prefsSymmetricKey));
private boolean isValidResponse(String msgGUID) {
return (mMsgGuid.equals(msgGUID));
}

private class RFC3339DateFormat extends SimpleDateFormat {
Expand Down Expand Up @@ -212,10 +203,6 @@ private boolean isChromeAvailable(Context context) {
return intent.resolveActivity(context.getPackageManager()) != null;
}

private static String getStoredSymmetricKey(ContextInspector contextInspector) {
return contextInspector.getStringPreference(AuthorizationRequest.PREFS_ENCRYPTION_KEY);
}

@Override
public Result parseBrowserResponse(ContextInspector contextInspector, Uri uri) {
String status = uri.getLastPathSegment();
Expand All @@ -232,14 +219,12 @@ public Result parseBrowserResponse(ContextInspector contextInspector, Uri uri) {
return new Result(new ResponseParsingException("Response incomplete"));
}

String msgGUID = payload.optString("msg_GUID");
if (TextUtils.isEmpty(payloadEnc) || !isValidResponse(contextInspector, msgGUID)) {
if (TextUtils.isEmpty(payloadEnc) || !isValidResponse(payload.optString("msg_GUID"))) {
return new Result(new ResponseParsingException("Response invalid"));
}

try {
JSONObject decryptedPayloadEnc = getDecryptedPayload(payloadEnc,
getStoredSymmetricKey(contextInspector));
JSONObject decryptedPayloadEnc = getDecryptedPayload(payloadEnc);

String error = payload.optString("error");
// the string 'null' is coming back in production
Expand Down Expand Up @@ -305,27 +290,19 @@ public void trackFpti(Context context, TrackingPoint trackingPoint, Protocol pro
PayPalOneTouchCore.getFptiManager(context).trackFpti(trackingPoint, getEnvironment(), fptiDataBundle, protocol);
}

private static JSONObject getDecryptedPayload(String payloadEnc, String symmetricKey)
throws IllegalBlockSizeException, InvalidKeyException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
private JSONObject getDecryptedPayload(String payloadEnc) throws IllegalBlockSizeException, InvalidKeyException,
NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
InvalidEncryptionDataException, JSONException, IllegalArgumentException {
byte[] base64PayloadEnc = Base64.decode(payloadEnc, Base64.DEFAULT);
byte[] key = EncryptionUtils.hexStringToByteArray(symmetricKey);
byte[] output = new OtcCrypto().decryptAESCTRData(base64PayloadEnc, key);
byte[] output = new OtcCrypto().decryptAESCTRData(base64PayloadEnc, mEncryptionKey);

return new JSONObject(new String(output));
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(getClientMetadataId());
dest.writeString(getClientId());
dest.writeString(getEnvironment());
super.writeToParcel(dest, flags);

dest.writeString(mPrivacyUrl);
dest.writeString(mUserAgreementUrl);
dest.writeSerializable(mScopes);
Expand All @@ -336,9 +313,8 @@ public void writeToParcel(Parcel dest, int flags) {
}

private AuthorizationRequest(Parcel source) {
clientMetadataId(source.readString());
clientId(source.readString());
environment(source.readString());
super(source);

mPrivacyUrl = source.readString();
mUserAgreementUrl = source.readString();
mScopes = (HashSet) source.readSerializable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@

public class BillingAgreementRequest extends CheckoutRequest {

private static final String TOKEN_QUERY_PARAM_KEY_BA_TOKEN = "ba_token";

public BillingAgreementRequest() {}

@Override
public BillingAgreementRequest pairingId(String pairingId) {
super.pairingId(pairingId);
return this;
}

@Override
public BillingAgreementRequest approvalURL(String approvalURL) {
super.approvalURL(approvalURL);
mTokenQueryParamKey = TOKEN_QUERY_PARAM_KEY_BA_TOKEN;
return this;
}

Expand All @@ -38,25 +45,8 @@ public Recipe getRecipeToExecute(Context context, OtcConfiguration config) {
return null;
}

public BillingAgreementRequest() {}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(getClientMetadataId());
dest.writeString(getClientId());
dest.writeString(getEnvironment());

dest.writeString(mApprovalUrl);
dest.writeString(mTokenQueryParamKey);
}

private BillingAgreementRequest(Parcel source) {
clientMetadataId(source.readString());
clientId(source.readString());
environment(source.readString());

mApprovalUrl = source.readString();
mTokenQueryParamKey = source.readString();
protected BillingAgreementRequest(Parcel source) {
super(source);
}

public static final Creator<BillingAgreementRequest> CREATOR = new Creator<BillingAgreementRequest>() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@

public class CheckoutRequest extends Request<CheckoutRequest> implements Parcelable {

private static final String PREFS_HERMES_TOKEN = "com.paypal.otc.hermes.token";
private static final String TOKEN_QUERY_PARAM_KEY_TOKEN = "token";
private static final String TOKEN_QUERY_PARAM_KEY_BA_TOKEN = "ba_token";

protected String mApprovalUrl;
protected String mTokenQueryParamKey;
Expand All @@ -50,18 +48,10 @@ public CheckoutRequest pairingId(String pairingId) {

public CheckoutRequest approvalURL(String approvalURL) {
mApprovalUrl = approvalURL;
selectTokenQueryParamKey(approvalURL);
mTokenQueryParamKey = TOKEN_QUERY_PARAM_KEY_TOKEN;
return this;
}

private void selectTokenQueryParamKey(String url) {
if (!TextUtils.isEmpty(url) && url.contains("ba_token")) {
mTokenQueryParamKey = TOKEN_QUERY_PARAM_KEY_BA_TOKEN;
} else {
mTokenQueryParamKey = TOKEN_QUERY_PARAM_KEY_TOKEN;
}
}

@Override
public String getBrowserSwitchUrl(Context context, OtcConfiguration config) {
return mApprovalUrl;
Expand All @@ -72,12 +62,6 @@ public Recipe getBrowserSwitchRecipe(OtcConfiguration config) {
return config.getBrowserCheckoutConfig();
}

@Override
public void persistRequiredFields(ContextInspector contextInspector) {
contextInspector.setPreference(PREFS_HERMES_TOKEN,
Uri.parse(mApprovalUrl).getQueryParameter(mTokenQueryParamKey));
}

@Override
public Result parseBrowserResponse(ContextInspector contextInspector, Uri uri) {
String status = uri.getLastPathSegment();
Expand All @@ -87,9 +71,9 @@ public Result parseBrowserResponse(ContextInspector contextInspector, Uri uri) {
return new Result();
}

String persistedXoToken = contextInspector.getStringPreference(PREFS_HERMES_TOKEN);
String requestXoToken = Uri.parse(mApprovalUrl).getQueryParameter(mTokenQueryParamKey);
String responseXoToken = uri.getQueryParameter(mTokenQueryParamKey);
if (null != responseXoToken && TextUtils.equals(persistedXoToken, responseXoToken)) {
if (responseXoToken != null && TextUtils.equals(requestXoToken, responseXoToken)) {
try {
JSONObject response = new JSONObject();
response.put("webURL", uri.toString());
Expand All @@ -109,11 +93,11 @@ public Result parseBrowserResponse(ContextInspector contextInspector, Uri uri) {

@Override
public boolean validateV1V2Response(ContextInspector contextInspector, Bundle extras) {
String persistedXoToken = contextInspector.getStringPreference(PREFS_HERMES_TOKEN);
String requestXoToken = Uri.parse(mApprovalUrl).getQueryParameter(mTokenQueryParamKey);
String webUrl = extras.getString("webURL");
if (null != webUrl) {
if (webUrl != null) {
String responseXoToken = Uri.parse(webUrl).getQueryParameter(mTokenQueryParamKey);
if (null != responseXoToken && TextUtils.equals(persistedXoToken, responseXoToken)) {
if (responseXoToken != null && TextUtils.equals(requestXoToken, responseXoToken)) {
return true;
}
}
Expand Down Expand Up @@ -150,28 +134,21 @@ public void trackFpti(Context context, TrackingPoint trackingPoint, Protocol pro
.trackFpti(trackingPoint, getEnvironment(), fptiDataBundle, protocol);
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(getClientMetadataId());
dest.writeString(getClientId());
dest.writeString(getEnvironment());
super.writeToParcel(dest, flags);

dest.writeString(mApprovalUrl);
dest.writeString(mTokenQueryParamKey);
dest.writeString(mPairingId);
}

private CheckoutRequest(Parcel source) {
clientMetadataId(source.readString());
clientId(source.readString());
environment(source.readString());
protected CheckoutRequest(Parcel source) {
super(source);

mApprovalUrl = source.readString();
mTokenQueryParamKey = source.readString();
mPairingId = source.readString();
}

public static final Creator<CheckoutRequest> CREATOR = new Creator<CheckoutRequest>() {
Expand Down
Loading

0 comments on commit e6cf46b

Please sign in to comment.