diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java index e5073ccd8f..d21b37d84c 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java @@ -36,6 +36,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.ViewTreeLifecycleOwner; import com.microsoft.identity.common.adal.internal.AuthenticationConstants; @@ -643,8 +644,13 @@ private void processCloudRedirectAndPrtHeader(@NonNull final WebView view, @NonN final SpanContext spanContext = getActivity() instanceof AuthorizationActivity ? ((AuthorizationActivity) getActivity()).getSpanContext() : null; final Span span = spanContext != null ? OTelUtility.createSpanFromParent(SpanName.ProcessCrossCloudRedirect.name(), spanContext) : OTelUtility.createSpan(SpanName.ProcessCrossCloudRedirect.name()); + final CrossCloudChallengeHandler crossCloudChallengeHandler = new CrossCloudChallengeHandler(view, mRequestHeaders, span); + processCloudRedirectAndPrtHeaderInternal(url, crossCloudChallengeHandler, view, methodTag, span); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public void processCloudRedirectAndPrtHeaderInternal(@NonNull final String url, @NonNull final CrossCloudChallengeHandler crossCloudChallengeHandler, @NonNull final WebView view, @NonNull final String methodTag, @NonNull final Span span) { try (final Scope scope = SpanExtension.makeCurrentSpan(span)) { - final CrossCloudChallengeHandler crossCloudChallengeHandler = new CrossCloudChallengeHandler(view, mRequestHeaders, span); crossCloudChallengeHandler.processChallenge(url); span.setStatus(StatusCode.OK); } catch (final Exception e) { diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/CrossCloudChallengeHandler.kt b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/CrossCloudChallengeHandler.kt index 0cb32b16cf..13e38ec47e 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/CrossCloudChallengeHandler.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/CrossCloudChallengeHandler.kt @@ -50,7 +50,8 @@ class CrossCloudChallengeHandler( } // Updates the headers by attaching a refresh token credential header. - private fun modifyHeadersWithRefreshTokenCredential( + // Making it accessible for testing. + fun modifyHeadersWithRefreshTokenCredential( url: String, ) { val methodTag = "$TAG:modifyHeadersWithRefreshTokenCredential" diff --git a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java index 1143a5a2dd..fff4ab35a1 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java @@ -30,10 +30,14 @@ import androidx.test.core.app.ApplicationProvider; import com.microsoft.identity.common.adal.internal.AuthenticationConstants; +import com.microsoft.identity.common.internal.ui.webview.challengehandlers.CrossCloudChallengeHandler; +import com.microsoft.identity.common.java.exception.ClientException; +import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectory; import com.microsoft.identity.common.internal.ui.webview.challengehandlers.SwitchBrowserRequestHandler; import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback; import com.microsoft.identity.common.java.providers.RawAuthorizationResult; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,6 +53,8 @@ import java.util.HashMap; +import io.opentelemetry.api.trace.Span; + @RunWith(RobolectricTestRunner.class) public class AzureActiveDirectoryWebViewClientTest { @@ -82,9 +88,11 @@ public class AzureActiveDirectoryWebViewClientTest { private static final String TEST_MSA_HEADER_FORWARDING_NEGATIVE_URL = "https://login.blah.com/oauth20_authorize.srf"; 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"; @Before - public void setup() { + public void setup() throws ClientException { mContext = ApplicationProvider.getApplicationContext(); mMockWebView = new WebView(mContext); mActivity = Robolectric.buildActivity(Activity.class).get(); @@ -112,6 +120,10 @@ public void onPageLoaded(final String url) { HashMap dummyHeaders = new HashMap<>(); dummyHeaders.put("key", "value"); mWebViewClient.setRequestHeaders(dummyHeaders); + mWebViewClient.setRequestUrl(TEST_PUBLIC_CLOUD_REDIRECT_URL); + if (!AzureActiveDirectory.isInitialized()) { + AzureActiveDirectory.performCloudDiscovery(); + } } @Test(expected = IllegalArgumentException.class) @@ -193,4 +205,33 @@ public void testUrlOverrideHandlesHeaderForwardingRequiredUrl() { public void testUrlOverrideHandlesNonceRedirectUrl() { assertTrue(mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_NONCE_REDIRECT_URL)); } + + @Test + public void testUrlOverrideHandlesCrossCloudRedirectUrl() { + assertTrue(mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_CROSS_CLOUD_REDIRECT_URL)); + } + + @Test + public void testProcessCloudRedirectAndPrtHeaderInternalSuccess() { + CrossCloudChallengeHandler mockCrossCloudChallengeHandler = Mockito.mock(CrossCloudChallengeHandler.class); + try { + mWebViewClient.processCloudRedirectAndPrtHeaderInternal(TEST_CROSS_CLOUD_REDIRECT_URL, mockCrossCloudChallengeHandler, mMockWebView, "methodTag", Span.current()); + } catch (Exception e) { + Assert.fail("Unexpected exception occured " + e); + } + } + + @Test + public void testProcessCloudRedirectAndPrtHeaderInternalException() { + CrossCloudChallengeHandler mockCrossCloudChallengeHandler = Mockito.mock(CrossCloudChallengeHandler.class); + WebView mockWebView = Mockito.mock(WebView.class); + Mockito.doThrow(new RuntimeException("Test Exception")).when(mockCrossCloudChallengeHandler).processChallenge(TEST_CROSS_CLOUD_REDIRECT_URL); + try { + mWebViewClient.processCloudRedirectAndPrtHeaderInternal(TEST_CROSS_CLOUD_REDIRECT_URL, mockCrossCloudChallengeHandler, mockWebView, "methodTag", Span.current()); + Mockito.verify(mockCrossCloudChallengeHandler, Mockito.times(1)).processChallenge(TEST_CROSS_CLOUD_REDIRECT_URL); + Mockito.verify(mockWebView).loadUrl(Mockito.anyString(), Mockito.any()); + } catch (Exception e) { + Assert.fail("Failure is not expected. We should have caught the exception and ignored it. " + e); + } + } } diff --git a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/CrossCloudChallengeHandlerTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/CrossCloudChallengeHandlerTest.kt new file mode 100644 index 0000000000..56c2891269 --- /dev/null +++ b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/CrossCloudChallengeHandlerTest.kt @@ -0,0 +1,121 @@ +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.internal.ui.webview.challengehandlers + +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import android.webkit.WebView +import com.microsoft.identity.common.java.broker.CommonRefreshTokenCredentialProvider +import com.microsoft.identity.common.java.opentelemetry.AttributeName +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.opentelemetry.api.trace.Span +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.* + +@RunWith(RobolectricTestRunner::class) +class CrossCloudChallengeHandlerTest { + + private lateinit var webView: WebView + private lateinit var headers: HashMap + private lateinit var span: Span + private lateinit var crossCloudChallengeHandler: CrossCloudChallengeHandler + + @Before + fun setUp() { + webView = mock(WebView::class.java) + headers = HashMap() + span = mock(Span::class.java) + crossCloudChallengeHandler = CrossCloudChallengeHandler(webView, headers, span) + } + + @Test + fun `testProcessChallenge success`() { + val testUrl = "https://example.com?login_hint=testuser" + crossCloudChallengeHandler.processChallenge(testUrl) + verify(webView).loadUrl(eq(testUrl), eq(headers)) + } + + @Test + fun `testProcessChallenge when exception is thrown`() { + val testUrl = "https://example.com?login_hint=testuser" + + mockkObject(CommonRefreshTokenCredentialProvider) + every { + CommonRefreshTokenCredentialProvider.getRefreshTokenCredential( + testUrl, + "testuser" + ) + } throws Exception() + + try { + crossCloudChallengeHandler.processChallenge(testUrl) + } catch (e: Exception) { + verify(webView, never()).loadUrl(eq(testUrl), eq(headers)) + } + } + + @Test + fun `modifyHeadersWithRefreshTokenCredential should update headers when prt is available`() { + val url = "https://login.microsoftonline.com?login_hint=testuser" + val username = "testuser" + val refreshTokenCredential = "refreshTokenCredential" + + mockkObject(CommonRefreshTokenCredentialProvider) + every { + CommonRefreshTokenCredentialProvider.getRefreshTokenCredential( + url, + username + ) + } returns refreshTokenCredential + + // Call the method + crossCloudChallengeHandler.modifyHeadersWithRefreshTokenCredential(url) + verify(span).setAttribute( + AttributeName.is_new_refresh_token_cred_header_attached.name, + true + ) + unmockkAll() + } + + @Test + fun `modifyHeadersWithRefreshTokenCredential should not update headers when login_hint is missing`() { + val url = "https://login.microsoftonline.com" + crossCloudChallengeHandler.modifyHeadersWithRefreshTokenCredential(url) + verify(span, never()).setAttribute( + AttributeName.is_new_refresh_token_cred_header_attached.name, + true + ) + } + + @Test + fun `modifyHeadersWithRefreshTokenCredential null refresh token credential`() { + val url = "https://login.microsoftonline.com?login_hint=testuser" + crossCloudChallengeHandler.modifyHeadersWithRefreshTokenCredential(url) + verify(span, never()).setAttribute( + AttributeName.is_new_refresh_token_cred_header_attached.name, + true + ) + } +}