Skip to content

Commit

Permalink
Fix out-of-band web payment authentication (#1537)
Browse files Browse the repository at this point in the history
When a user enters the in-app web payment authentication flow,
is taken to another app to complete payment authentication
out-of-band (i.e. a bank app), then returns to the WebView,
make a request to the completion URL.

This resolves the issue where the bank page loaded in the
WebView doesn't detect that the user returned from the
bank app.
  • Loading branch information
mshafrir-stripe authored Sep 13, 2019
1 parent 309c4aa commit e58c470
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 25 deletions.
51 changes: 41 additions & 10 deletions stripe/src/main/java/com/stripe/android/view/PaymentAuthWebView.kt
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

0 comments on commit e58c470

Please sign in to comment.