Skip to content
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
vNext
----------
- [MINOR] Implement a WebView handler to facilitate broker app installation via the Play Store (#2643)

Version 21.1.0
----------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,12 @@ public static String computeMaxHostBrokerProtocol() {

public static final String SSO_NONCE_PARAMETER = "sso_nonce";

/**
* Prefix for intent-based URIs. This is typically used to construct or parse
* intent URIs that are used for inter-app communication or deep linking.
*/
public static final String INTENT_PREFIX = "intent://";

/**
* PRT nonce.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
Expand All @@ -41,6 +42,7 @@

import com.microsoft.identity.common.adal.internal.AuthenticationConstants;
import com.microsoft.identity.common.adal.internal.util.StringExtensions;
import com.microsoft.identity.common.internal.broker.BrokerData;
import com.microsoft.identity.common.internal.broker.PackageHelper;
import com.microsoft.identity.common.internal.fido.CredManFidoManager;
import com.microsoft.identity.common.internal.fido.FidoChallenge;
Expand Down Expand Up @@ -252,6 +254,10 @@ else if (isRedirectUrl(formattedURL)) {
processInvalidRedirectUri(view, url);
} else if (isBlankPageRequest(formattedURL)) {
Logger.info(methodTag,"It is an blank page request");
} else if (isIntentRequestToInstallBrokerApp(formattedURL)) {
Logger.info(methodTag, "It is an intent request");
// Intent URI format is case sensitive, so we need to provide the original URI.
processIntentToInstallBrokerApp(view, url);
} else if (!isUriSSLProtected(formattedURL)) {
Logger.info(methodTag,"Check for SSL protection");
processSSLProtectionCheck(view, url);
Expand Down Expand Up @@ -313,6 +319,33 @@ private boolean isNonceRedirect(@NonNull final String url) {
return url.contains(AuthenticationConstants.Broker.SSO_NONCE_PARAMETER);
}

/**
* Determines if the provided URL is a valid request to install a broker app.
* <p>
* This method checks if the URL starts with the intent prefix, is targeting the Google Play Store app,
* and is associated with a broker app. It ensures that only valid intent requests are processed.
*
* @param url The URL to evaluate.
* @return {@code true} if the URL is a permitted intent request, {@code false} otherwise.
*/
private boolean isIntentRequestToInstallBrokerApp(@NonNull final String url) {
// Check if the URL is an intent request
if (!url.startsWith(AuthenticationConstants.Broker.INTENT_PREFIX)) {
return false;
}
// Check if the intent request is for the google play store app
if (!url.contains(";package=com.android.vending;")) {
return false;
}
// Check if the url query parameter is for a broker app.
for (final BrokerData brokerData : BrokerData.getAllBrokers()) {
if (url.contains("id=" + brokerData.getPackageName())) {
return true;
}
}
return false;
}

private boolean isCrossCloudRedirect(@NonNull final String url) {
try {
final URL currentUrl = new URL(url);
Expand Down Expand Up @@ -576,6 +609,35 @@ private void processInvalidRedirectUri(@NonNull final WebView view,
view.stopLoading();
}

/**
* This method is used to process the intent to install the broker app.
* It parses the intent URI and starts the activity if the package name is valid.
*
* @param view The WebView that will be used to open the URL.
* @param intentUrl The URL to be opened.
*/
private void processIntentToInstallBrokerApp(@NonNull final WebView view, @NonNull final String intentUrl) {
final String methodTag = TAG + ":processIntentToInstallBrokerApp";
try {
final Intent intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME);
if (intent != null && intent.getPackage() != null) {
view.getContext().startActivity(intent);
Logger.info(methodTag, "Intent request sent to launch the app: " + intent.getPackage());
} else {
Logger.warn(methodTag, "Unable to parse the intent URI");
}
} catch (final URISyntaxException e) {
Logger.error(methodTag, "Failed to parse the intent URI due to invalid syntax.", e);
returnError(ErrorStrings.URI_SYNTAX_ERROR, e.getMessage());
} catch (final ActivityNotFoundException e) {
Logger.error(methodTag, "No activity found to handle the intent.", e);
returnError(ErrorStrings.ACTIVITY_NOT_FOUND, e.getMessage());
} catch (final Throwable throwable) {
Logger.error(methodTag, "An unexpected error occurred while processing the intent URI.", throwable);
returnError(ErrorStrings.UNEXPECTED_ERROR, throwable.getMessage());
}
}

private void processSSLProtectionCheck(@NonNull final WebView view,
@NonNull final String url) {
final String methodTag = TAG + ":processSSLProtectionCheck";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class AzureActiveDirectoryWebViewClientTest {
private static final String TEST_NONCE_REDIRECT_URL = "https://login.microsoftonline.com/organizations/oAuth2/v2.0/authorize?&sso_nonce=ABCD";
private static final String TEST_CROSS_CLOUD_REDIRECT_URL = "https://login.microsoftonline.us/organizations/oAuth2/v2.0/authorize?x=10";
private static final String TEST_PUBLIC_CLOUD_REDIRECT_URL = "https://login.microsoftonline.com/organizations/oAuth2/v2.0/authorize?x=10";
private static final String TEST_INTENT_INSTALL_BROKER_REDIRECT_URL = "intent://play.google.com/store/apps/details?id=com.azure.authenticator&referrer=%20adjust_reftag%3Dc6f1p4ErudH2C%26utm_source%3DLanding%2BPage%2BOrganic%2B-%2Bapp%2Bstore%2Bbadges%26utm_campaign%3Dappstore_android&pcampaignid=web_auto_redirect&web_logged_in=0&redirect_entry_point=dp#Intent;scheme=https;action=android.intent.action.VIEW;package=com.android.vending;end";

@Before
public void setup() throws ClientException {
Expand Down Expand Up @@ -234,4 +235,9 @@ public void testProcessCloudRedirectAndPrtHeaderInternalException() {
Assert.fail("Failure is not expected. We should have caught the exception and ignored it. " + e);
}
}

@Test
public void testUrlOverrideHandlesIntentRedirectUrl() {
assertTrue(mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_INTENT_INSTALL_BROKER_REDIRECT_URL));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -460,4 +460,18 @@ private ErrorStrings() {
*/
public static final String GET_PENDING_INTENT_CANCELED = "get_pending_intent_canceled";

/**
* Error occurred while attempting to parse a URI due to a syntax error.
*/
public static final String URI_SYNTAX_ERROR = "uri_syntax_error";

/**
* Error occurred if the activity can not be found to execute the given Intent
*/
public static final String ACTIVITY_NOT_FOUND = "activity_not_found";

/**
* A generic error code used when no other error code is applicable.
*/
public static final String UNEXPECTED_ERROR = "unexpected_error";
}
Loading