Skip to content

Commit 15418a4

Browse files
committed
2 parents 387da03 + 5d30739 commit 15418a4

11 files changed

Lines changed: 573 additions & 9 deletions

File tree

changelog.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ vNext
44
- [MINOR] Remove LruCache from SharedPreferencesFileManager (#2910)
55
- [MINOR] Edge TB: Claims (#2925)
66
- [PATCH] Update Moshi to 1.15.2 to resolve okio CVE-2023-3635 vulnerability (#3005)
7+
- [MINOR] Handle target="_blank" links in authorization WebView (#3010)
8+
- [MINOR] Handle openid-vc urls in webview (#3013)
79

810
Version 24.0.1
911
----------

common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,6 +1312,13 @@ public static String computeMaxHostBrokerProtocol() {
13121312
*/
13131313
public static final String AMAZON_APP_REDIRECT_PREFIX = "aea://";
13141314

1315+
/**
1316+
* Custom URI scheme prefix for OpenID Verifiable Credentials.
1317+
* WebView cannot load this scheme natively; it must be intercepted and
1318+
* forwarded to an external handler (wallet app) via an ACTION_VIEW intent.
1319+
*/
1320+
public static final String OPENID_VC_SCHEME_PREFIX = "openid-vc://";
1321+
13151322
/**
13161323
* Prefix for the Authenticator MFA linking.
13171324
*/
@@ -1481,6 +1488,36 @@ public static String computeMaxHostBrokerProtocol() {
14811488
*/
14821489
public static final String REDIRECT_SSL_PREFIX = "https://";
14831490

1491+
/**
1492+
* Path segment for the TLR (Total loss recovery) start page.
1493+
*/
1494+
public static final String TLR_START_PATH = "/tlr/start";
1495+
1496+
/**
1497+
* WebView routing value when the target URL from a target=_blank navigation is null.
1498+
*/
1499+
public static final String WEBVIEW_TARGET_BLANK_ROUTE_NULL_URL = "null_url";
1500+
1501+
/**
1502+
* WebView routing value when the target URL is not SSL-protected.
1503+
*/
1504+
public static final String WEBVIEW_TARGET_BLANK_ROUTE_NON_SSL = "non_ssl";
1505+
1506+
/**
1507+
* WebView routing value when the target URL is loaded inline (non-TLR page).
1508+
*/
1509+
public static final String WEBVIEW_TARGET_BLANK_ROUTE_NON_TLR = "non_tlr_flow";
1510+
1511+
/**
1512+
* WebView routing value when the target URL is delegated to the system browser (TLR page).
1513+
*/
1514+
public static final String WEBVIEW_TARGET_BLANK_ROUTE_TLR = "tlr_flow";
1515+
1516+
/**
1517+
* WebView routing value when the popup was not initiated by a user gesture.
1518+
*/
1519+
public static final String WEBVIEW_TARGET_BLANK_ROUTE_NO_USER_GESTURE = "no_user_gesture";
1520+
14841521
/**
14851522
* Prefix in the redirect for PlayStore.
14861523
*/

common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@
4040
import android.net.Uri;
4141
import android.os.Build;
4242
import android.os.Bundle;
43+
import android.os.Message;
4344
import android.view.LayoutInflater;
4445
import android.view.MotionEvent;
4546
import android.view.View;
4647
import android.view.ViewGroup;
4748
import android.webkit.PermissionRequest;
4849
import android.webkit.WebChromeClient;
50+
import android.webkit.WebResourceRequest;
4951
import android.webkit.WebSettings;
5052
import android.webkit.WebView;
53+
import android.webkit.WebViewClient;
5154
import android.widget.ProgressBar;
5255

5356
import androidx.activity.result.ActivityResultLauncher;
@@ -79,6 +82,7 @@
7982
import com.microsoft.identity.common.java.util.StringUtil;
8083
import com.microsoft.identity.common.logging.Logger;
8184

85+
import com.microsoft.identity.common.java.opentelemetry.AttributeName;
8286
import java.io.UnsupportedEncodingException;
8387
import java.net.URLEncoder;
8488
import java.util.Arrays;
@@ -87,8 +91,14 @@
8791

8892
import static com.microsoft.identity.common.java.AuthenticationConstants.OAuth2.UTID;
8993

94+
import com.microsoft.identity.common.java.opentelemetry.OTelUtility;
95+
import com.microsoft.identity.common.java.opentelemetry.SpanExtension;
96+
import com.microsoft.identity.common.java.opentelemetry.SpanName;
9097

98+
import io.opentelemetry.api.trace.Span;
9199
import io.opentelemetry.api.trace.SpanContext;
100+
import io.opentelemetry.api.trace.StatusCode;
101+
import io.opentelemetry.context.Scope;
92102

93103
/**
94104
* Authorization fragment with embedded webview.
@@ -147,6 +157,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
147157
if (activity != null) {
148158
WebViewUtil.setDataDirectorySuffix(activity.getApplicationContext());
149159
}
160+
150161
if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_LEGACY_FIDO_SECURITY_KEY_LOGIC)
151162
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
152163
mFidoLauncher = registerForActivityResult(
@@ -264,6 +275,14 @@ public void onPageLoaded(final String url) {
264275
if (!mAuthResultSent && !StringExtensions.isNullOrBlank(javascriptToExecute[0])) {
265276
mWebView.evaluateJavascript(javascriptToExecute[0], null);
266277
}
278+
279+
// Dynamically toggle multiple-windows support so that target="_blank"
280+
// interception is active ONLY on the TLR start page. On all other
281+
// pages the WebView behaves exactly as before.
282+
if (CommonFlightsManager.INSTANCE.getFlightsProvider()
283+
.isFlightEnabled(CommonFlight.ENABLE_WEBVIEW_MULTIPLE_WINDOWS)) {
284+
mWebView.getSettings().setSupportMultipleWindows(isTlrUrl(url));
285+
}
267286
}
268287
},
269288
mRedirectUri,
@@ -352,6 +371,7 @@ public boolean onTouch(final View view, final MotionEvent event) {
352371
mWebView.getSettings().setUseWideViewPort(true);
353372
mWebView.getSettings().setBuiltInZoomControls(webViewZoomControlsEnabled);
354373
mWebView.getSettings().setSupportZoom(webViewZoomEnabled);
374+
355375
mWebView.setVisibility(View.INVISIBLE);
356376
mWebView.setWebViewClient(webViewClient);
357377
mWebView.setWebChromeClient(new WebChromeClient() {
@@ -376,10 +396,125 @@ public Bitmap getDefaultVideoPoster() {
376396
// We will return a 10x10 empty image, instead of the default grey playback image. #2424
377397
return Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
378398
}
399+
400+
@Override
401+
public boolean onCreateWindow(final WebView view, boolean isDialog,
402+
boolean isUserGesture, final Message resultMsg) {
403+
if (resultMsg.obj == null) {
404+
Logger.error(methodTag, "onCreateWindow: resultMsg.obj is null, cannot set up transport.", null);
405+
return false;
406+
}
407+
408+
final SpanContext parentSpanContext = requireActivity() instanceof AuthorizationActivity
409+
? ((AuthorizationActivity) requireActivity()).getSpanContext() : null;
410+
final Span span = OTelUtility.createSpanFromParent(
411+
SpanName.WebViewTargetBlankNavigation.name(), parentSpanContext);
412+
boolean windowHandled = false;
413+
try (final Scope scope = SpanExtension.makeCurrentSpan(span)) {
414+
Logger.info(methodTag, "onCreateWindow: intercepting target=_blank navigation.");
415+
final WebView interceptorWebView = new WebView(view.getContext());
416+
interceptorWebView.setWebViewClient(new WebViewClient() {
417+
@Override
418+
public boolean shouldOverrideUrlLoading(WebView v, WebResourceRequest request) {
419+
handleInterceptedUrlFromNewWindow(view, v, request, span, isUserGesture);
420+
return true;
421+
}
422+
});
423+
final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
424+
transport.setWebView(interceptorWebView);
425+
resultMsg.sendToTarget();
426+
// Span status and end are handled in handleInterceptedUrlFromNewWindow,
427+
// which fires asynchronously when shouldOverrideUrlLoading is called.
428+
windowHandled = true;
429+
} catch (@NonNull final Exception e) {
430+
Logger.error(methodTag, "Error handling target=_blank navigation.", e);
431+
span.recordException(e);
432+
span.setStatus(StatusCode.ERROR);
433+
span.end();
434+
}
435+
return windowHandled;
436+
}
379437
});
380438
setupPasskeyWebListener(mWebView, webViewClient);
381439
}
382440

441+
/**
442+
* Handles the URL intercepted from a target=_blank navigation (onCreateWindow).
443+
* Routes the URL based on whether the main WebView is currently on a TLR page:
444+
* - TLR page: opens the URL in an external browser.
445+
* - Non-TLR page: loads the URL inline in the main WebView.
446+
*
447+
* @param mainWebView The main authentication WebView.
448+
* @param interceptorWebView The temporary interceptor WebView (will be destroyed after handling).
449+
* @param request The intercepted URL request.
450+
* @param span The telemetry span to record which routing path is taken.
451+
* @param isUserGesture Whether the popup was initiated by a user gesture (e.g. a click).
452+
*/
453+
@VisibleForTesting
454+
void handleInterceptedUrlFromNewWindow(@NonNull final WebView mainWebView,
455+
@NonNull final WebView interceptorWebView,
456+
@NonNull final WebResourceRequest request,
457+
@NonNull final Span span,
458+
final boolean isUserGesture) {
459+
final String methodTag = TAG + ":handleInterceptedUrlFromNewWindow";
460+
try {
461+
final String targetUrl = request.getUrl().toString();
462+
final String currentPageUrl = mainWebView.getUrl();
463+
464+
if (targetUrl == null) {
465+
span.setAttribute(AttributeName.target_blank_navigation_route.name(), AuthenticationConstants.Broker.WEBVIEW_TARGET_BLANK_ROUTE_NULL_URL);
466+
Logger.warn(methodTag, "onCreateWindow: target URL is null, ignoring.");
467+
} else if (!isUserGesture) {
468+
// Not initiated by user gesture: load inline as a safe fallback instead of
469+
// opening an external browser, to prevent programmatic/scripted popups.
470+
span.setAttribute(AttributeName.target_blank_navigation_route.name(), AuthenticationConstants.Broker.WEBVIEW_TARGET_BLANK_ROUTE_NO_USER_GESTURE);
471+
Logger.warn(methodTag, "onCreateWindow: popup not initiated by user gesture, loading URL inline.");
472+
mainWebView.loadUrl(targetUrl);
473+
} else if (!targetUrl.toLowerCase().startsWith(AuthenticationConstants.Broker.REDIRECT_SSL_PREFIX)) {
474+
// Non-SSL URL: refuse to open, matching AzureActiveDirectoryWebViewClient behavior.
475+
span.setAttribute(AttributeName.target_blank_navigation_route.name(), AuthenticationConstants.Broker.WEBVIEW_TARGET_BLANK_ROUTE_NON_SSL);
476+
Logger.error(methodTag, "onCreateWindow: URL is not SSL protected, refusing to open.", null);
477+
} else if (!isTlrUrl(currentPageUrl)) {
478+
// Non-TLR page: load inline, same as WebView default behavior.
479+
span.setAttribute(AttributeName.target_blank_navigation_route.name(), AuthenticationConstants.Broker.WEBVIEW_TARGET_BLANK_ROUTE_NON_TLR);
480+
Logger.warn(methodTag, "onCreateWindow: non-TLR page, loading URL inline as fallback.");
481+
mainWebView.loadUrl(targetUrl);
482+
} else {
483+
// TLR page: delegate to system browser so user can view terms externally.
484+
span.setAttribute(AttributeName.target_blank_navigation_route.name(), AuthenticationConstants.Broker.WEBVIEW_TARGET_BLANK_ROUTE_TLR);
485+
Logger.info(methodTag, "onCreateWindow: TLR page, delegating URL to system browser.");
486+
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(targetUrl));
487+
mainWebView.getContext().startActivity(browserIntent);
488+
}
489+
span.setStatus(StatusCode.OK);
490+
} catch (final Exception e) {
491+
span.recordException(e);
492+
span.setStatus(StatusCode.ERROR);
493+
Logger.error(methodTag, "Error handling target=_blank URL.", e);
494+
} finally {
495+
span.end();
496+
// Destroy the interceptor WebView after it has served its purpose
497+
interceptorWebView.post(interceptorWebView::destroy);
498+
}
499+
}
500+
501+
/**
502+
* Checks whether the given URL corresponds to a TLR (Terms, License, and Restrictions)
503+
* start page.
504+
*
505+
* @param url The URL to check.
506+
* @return {@code true} if the URL is a TLR start page, {@code false} otherwise.
507+
*/
508+
@VisibleForTesting
509+
boolean isTlrUrl(@Nullable final String url) {
510+
if (url == null) {
511+
return false;
512+
}
513+
final String lowerUrl = url.toLowerCase();
514+
return lowerUrl.startsWith(AuthenticationConstants.Broker.REDIRECT_SSL_PREFIX)
515+
&& lowerUrl.contains(AuthenticationConstants.Broker.TLR_START_PATH);
516+
}
517+
383518
/**
384519
* Loads starting authorization request url into WebView.
385520
*/

common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@ else if (isRedirectUrl(formattedURL)) {
338338
} else if (isAmazonAppRedirect(formattedURL)) {
339339
Logger.info(methodTag, "It is an Amazon app request");
340340
processAmazonAppUri(url);
341+
} else if (isOpenIdVcUrl(formattedURL)) {
342+
// TO-DO : Decide whether flight is needed in this case. https://identitydivision.visualstudio.com/Engineering/_workitems/edit/3541420
343+
Logger.info(methodTag, "It is an OpenID Verifiable Credentials request.");
344+
processOpenIdVcRequest(view, url);
341345
} else if (isInvalidRedirectUri(url)) {
342346
Logger.info(methodTag,"Check for Redirect Uri.");
343347
processInvalidRedirectUri(view, url);
@@ -496,6 +500,18 @@ private boolean isAmazonAppRedirect(@NonNull final String url) {
496500
return url.startsWith(AMAZON_APP_REDIRECT_PREFIX);
497501
}
498502

503+
/**
504+
* Checks if the URL uses the openid-vc:// custom scheme.
505+
* This scheme is used by OpenID Verifiable Credentials flows and must be
506+
* intercepted so a registered wallet app can handle it.
507+
*
508+
* @param url The lowercase URL to check.
509+
* @return true if the URL starts with the openid-vc:// scheme.
510+
*/
511+
private boolean isOpenIdVcUrl(@NonNull final String url) {
512+
return url.startsWith(AuthenticationConstants.Broker.OPENID_VC_SCHEME_PREFIX);
513+
}
514+
499515
private boolean isWebCpEnrollmentUrl(@NonNull final String url) {
500516
return url.startsWith(AuthenticationConstants.Broker.WEBCP_ENROLLMENT_URL);
501517
}
@@ -901,6 +917,43 @@ private void processAmazonAppUri(@NonNull final String url) {
901917
Logger.info(methodTag, "Sent Intent to launch Amazon app");
902918
}
903919

920+
/**
921+
* Handles an openid-vc:// URL by stopping the WebView and launching an
922+
* {@link Intent#ACTION_VIEW} intent so the system can route it to the
923+
* registered wallet application.
924+
*
925+
* @param view The WebView that intercepted the navigation.
926+
* @param url The original (non-lowercased) openid-vc:// URL.
927+
*/
928+
private void processOpenIdVcRequest(@NonNull final WebView view, @NonNull final String url) {
929+
final String methodTag = TAG + ":processOpenIdVcRequest";
930+
view.stopLoading();
931+
final Span span = createSpanWithAttributesFromParent(SpanName.ProcessOpenIdVcRequest.name());
932+
try (final Scope scope = SpanExtension.makeCurrentSpan(span)) {
933+
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
934+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
935+
if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
936+
getActivity().startActivity(intent);
937+
Logger.info(methodTag, "Launched external handler for OpenID VC request.");
938+
span.setAttribute(AttributeName.is_openid_vc_handler_found.name(), true);
939+
span.setStatus(StatusCode.OK);
940+
} else {
941+
Logger.warn(methodTag, "No application found to handle openid-vc:// URI.");
942+
span.setAttribute(AttributeName.is_openid_vc_handler_found.name(), false);
943+
span.setStatus(StatusCode.ERROR, "No handler found for openid-vc:// URI");
944+
returnError(ErrorStrings.ACTIVITY_NOT_FOUND, "No application found to handle the OpenID Verifiable Credentials request.");
945+
}
946+
} catch (final ActivityNotFoundException e) {
947+
Logger.error(methodTag, "Failed to launch handler for openid-vc:// URI.", e);
948+
span.setAttribute(AttributeName.is_openid_vc_handler_found.name(), false);
949+
span.recordException(e);
950+
span.setStatus(StatusCode.ERROR, "Failed to launch handler for openid-vc:// URI");
951+
returnError(ErrorStrings.ACTIVITY_NOT_FOUND, "Failed to launch handler for the OpenID Verifiable Credentials request.");
952+
} finally {
953+
span.end();
954+
}
955+
}
956+
904957
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
905958
protected void openLinkInBrowser(final String url) {
906959
final String methodTag = TAG + ":openLinkInBrowser";

0 commit comments

Comments
 (0)