Skip to content

Commit f279e85

Browse files
authored
Updating handling of ssl error received in Android WebView's onReceivedSslError callback, Fixes AB#3268908 (#2691)
Update: After talking to @iulico-1 from OneAuth. 1. First rolling out with Broker and keeping disabled for OneAuth (and MSAL Android). Added SSL error telemetry 2. Enable for oneauth after we see no issues in Broker. So, the toggle code to let OneAuth enable/disable is removed from the PR. 3. Investigate how we can pass the error to OneAuth to track SSL failure and User canceled the flow because of SSL error. ===================== Fixes [AB#3268908](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3268908) **Summary:** This PR updates the handling of SSL errors in the Android WebView used for authentication. The change is in response to an incident where the MSA sign-up flow failed due to an expired certificate on an MSA UX web page. The expired certificate was associated with a resource loaded by the page; however, the existing SSL error handling in Common’s WebView would cancel the loading of that resource and subsequently terminate the entire authentication flow. (See PBI for details.) **Solution** **Option 1** We simply log and stop loading the resource that caused the error by calling `SssHandler.cancel()` but not stop the flow by erroring out. This is a simple change. The only thing is the main URL (the url loading in the WebView) itself has SSL error then the flow would end up in *blank page*. This is as bad as finishing the flow but does not inform calling app of the error via exception. But if sub-resource has the error, then the flow continues. **Flighting** **New SSL Error Handler**: - ~~Adds a new intent key (`WEB_VIEW_NEW_SSL_ERROR_HANDLER_ENABLED`) in `AuthenticationConstants.java` to control whether the new SSL error handler logic is enabled. This can be used by OneAuth to opt-in or opt-out. If the key is not passed, then Common's default logic based on flighting will be used. If the key passed, that will be used (primarily meant for OneAuth/MSALCPP).~~. The toggle would be added later if required. This simplifies current logic to only rely on Common/broker's flighting. - Adds a new flight value is well defaulting to `false`. So, OneAuth and MSAL would also use default flight value and continue using current behavior. For Broker, we would enable new flow with default `true` (AzureAD/ad-accounts-for-android#3141), which can be controlled remotely to turn off if required. **Option 2** (Not this PR, added for reference) The objective of this update is to improve SSL error handling so that the flow is only cancelled if an SSL error occurs on the main frame resource (the primary page being loaded). If an SSL error affects a sub-resource, only the loading of that specific resource is cancelled, and the overall authentication flow continues. This approach helps prevent unnecessary failures caused by SSL errors on secondary or non-critical resources. (#2688) The con of the approach is client needs to track the active url that's about and being loaded in WebView. The tracking adds cost of developing and maintaining to solution without significant benefit over option 1.
1 parent af4dbe5 commit f279e85

File tree

6 files changed

+143
-49
lines changed

6 files changed

+143
-49
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
vNext
22
----------
3+
- [MINOR] Updating handling of ssl error received in Android WebView's onReceivedSslError callback (#2691)
34
- [MINOR] Fixing the sign in screens when edge to edge is enabled (#2665)
45
- [MINOR] Showing webcp flow in webview (#2673)
56
- [MINOR] Native auth: Make native auth MFA feature more backward compatible(#2669)

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@
2222
// THE SOFTWARE.
2323
package com.microsoft.identity.common.internal.providers.oauth2;
2424

25+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.AUTH_INTENT;
26+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.POST_PAGE_LOADED_URL;
27+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REDIRECT_URI;
28+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REQUEST_HEADERS;
29+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REQUEST_URL;
30+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.WEB_VIEW_ZOOM_CONTROLS_ENABLED;
31+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.WEB_VIEW_ZOOM_ENABLED;
32+
import static com.microsoft.identity.common.java.AuthenticationConstants.SdkPlatformFields.PRODUCT;
33+
import static com.microsoft.identity.common.java.AuthenticationConstants.SdkPlatformFields.VERSION;
34+
2535
import android.annotation.SuppressLint;
2636
import android.app.Activity;
2737
import android.content.Context;
@@ -46,40 +56,30 @@
4656
import androidx.fragment.app.FragmentActivity;
4757

4858
import com.microsoft.identity.common.R;
49-
import com.microsoft.identity.common.internal.fido.LegacyFidoActivityResultContract;
50-
import com.microsoft.identity.common.internal.fido.LegacyFido2ApiObject;
51-
import com.microsoft.identity.common.internal.ui.webview.ISendResultCallback;
52-
import com.microsoft.identity.common.internal.ui.webview.ProcessUtil;
53-
import com.microsoft.identity.common.internal.ui.webview.switchbrowser.SwitchBrowserProtocolCoordinator;
54-
import com.microsoft.identity.common.java.WarningType;
5559
import com.microsoft.identity.common.adal.internal.AuthenticationConstants;
5660
import com.microsoft.identity.common.adal.internal.util.StringExtensions;
61+
import com.microsoft.identity.common.internal.fido.LegacyFido2ApiObject;
62+
import com.microsoft.identity.common.internal.fido.LegacyFidoActivityResultContract;
5763
import com.microsoft.identity.common.internal.ui.webview.AzureActiveDirectoryWebViewClient;
64+
import com.microsoft.identity.common.internal.ui.webview.ISendResultCallback;
5865
import com.microsoft.identity.common.internal.ui.webview.OnPageLoadedCallback;
66+
import com.microsoft.identity.common.internal.ui.webview.ProcessUtil;
5967
import com.microsoft.identity.common.internal.ui.webview.WebViewUtil;
68+
import com.microsoft.identity.common.internal.ui.webview.switchbrowser.SwitchBrowserProtocolCoordinator;
69+
import com.microsoft.identity.common.java.WarningType;
6070
import com.microsoft.identity.common.java.constants.FidoConstants;
6171
import com.microsoft.identity.common.java.exception.ClientException;
6272
import com.microsoft.identity.common.java.flighting.CommonFlight;
6373
import com.microsoft.identity.common.java.flighting.CommonFlightsManager;
64-
import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback;
6574
import com.microsoft.identity.common.java.providers.RawAuthorizationResult;
75+
import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback;
6676
import com.microsoft.identity.common.java.util.ClientExtraSku;
6777
import com.microsoft.identity.common.logging.Logger;
6878

69-
import java.util.Arrays;
70-
import java.util.HashMap;
7179
import java.io.UnsupportedEncodingException;
7280
import java.net.URLEncoder;
73-
74-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.AUTH_INTENT;
75-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.POST_PAGE_LOADED_URL;
76-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REDIRECT_URI;
77-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REQUEST_HEADERS;
78-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REQUEST_URL;
79-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.WEB_VIEW_ZOOM_CONTROLS_ENABLED;
80-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.WEB_VIEW_ZOOM_ENABLED;
81-
import static com.microsoft.identity.common.java.AuthenticationConstants.SdkPlatformFields.PRODUCT;
82-
import static com.microsoft.identity.common.java.AuthenticationConstants.SdkPlatformFields.VERSION;
81+
import java.util.Arrays;
82+
import java.util.HashMap;
8383

8484
import io.opentelemetry.api.trace.SpanContext;
8585

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

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
// THE SOFTWARE.
2323
package com.microsoft.identity.common.internal.ui.webview;
2424

25+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Browser.SSL_HELP_URL;
26+
2527
import android.app.Activity;
2628
import android.graphics.Bitmap;
2729
import android.net.Uri;
@@ -41,23 +43,33 @@
4143

4244
import com.microsoft.identity.common.adal.internal.AuthenticationConstants;
4345
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.ChallengeFactory;
44-
import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback;
4546
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.IChallengeHandler;
4647
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.NtlmChallenge;
4748
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.NtlmChallengeHandler;
4849
import com.microsoft.identity.common.internal.util.StringUtil;
4950
import com.microsoft.identity.common.java.exception.ClientException;
51+
import com.microsoft.identity.common.java.flighting.CommonFlight;
52+
import com.microsoft.identity.common.java.flighting.CommonFlightsManager;
53+
import com.microsoft.identity.common.java.logging.DiagnosticContext;
54+
import com.microsoft.identity.common.java.opentelemetry.AttributeName;
55+
import com.microsoft.identity.common.java.opentelemetry.OTelUtility;
5056
import com.microsoft.identity.common.java.providers.RawAuthorizationResult;
57+
import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback;
5158
import com.microsoft.identity.common.logging.Logger;
5259

53-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Browser.SSL_HELP_URL;
54-
5560
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
61+
import io.opentelemetry.api.common.Attributes;
62+
import io.opentelemetry.api.metrics.LongCounter;
5663

5764
public abstract class OAuth2WebViewClient extends WebViewClient {
5865
/* constants */
5966
private static final String TAG = OAuth2WebViewClient.class.getSimpleName();
6067

68+
private static final LongCounter sWebViewSslErrorCount = OTelUtility.createLongCounter(
69+
"web_view_ssl_error_count",
70+
"Number of SSL errors received in onReceivedSslError"
71+
);
72+
6173
private final IAuthorizationCompletionCallback mCompletionCallback;
6274
private final OnPageLoadedCallback mPageLoadedCallback;
6375
private final Activity mActivity;
@@ -89,12 +101,13 @@ IAuthorizationCompletionCallback getCompletionCallback() {
89101
*/
90102
OAuth2WebViewClient(@NonNull final Activity activity,
91103
@NonNull final IAuthorizationCompletionCallback completionCallback,
92-
@NonNull final OnPageLoadedCallback pageLoadedCallback) {
93-
//the validation of redirect url and authorization request should be in upper level before launching the webview.
104+
@NonNull final OnPageLoadedCallback pageLoadedCallback
105+
) {
106+
// the validation of redirect url and authorization request should be in upper level before launching the webview.
94107
mActivity = activity;
95108
mCompletionCallback = completionCallback;
96109
mPageLoadedCallback = pageLoadedCallback;
97-
}
110+
}
98111

99112
@Override
100113
public void onReceivedHttpAuthRequest(WebView view, final HttpAuthHandler handler,
@@ -171,17 +184,21 @@ public void onReceivedSslError(final WebView view,
171184
final SslErrorHandler handler,
172185
final SslError error) {
173186
// Developer does not have option to control this for now
187+
final String methodTag = TAG + ":onReceivedSslError";
174188
super.onReceivedSslError(view, handler, error);
175-
handler.cancel();
176-
177-
final String errMsg = "Received SSL Error during request. For more info see: " + SSL_HELP_URL;
178-
179-
Logger.error(TAG + ":onReceivedSslError", errMsg, null);
180-
181-
// Send the result back to the calling activity
182-
mCompletionCallback.onChallengeResponseReceived(
183-
RawAuthorizationResult.fromException(
184-
new ClientException("Code:" + ERROR_FAILED_SSL_HANDSHAKE, error.toString())));
189+
final String errMsg = "Received SSL Error during request. For more info see: " + SSL_HELP_URL + ". Error: " + error.toString();
190+
191+
Logger.warn(methodTag, errMsg);
192+
final Attributes attributes = Attributes.builder()
193+
.put(AttributeName.web_view_ssl_primary_error_code.name(), error.getPrimaryError())
194+
.build();
195+
sWebViewSslErrorCount.add(1, attributes);
196+
if (!CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.SHOULD_PRESERVE_WEBVIEW_FLOW_ON_SSL_ERROR)) {
197+
// Send the result back to the calling activity
198+
mCompletionCallback.onChallengeResponseReceived(
199+
RawAuthorizationResult.fromException(
200+
new ClientException("Code:" + ERROR_FAILED_SSL_HANDSHAKE, error.toString())));
201+
}
185202
}
186203

187204
@Override

common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,20 @@
2222
// THE SOFTWARE.
2323
package com.microsoft.identity.common.internal.ui.webview;
2424

25+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.AUTHENTICATOR_MFA_LINKING_PREFIX;
26+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.COMPANY_PORTAL_APP_PACKAGE_NAME;
27+
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.PLAY_STORE_INSTALL_PREFIX;
28+
import static org.junit.Assert.assertFalse;
29+
import static org.junit.Assert.assertTrue;
30+
import static org.mockito.ArgumentMatchers.any;
31+
import static org.mockito.ArgumentMatchers.eq;
32+
import static org.mockito.Mockito.never;
33+
import static org.mockito.Mockito.when;
34+
2535
import android.app.Activity;
2636
import android.content.Context;
37+
import android.net.http.SslError;
38+
import android.webkit.SslErrorHandler;
2739
import android.webkit.WebView;
2840

2941
import androidx.annotation.NonNull;
@@ -33,14 +45,15 @@
3345
import com.microsoft.identity.common.internal.mocks.MockCommonFlightsManager;
3446
import com.microsoft.identity.common.internal.ui.DualScreenActivity;
3547
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.ReAttachPrtHeaderHandler;
48+
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.SwitchBrowserRequestHandler;
3649
import com.microsoft.identity.common.java.exception.ClientException;
3750
import com.microsoft.identity.common.java.flighting.CommonFlight;
3851
import com.microsoft.identity.common.java.flighting.CommonFlightsManager;
52+
import com.microsoft.identity.common.java.flighting.IFlightsManager;
3953
import com.microsoft.identity.common.java.flighting.IFlightsProvider;
54+
import com.microsoft.identity.common.java.providers.RawAuthorizationResult;
4055
import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectory;
41-
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.SwitchBrowserRequestHandler;
4256
import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback;
43-
import com.microsoft.identity.common.java.providers.RawAuthorizationResult;
4457
import com.microsoft.identity.common.shadows.ShadowProcessUtil;
4558

4659
import org.junit.Assert;
@@ -52,18 +65,13 @@
5265
import org.robolectric.RobolectricTestRunner;
5366
import org.robolectric.annotation.Config;
5467

55-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.AUTHENTICATOR_MFA_LINKING_PREFIX;
56-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.COMPANY_PORTAL_APP_PACKAGE_NAME;
57-
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.PLAY_STORE_INSTALL_PREFIX;
58-
import static org.junit.Assert.assertFalse;
59-
import static org.junit.Assert.assertTrue;
60-
import static org.mockito.Mockito.when;
61-
6268
import java.util.HashMap;
6369

6470
import io.opentelemetry.api.trace.Span;
6571

66-
72+
/**
73+
* Tests for {@link AzureActiveDirectoryWebViewClient}.
74+
*/
6775
@RunWith(RobolectricTestRunner.class)
6876
public class AzureActiveDirectoryWebViewClientTest {
6977
private WebView mMockWebView;
@@ -129,7 +137,8 @@ public void onPageLoaded(final String url) {
129137
}
130138
},
131139
TEST_REDIRECT_URI,
132-
Mockito.mock(SwitchBrowserRequestHandler.class));
140+
Mockito.mock(SwitchBrowserRequestHandler.class)
141+
);
133142
HashMap<String, String> dummyHeaders = new HashMap<>();
134143
dummyHeaders.put("key", "value");
135144
mWebViewClient.setRequestHeaders(dummyHeaders);
@@ -374,12 +383,67 @@ public void onPageLoaded(final String url) {
374383
}
375384
},
376385
TEST_REDIRECT_URI,
377-
Mockito.mock(SwitchBrowserRequestHandler.class));
386+
Mockito.mock(SwitchBrowserRequestHandler.class)
387+
);
378388
mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_PASSKEY_REDIRECT_URL);
379389
} catch (ClassCastException e) {
380390
Assert.fail("Failure is not expected. The class checks should have prevented this." + e);
381391
} catch (Exception e) {
382392
Assert.fail("Failure is not expected." + e);
383393
}
384394
}
395+
396+
@Test
397+
public void testOnReceivedSslError_Legacy() {
398+
final String mockActiveUrl = "https://login.microsoftonline.com/organizations/oAuth2/v2.0/authorize";
399+
final SslErrorHandler mockHandler = Mockito.mock(android.webkit.SslErrorHandler.class);
400+
final SslError mockError = Mockito.mock(android.net.http.SslError.class);
401+
final IAuthorizationCompletionCallback mockCallback = Mockito.mock(IAuthorizationCompletionCallback.class);
402+
when(mockError.getUrl()).thenReturn("https://example.com");
403+
final AzureActiveDirectoryWebViewClient mockWebViewClient = new AzureActiveDirectoryWebViewClient(
404+
mActivity,
405+
mockCallback,
406+
url -> {},
407+
TEST_REDIRECT_URI,
408+
Mockito.mock(SwitchBrowserRequestHandler.class));
409+
final WebView mockWebView = new WebView(mContext);
410+
mockWebView.setWebViewClient(mockWebViewClient);
411+
412+
// act
413+
mockWebViewClient.onReceivedSslError(mockWebView, mockHandler, mockError);
414+
415+
Mockito.verify(mockHandler, Mockito.times(1)).cancel();
416+
Mockito.verify(mockCallback, Mockito.times(1)).onChallengeResponseReceived(any());
417+
}
418+
419+
@Test
420+
public void testOnReceivedSslError() {
421+
final IFlightsManager mockFlightsManager = Mockito.mock(IFlightsManager.class);
422+
final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class);
423+
when(mockFlightsProvider.isFlightEnabled(eq(CommonFlight.SHOULD_PRESERVE_WEBVIEW_FLOW_ON_SSL_ERROR))).thenReturn(true);
424+
when(mockFlightsManager.getFlightsProvider()).thenReturn(mockFlightsProvider);
425+
CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockFlightsManager);
426+
final SslErrorHandler mockHandler = Mockito.mock(android.webkit.SslErrorHandler.class);
427+
final SslError mockError = Mockito.mock(android.net.http.SslError.class);
428+
final IAuthorizationCompletionCallback mockCallback = Mockito.mock(IAuthorizationCompletionCallback.class);
429+
when(mockError.getUrl()).thenReturn("https://example.com");
430+
final AzureActiveDirectoryWebViewClient mockWebViewClient = new AzureActiveDirectoryWebViewClient(
431+
mActivity,
432+
mockCallback,
433+
url -> {},
434+
TEST_REDIRECT_URI,
435+
Mockito.mock(SwitchBrowserRequestHandler.class)
436+
);
437+
final WebView mockWebView = new WebView(mContext);
438+
mockWebView.setWebViewClient(mockWebViewClient);
439+
440+
// act
441+
mockWebViewClient.onReceivedSslError(mockWebView, mockHandler, mockError);
442+
443+
// verify that the handler is cancelled and the callback is invoked
444+
Mockito.verify(mockHandler, Mockito.times(1)).cancel();
445+
Mockito.verify(mockCallback, never()).onChallengeResponseReceived(any());
446+
447+
CommonFlightsManager.INSTANCE.resetFlightsManager();
448+
}
385449
}

