Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix out-of-band web payment authentication #1537

Merged
merged 1 commit into from
Sep 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ internal class PaymentAuthWebView : WebView {
return webViewClient?.hasOpenedApp == true
}

fun getCompletionUrl(): String? = webViewClient?.completionUrlParam

@SuppressLint("SetJavaScriptEnabled")
private fun configureSettings() {
settings.javaScriptEnabled = true
Expand All @@ -63,8 +65,13 @@ internal class PaymentAuthWebView : WebView {
private val clientSecret: String,
returnUrl: String?
) : WebViewClient() {
private val returnUri: Uri? = if (returnUrl != null) Uri.parse(returnUrl) else null
// user-specified return URL
private val userReturnUri: Uri? = if (returnUrl != null) Uri.parse(returnUrl) else null

var completionUrlParam: String? = null
private set

// true if another app was opened from this WebView
var hasOpenedApp: Boolean = false
private set

Expand All @@ -80,8 +87,12 @@ internal class PaymentAuthWebView : WebView {
}
}

private fun isCompletionUrl(url: String): Boolean {
for (completionUrl in COMPLETION_URLS) {
private fun isAuthenticateUrl(url: String) = isWhiteListedUrl(url, AUTHENTICATE_URLS)

private fun isCompletionUrl(url: String) = isWhiteListedUrl(url, COMPLETION_URLS)

private fun isWhiteListedUrl(url: String, whitelistedUrls: Set<String>): Boolean {
for (completionUrl in whitelistedUrls) {
if (url.startsWith(completionUrl)) {
return true
}
Expand All @@ -92,6 +103,8 @@ internal class PaymentAuthWebView : WebView {

override fun shouldOverrideUrlLoading(view: WebView, urlString: String): Boolean {
val uri = Uri.parse(urlString)
updateCompletionUrl(uri)

return if (isReturnUrl(uri)) {
onAuthCompleted()
true
Expand Down Expand Up @@ -126,6 +139,18 @@ internal class PaymentAuthWebView : WebView {
}
}

private fun updateCompletionUrl(uri: Uri) {
val returnUrlParam = if (isAuthenticateUrl(uri.toString())) {
uri.getQueryParameter(PARAM_RETURN_URL)
} else {
null
}

if (!returnUrlParam.isNullOrBlank()) {
completionUrlParam = returnUrlParam
}
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun shouldOverrideUrlLoading(
view: WebView,
Expand All @@ -138,19 +163,19 @@ internal class PaymentAuthWebView : WebView {
when {
isPredefinedReturnUrl(uri) -> return true

// If the `returnUri` is known, look for URIs that match it.
returnUri != null ->
return returnUri.scheme != null &&
returnUri.scheme == uri.scheme &&
returnUri.host != null &&
returnUri.host == uri.host
// If the `userReturnUri` is known, look for URIs that match it.
userReturnUri != null ->
return userReturnUri.scheme != null &&
userReturnUri.scheme == uri.scheme &&
userReturnUri.host != null &&
userReturnUri.host == uri.host
else -> {
// Skip opaque (i.e. non-hierarchical) URIs
if (uri.isOpaque) {
return false
}

// If the `returnUri` is unknown, look for URIs that contain a
// If the `userReturnUri` is unknown, look for URIs that contain a
// `payment_intent_client_secret` or `setup_intent_client_secret`
// query parameter, and check if its values matches the given `clientSecret`
// as a query parameter.
Expand Down Expand Up @@ -180,10 +205,16 @@ internal class PaymentAuthWebView : WebView {
const val PARAM_PAYMENT_CLIENT_SECRET = "payment_intent_client_secret"
const val PARAM_SETUP_CLIENT_SECRET = "setup_intent_client_secret"

private val AUTHENTICATE_URLS = setOf(
"https://hooks.stripe.com/three_d_secure/authenticate"
)

private val COMPLETION_URLS = setOf(
"https://hooks.stripe.com/redirect/complete/src_",
"https://hooks.stripe.com/3d_secure/complete/tdsrc_"
)

private const val PARAM_RETURN_URL = "return_url"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@ protected void onPostResume() {

if (mWebView != null && mWebView.hasOpenedApp()) {
// If another app was opened, assume it was a bank app where payment authentication
// was completed. Upon foregrounding this screen, finish the Activity.
finish();
// was completed. Upon foregrounding this screen, load the completion URL.
final String completionUrl = mWebView.getCompletionUrl();
if (completionUrl != null) {
mWebView.loadUrl(completionUrl);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.robolectric.RobolectricTestRunner;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
Expand All @@ -40,45 +41,44 @@ public void setup() {

@Test
public void shouldOverrideUrlLoading_withPaymentIntent_shouldSetResult() {
final String deepLink = "stripe://payment_intent_return?payment_intent=pi_123&" +
final String url = "stripe://payment_intent_return?payment_intent=pi_123&" +
"payment_intent_client_secret=pi_123_secret_456&source_type=card";
final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =
createWebViewClient(
"pi_123_secret_456",
"stripe://payment_intent_return"
);
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink);
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, url);
verify(mActivity).finish();
}

@Test
public void shouldOverrideUrlLoading_withSetupIntent_shouldSetResult() {
final String deepLink = "stripe://payment_auth?setup_intent=seti_1234" +
final String url = "stripe://payment_auth?setup_intent=seti_1234" +
"&setup_intent_client_secret=seti_1234_secret_5678&source_type=card";

final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =
createWebViewClient("seti_1234_secret_5678", "stripe://payment_auth");
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink);
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, url);
verify(mActivity).finish();
}

@Test
public void shouldOverrideUrlLoading_withoutReturnUrl_onPaymentIntentImplicitReturnUrl_shouldSetResult() {
final String deepLink = "stripe://payment_intent_return?payment_intent=pi_123&" +
final String url = "stripe://payment_intent_return?payment_intent=pi_123&" +
"payment_intent_client_secret=pi_123_secret_456&source_type=card";
final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =
createWebViewClient("pi_123_secret_456");
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink);
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, url);
verify(mActivity).finish();
}

@Test
public void shouldOverrideUrlLoading_withoutReturnUrl_onSetupIntentImplicitReturnUrl_shouldSetResult() {
final String deepLink = "stripe://payment_auth?setup_intent=seti_1234" +
final String url = "stripe://payment_auth?setup_intent=seti_1234" +
"&setup_intent_client_secret=seti_1234_secret_5678&source_type=card";
final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =
createWebViewClient("seti_1234_secret_5678");
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink);
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, url);
verify(mActivity).finish();
}

Expand Down Expand Up @@ -120,19 +120,19 @@ public void onPageFinished_witRedirectCompleteUrl_shouldFinish() {

@Test
public void shouldOverrideUrlLoading_withOpaqueUri_shouldNotCrash() {
final String deepLink = "mailto:[email protected]?payment_intent=pi_123&" +
final String url = "mailto:[email protected]?payment_intent=pi_123&" +
"payment_intent_client_secret=pi_123_secret_456&source_type=card";
final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =
createWebViewClient("pi_123_secret_456");
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink);
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, url);
}

@Test
public void shouldOverrideUrlLoading_withUnsupportedDeeplink_shouldFinish() {
final String deepLink = "deep://link";
final String url = "deep://link";
final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =
createWebViewClient("pi_123_secret_456");
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink);
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, url);
verify(mActivity).finish();
}

Expand All @@ -152,6 +152,29 @@ public void shouldOverrideUrlLoading_withIntentUri_shouldParseUri() {
verify(mActivity).finish();
}

@Test
public void shouldOverrideUrlLoading_withAuthenticationUrlWithReturnUrlParam_shouldPopulateCompletionUrl() {
final String url =
"https://hooks.stripe.com/three_d_secure/authenticate?amount=1250&client_secret=src_client_secret_abc123&return_url=https%3A%2F%2Fhooks.stripe.com%2Fredirect%2Fcomplete%2Fsrc_X9Y8Z7%3Fclient_secret%3Dsrc_client_secret_abc123&source=src_X9Y8Z7&usage=single_use";
final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =
createWebViewClient("pi_123_secret_456");
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, url);
assertEquals(
"https://hooks.stripe.com/redirect/complete/src_X9Y8Z7?client_secret=src_client_secret_abc123",
paymentAuthWebViewClient.getCompletionUrlParam()
);
}

@Test
public void shouldOverrideUrlLoading_withAuthenticationUrlWithoutReturnUrlParam_shouldNotPopulateCompletionUrl() {
final String url =
"https://hooks.stripe.com/three_d_secure/authenticate?amount=1250&client_secret=src_client_secret_abc123&return_url=&source=src_X9Y8Z7&usage=single_use";
final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =
createWebViewClient("pi_123_secret_456");
paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, url);
assertNull(paymentAuthWebViewClient.getCompletionUrlParam());
}

@NonNull
private PaymentAuthWebView.PaymentAuthWebViewClient createWebViewClient(
@NonNull String clientSecret
Expand Down