common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,13 @@ public enum CommonFlight implements IFlightConfig {
139139
/**
140140
* Flight to enable the Web CP for a tenant list.
141141
*/
142-
TENANT_LIST_TO_ENABLE_WEB_CP_IN_WEBVIEW("TenantListToEnableWebCpInWebView", "");
142+
TENANT_LIST_TO_ENABLE_WEB_CP_IN_WEBVIEW("TenantListToEnableWebCpInWebView", ""),
143+
144+
/**
145+
* Flight to enable the WebView flow to not cancel and preserve WebView flow on SSL errors.
146+
* The web resource running into SSL will itself not be loaded.
147+
*/
148+
SHOULD_PRESERVE_WEBVIEW_FLOW_ON_SSL_ERROR("ShouldPreserveWebViewFlowOnSslError", false);
143149

144150
private String key;
145151
private Object defaultValue;

common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,5 +378,11 @@ public enum AttributeName {
378378
/**
379379
* Records if the webcp is enabled in webview.
380380
*/
381-
is_webcp_in_webview_enabled
381+
is_webcp_in_webview_enabled,
382+
383+
/**
384+
* Records the if webview received an SSL error and
385+
* corresponding primary error code.
386+
*/
387+
web_view_ssl_primary_error_code
382388
}

0 commit comments

Comments
 (0)