diff --git a/changelog.txt b/changelog.txt index 6afc424369..214f090d7d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [PATCH] Fix Switch browser back stack (#2750) - [MINOR] Move ests telemetry behind feature flag (#2742) - [MINOR] Update Broker ATS flow for Resource Account (#2704) - [PATCH] Handle BadTokenException gracefully in CBA dialogs (#2731) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationActivityFactory.kt b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationActivityFactory.kt index da7cc5140a..f17fed48d9 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationActivityFactory.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationActivityFactory.kt @@ -65,11 +65,6 @@ object AuthorizationActivityFactory { val libraryConfig = LibraryConfiguration.getInstance() if (ProcessUtil.isBrokerProcess(parameters.context)) { intent = Intent(parameters.context, BrokerAuthorizationActivity::class.java) - if (parameters.requestUrl.contains(AuthenticationConstants.SWITCH_BROWSER.CLIENT_SUPPORTS_FLOW)) { - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - // In the case of a SwitchBrowser protocol, we need to transition from the browser to the WebView. - // These flags ensure that we have a new task stack that allows for this transition. - } } else if (libraryConfig.isAuthorizationInCurrentTask && parameters.authorizationAgent != AuthorizationAgent.WEBVIEW) { // We exclude the case when the authorization agent is already selected as WEBVIEW because of confusion // that results from attempting to use the CurrentTaskAuthorizationActivity in that case, because as webview diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/BrokerAuthorizationActivity.java b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/BrokerAuthorizationActivity.java index f3df344522..bac2575359 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/BrokerAuthorizationActivity.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/BrokerAuthorizationActivity.java @@ -22,26 +22,9 @@ // THE SOFTWARE. package com.microsoft.identity.common.internal.providers.oauth2; -import android.content.Intent; - /** * Declares as a separate class so that we can specify attributes exclusively to :auth process * in AndroidManifest without overriding MSAL's (In case where MSAL and broker is shipped together). */ public class BrokerAuthorizationActivity extends AuthorizationActivity { - - /** - * Refreshes the WebView with new intent data after the user completes authentication in the browser. - * - *

In the Switch browser flow, once the user finishes authentication in the browser, ETS will send a request - * to the broker containing a code and an action URI. The broker will then send this request data back to the - * WebView authorization activity via an intent. This method is used to refresh the WebView with the new intent - * data that includes the code and action URI. - * see {@link WebViewAuthorizationFragment#onResume()} - */ - @Override - protected void onNewIntent(final Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - } } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/SwitchBrowserActivity.kt b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/SwitchBrowserActivity.kt new file mode 100644 index 0000000000..d1bd9ed564 --- /dev/null +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/SwitchBrowserActivity.kt @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. +// 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.providers.oauth2 + +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.FragmentActivity +import com.microsoft.identity.common.logging.Logger +import androidx.core.net.toUri +import com.microsoft.identity.common.internal.ui.browser.CustomTabsManager + + +/** + * Activity responsible for handling browser switching flows. + * + * This activity serves as an intermediary between the WebView-based authentication and external browser + * authentication. When a Switch Browser challenge is received in [WebViewAuthorizationFragment], this activity + * is launched to handle the browser switch operation. + * + * **Flow Overview:** + * 1. WebViewAuthorizationFragment receives a SwitchBrowser challenge + * 2. This activity is launched with browser configuration parameters + * 3. Activity launches the specified browser (Custom Tabs or standard browser) + * 4. User completes authentication in the external browser + * 5. BrokerBrowserRedirectActivity is launched when the redirect URI is triggered. + * 5. BrokerBrowserRedirectActivity redirects back to this activity via onNewIntent() + * 6. Activity passes the result back to WebViewAuthorizationFragment + * 7. Activity finishes and removes itself from the task stack + * + * Activity back stack behavior: + * 1 BrokerAuthorizationActivity hosting WebViewAuthorizationFragment --launches--> SwitchBrowserActivity in a new task. + * 2 SwitchBrowserActivity --launches--> 3rd Party Browser (Custom Tabs or standard browser) in current task. + * 3 3rd Party Browser --redirects to--> BrokerBrowserRedirectActivity in a new task. + * 4 BrokerBrowserRedirectActivity -- launches--> SwitchBrowserActivity in the existing task, and finishes current task. + * 5 SwitchBrowserActivity --passes result to--> WebViewAuthorizationFragment, and finishes current activity stack. + * + * **Security Note:** This activity is not exported and can only be launched within the app + * to prevent external apps from triggering unwanted browser switches. + * + * @see WebViewAuthorizationFragment + */ +class SwitchBrowserActivity : FragmentActivity() { + + // Flag to track if a Custom Chrome Tab (CCT) has been launched + private var cctLaunched = false + private var customTabsManager = CustomTabsManager(this) + + companion object { + private val TAG: String = SwitchBrowserActivity::class.java.simpleName + + /** Intent extra key for the target browser package name */ + const val BROWSER_PACKAGE_NAME = "browser_package_name" + + /** Intent extra key indicating if the browser supports Custom Tabs */ + const val BROWSER_SUPPORTS_CUSTOM_TABS = "browser_supports_custom_tabs" + + /** Intent extra key for the URI to process in the browser */ + const val PROCESS_URI = "process_uri" + + /** Intent extra key indicating a resume request from the browser redirect */ + const val RESUME_REQUEST = "resume_request" + } + + /** + * Initializes the activity and launches the appropriate browser for DUNA authentication. + * + * This method extracts the browser configuration from intent extras and launches either + * a Custom Tabs intent or a standard browser intent based on browser capabilities. + * + * @param savedInstanceState Saved instance state bundle (unused in this implementation) + */ + override fun onCreate(savedInstanceState: Bundle?) { + val methodTag = "$TAG:onCreate" + super.onCreate(savedInstanceState) + Logger.info(methodTag, "SwitchBrowserActivity created - Launching browser") + launchBrowser() + } + + + /** + * Launches the specified browser for DUNA authentication based on intent extras. + * + * This method reads the target browser package name, Custom Tabs support flag, + * and the process URI from the intent extras. It then constructs and launches + * either a Custom Tabs intent or a standard browser intent accordingly. + * + * If required parameters are missing, it logs an error and finishes the activity. + */ + private fun launchBrowser() { + val methodTag = "$TAG:launchBrowser" + cctLaunched = false + // Extract configuration parameters from intent extras + val extras = this.intent.extras ?: Bundle() + val browserPackageName = extras.getString(BROWSER_PACKAGE_NAME) + val browserSupportsCustomTabs = extras.getBoolean(BROWSER_SUPPORTS_CUSTOM_TABS, false) + val processUri = extras.getString(PROCESS_URI) + + // Validate required parameters + if (browserPackageName.isNullOrBlank()) { + Logger.error(methodTag, "No browser package name found in extras - Cannot proceed with browser switch", null) + finish() + return + } + if (processUri.isNullOrBlank()) { + Logger.error(methodTag, "No process URI found in extras - Cannot proceed with browser switch", null) + finish() + return + } + + Logger.info( + methodTag, + "Launching switch browser request on browser: $browserPackageName, Custom Tabs supported: $browserSupportsCustomTabs" + ) + + // Create an intent to launch the browser + val browserIntent: Intent + if (browserSupportsCustomTabs) { + Logger.info(methodTag, "CustomTabsService is supported.") + //create customTabsIntent + if (!customTabsManager.bind(this, browserPackageName)) { + Logger.warn(methodTag, "Failed to bind CustomTabsService.") + browserIntent = Intent(Intent.ACTION_VIEW) + } else { + browserIntent = customTabsManager.customTabsIntent.intent + } + } else { + Logger.warn(methodTag, "CustomTabsService is NOT supported") + browserIntent = Intent(Intent.ACTION_VIEW) + } + browserIntent.setPackage(browserPackageName) + browserIntent.setData(processUri.toUri()) + startActivity(browserIntent) + } + + /** + * Handles the redirect back from the browser after DUNA authentication completion. + * + * This method is called when the browser redirects back to the app with the authentication + * result. The intent contains the authentication response which is passed back to the + * WebViewAuthorizationFragment for processing. + * + * **Important:** This method also finishes the activity and removes it from the task stack + * to prevent it from remaining in the back stack after the authentication flow completes. + * + * @param intent The intent containing the authentication result from the browser redirect + */ + override fun onNewIntent(intent: Intent?) { + val methodTag = "$TAG:onNewIntent" + super.onNewIntent(intent) + // Update the activity's intent with the new intent containing the auth result + Logger.info(methodTag, "On new intent received.") + setIntent(intent) + + if (intent != null) { + if (intent.hasExtra(PROCESS_URI)) { + // Handle scenario where a new browser switch request is received while one is already in progress + // This can occur when the user initiates another auth request before completing the first one. + Logger.warn( + methodTag, + "Received new switch browser request while one is already in progress" + + " - Restarting browser switch flow" + ) + // Launch the new browser request, which will reset cctLaunched and start fresh + launchBrowser() + return + } + if (intent.hasExtra(RESUME_REQUEST)) { + WebViewAuthorizationFragment.setSwitchBrowserBundle(intent.extras) + // Clean up: finish this activity and remove it from task stack + Logger.info(methodTag, "Finishing activity and removing from task stack") + finishAndRemoveTask() + return + } + } + // Clean up: finish this activity and remove it from task stack + Logger.info(methodTag, "Unexpected intent - Finishing activity and removing from task stack") + finishAndRemoveTask() + } + + /** + * Handles the activity resume lifecycle event and manages Custom Chrome Tab (CCT) launch state. + * + * This method implements a critical part of the browser switch flow by tracking whether a Custom Chrome Tab + * has been launched and handling the case where the user returns to this activity without completing + * the authentication flow in the browser. + * + * **Behavior Logic:** + * - On first resume (after onCreate): Sets cctLaunched flag to true and continues normally + * - On subsequent resumes: If CCT was already launched, assumes user backed out of browser and finishes activity + * + * **Why This Logic is Needed:** + * When a Custom Chrome Tab is launched, this activity goes into the background. If the user presses the back + * button in the CCT or otherwise returns to this activity without completing authentication, we need to + * clean up and finish this activity to prevent it from remaining in the back stack. + * + * **Flow Scenarios:** + * 1. **Normal Flow**: onCreate → onResume (1st time) → CCT launched → user completes auth → onNewIntent → finish + * 2. **User Cancellation**: onCreate → onResume (1st time) → CCT launched → user backs out → onResume (2nd time) → finish + * + * **Important Notes:** + * - This prevents the activity from staying alive indefinitely if authentication is cancelled + * - Uses finishAndRemoveTask() to clean up the entire task stack, not just this activity + * - The cctLaunched flag is essential for distinguishing between the initial resume and subsequent resumes + */ + override fun onResume() { + super.onResume() + val methodTag = "$TAG:onResume" + Logger.info(methodTag, "onResume called - Managing CCT launch state") + + if (cctLaunched) { + // User has returned to this activity after CCT was launched, likely due to backing out + Logger.info(methodTag, "CCT was launched previously and user returned - Assuming cancellation, finishing activity") + finishAndRemoveTask() + } else { + // First resume after onCreate - mark CCT as launched for future reference + Logger.info(methodTag, "First resume after onCreate - Marking CCT as launched") + } + + cctLaunched = true + } + + override fun onDestroy() { + super.onDestroy() + customTabsManager.unbind() + } +} diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java index 91cbacc1cd..1b047f2187 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java @@ -33,7 +33,6 @@ import static com.microsoft.identity.common.java.AuthenticationConstants.SdkPlatformFields.VERSION; import android.annotation.SuppressLint; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -129,10 +128,13 @@ public class WebViewAuthorizationFragment extends AuthorizationFragment { private boolean isBrokerRequest = false; + private static Bundle switchBrowserBundle; + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); final String methodTag = TAG + ":onCreate"; + Logger.verbose(methodTag, "WebViewAuthorizationFragment onCreate"); final FragmentActivity activity = getActivity(); if (activity != null) { WebViewUtil.setDataDirectorySuffix(activity.getApplicationContext()); @@ -151,47 +153,36 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override public void onResume() { super.onResume(); + Logger.verbose(TAG + ":onResume", "WebViewAuthorizationFragment onResume"); if (getSwitchBrowserCoordinator().isExpectingSwitchBrowserResume()) { - resumeSwitchBrowser(getExtras()); - } - } - - /** - * Get the extras from the activity intent. - * - * @return Bundle with the extras - */ - @NonNull - private Bundle getExtras() { - final Activity activity = getActivity(); - if (activity == null) { - return Bundle.EMPTY; - } - final Intent intent = activity.getIntent(); - if (intent == null) { - return Bundle.EMPTY; + resumeSwitchBrowser(); + } else { + setSwitchBrowserBundle(null); } - final Bundle extras = intent.getExtras(); - return extras == null ? Bundle.EMPTY : extras; } /** * Resume the switch browser protocol flow. - * - * @param extras Bundle with the data to resume the switch browser protocol flow. */ - private void resumeSwitchBrowser(@NonNull final Bundle extras) { + private void resumeSwitchBrowser() { final String methodTag = TAG + ":resumeSwitchBrowser"; try { + if (switchBrowserBundle == null) { + throw new ClientException( + ClientException.NULL_OBJECT, + "No switch browser bundle found to resume the flow." + ); + } Logger.info(methodTag, "Resuming switch browser flow"); getSwitchBrowserCoordinator().processSwitchBrowserResume( mAuthorizationRequestUrl, - extras, + switchBrowserBundle, (switchBrowserResumeUri, switchBrowserResumeHeaders) -> { launchWebView(switchBrowserResumeUri.toString(), switchBrowserResumeHeaders); return null; } ); + setSwitchBrowserBundle(null); } catch (final ClientException e) { Logger.error(methodTag, "Error processing switch browser resume", e); sendResult(RawAuthorizationResult.fromException(e)); @@ -476,4 +467,12 @@ private SwitchBrowserProtocolCoordinator getSwitchBrowserCoordinator() { } return mSwitchBrowserProtocolCoordinator; } + + /** + * Set the switch browser bundle to be used when resuming the flow. + * @param bundle The bundle containing the data needed to resume the flow. + */ + public static synchronized void setSwitchBrowserBundle(@Nullable final Bundle bundle) { + switchBrowserBundle = bundle; + } } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/SwitchBrowserRequestHandler.kt b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/SwitchBrowserRequestHandler.kt index df6f78268a..401fbb13bc 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/SwitchBrowserRequestHandler.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/SwitchBrowserRequestHandler.kt @@ -23,11 +23,10 @@ package com.microsoft.identity.common.internal.ui.webview.challengehandlers import android.app.Activity -import android.content.Context import android.content.Intent import com.microsoft.identity.common.adal.internal.AuthenticationConstants.SWITCH_BROWSER +import com.microsoft.identity.common.internal.providers.oauth2.SwitchBrowserActivity import com.microsoft.identity.common.internal.ui.browser.AndroidBrowserSelector -import com.microsoft.identity.common.internal.ui.browser.CustomTabsManager import com.microsoft.identity.common.internal.ui.webview.switchbrowser.SwitchBrowserUriHelper import com.microsoft.identity.common.internal.ui.webview.switchbrowser.SwitchBrowserUriHelper.isSwitchBrowserRedirectUrl import com.microsoft.identity.common.java.browser.IBrowserSelector @@ -48,8 +47,6 @@ import io.opentelemetry.api.trace.StatusCode */ class SwitchBrowserRequestHandler( private val activity: Activity, - private val context: Context, - private val customTabsManager: CustomTabsManager, private val browserSelector: IBrowserSelector, private val spanContext: SpanContext? ) : IChallengeHandler { @@ -58,8 +55,7 @@ class SwitchBrowserRequestHandler( OTelUtility.createSpanFromParent(SpanName.SwitchBrowserProcess.name, spanContext) } - var isChallengeHandled: Boolean = false - private set + var isSwitchBrowserChallengeActive: Boolean = false companion object { private val TAG = SwitchBrowserRequestHandler::class.simpleName @@ -67,8 +63,6 @@ class SwitchBrowserRequestHandler( constructor(activity: Activity, spanContext: SpanContext?) : this( activity, - activity.applicationContext, - CustomTabsManager(activity.applicationContext), AndroidBrowserSelector(activity.applicationContext), spanContext ) @@ -108,34 +102,6 @@ class SwitchBrowserRequestHandler( span.end() throw exception } - - // Create an intent to launch the browser - val browserIntent: Intent - if (browser.isCustomTabsServiceSupported) { - Logger.info(methodTag, "CustomTabsService is supported.") - //create customTabsIntent - if (!customTabsManager.bind(context, browser.packageName)) { - Logger.warn(methodTag, "Failed to bind CustomTabsService.") - browserIntent = Intent(Intent.ACTION_VIEW) - } else { - browserIntent = customTabsManager.customTabsIntent.intent - } - } else { - Logger.warn(methodTag, "CustomTabsService is NOT supported") - browserIntent = Intent(Intent.ACTION_VIEW) - } - Logger.info( - methodTag, - "Launching switch browser request on browser: ${browser.packageName}" - ) - browserIntent.setPackage(browser.packageName) - browserIntent.setData(switchBrowserChallenge.processUri) - activity.startActivity(browserIntent) - isChallengeHandled = true - span.setAttribute( - AttributeName.is_switch_browser_request_handled.name, - isChallengeHandled - ) span.setAttribute( AttributeName.browser_package_name.name, browser.packageName @@ -144,7 +110,15 @@ class SwitchBrowserRequestHandler( AttributeName.is_custom_tabs_supported.name, browser.isCustomTabsServiceSupported ) + val switchBrowserIntent = Intent(activity, SwitchBrowserActivity::class.java).apply { + putExtra(SwitchBrowserActivity.BROWSER_PACKAGE_NAME, browser.packageName) + putExtra(SwitchBrowserActivity.BROWSER_SUPPORTS_CUSTOM_TABS, browser.isCustomTabsServiceSupported) + putExtra(SwitchBrowserActivity.PROCESS_URI, switchBrowserChallenge.processUri.toString()) + setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + activity.startActivity(switchBrowserIntent) span.setStatus(StatusCode.OK) + isSwitchBrowserChallengeActive = true span.end() } } @@ -165,15 +139,10 @@ class SwitchBrowserRequestHandler( } /** - * Handles the necessary cleanup after a challenge is completed. - * This includes unbinding the custom tabs manager and resetting the challenge handled flag. + * Reset the challenge state. + * This method is called after processing the switch browser resume action. */ fun resetChallengeState() { - // Unbind the custom tabs manager - customTabsManager.unbind() - // Reset the challenge handled flag - isChallengeHandled = false - + isSwitchBrowserChallengeActive = false } - } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserProtocolCoordinator.kt b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserProtocolCoordinator.kt index 5a41ff9236..bfcfc29d01 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserProtocolCoordinator.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserProtocolCoordinator.kt @@ -27,9 +27,8 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.AUTHORIZATION_AGENT import com.microsoft.identity.common.adal.internal.AuthenticationConstants.SWITCH_BROWSER -import com.microsoft.identity.common.internal.providers.oauth2.BrokerAuthorizationActivity +import com.microsoft.identity.common.internal.providers.oauth2.SwitchBrowserActivity import com.microsoft.identity.common.internal.ui.webview.challengehandlers.SwitchBrowserRequestHandler import com.microsoft.identity.common.java.AuthenticationConstants.AAD.AUTHORIZATION import com.microsoft.identity.common.java.exception.ClientException @@ -37,11 +36,11 @@ import com.microsoft.identity.common.java.opentelemetry.AttributeName import com.microsoft.identity.common.java.opentelemetry.OTelUtility import com.microsoft.identity.common.java.opentelemetry.SpanExtension import com.microsoft.identity.common.java.opentelemetry.SpanName -import com.microsoft.identity.common.java.ui.AuthorizationAgent import com.microsoft.identity.common.logging.Logger import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.SpanContext import io.opentelemetry.api.trace.StatusCode +import androidx.core.net.toUri /** * SwitchBrowserProtocolCoordinator is responsible for coordinating the switch browser protocol. @@ -77,26 +76,25 @@ class SwitchBrowserProtocolCoordinator( * Returns the [Intent] used to start the WebView. */ fun getIntentToResumeWebViewAuth(context: Context, intentDataString: String): Intent { - val uri = Uri.parse(intentDataString) - val intent = Intent(context, BrokerAuthorizationActivity::class.java) - intent.putExtra(AUTHORIZATION_AGENT, AuthorizationAgent.WEBVIEW) - // Ensures that if the activity is already running at the top of the stack (WebView), - // a new instance is not created, but its existing instance is brought to the foreground - // instead of launching a new one. - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) - intent.putExtra( - SWITCH_BROWSER.ACTION_URI, - uri.getQueryParameter(SWITCH_BROWSER.ACTION_URI) - ) - intent.putExtra( - SWITCH_BROWSER.CODE, - uri.getQueryParameter(SWITCH_BROWSER.CODE) - ) - intent.putExtra( - SWITCH_BROWSER.STATE, - uri.getQueryParameter(SWITCH_BROWSER.STATE) - ) - return intent + val uri = intentDataString.toUri() + return Intent(context, SwitchBrowserActivity::class.java).apply { + putExtra( + SWITCH_BROWSER.ACTION_URI, + uri.getQueryParameter(SWITCH_BROWSER.ACTION_URI) + ) + putExtra( + SWITCH_BROWSER.CODE, + uri.getQueryParameter(SWITCH_BROWSER.CODE) + ) + putExtra( + SWITCH_BROWSER.STATE, + uri.getQueryParameter(SWITCH_BROWSER.STATE) + ) + putExtra( + SwitchBrowserActivity.RESUME_REQUEST, + true + ) + } } } @@ -154,7 +152,7 @@ class SwitchBrowserProtocolCoordinator( */ fun isExpectingSwitchBrowserResume(): Boolean { val methodTag = "$TAG:isExpectingSwitchBrowserResume" - Logger.verbose(methodTag, "ExpectingRequest: ${switchBrowserRequestHandler.isChallengeHandled}") - return switchBrowserRequestHandler.isChallengeHandled + Logger.verbose(methodTag, "ExpectingRequest: ${switchBrowserRequestHandler.isSwitchBrowserChallengeActive}") + return switchBrowserRequestHandler.isSwitchBrowserChallengeActive } } diff --git a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/SwitchBrowserRequestHandlerTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/SwitchBrowserRequestHandlerTest.kt index 1a17bcb844..65da22c900 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/SwitchBrowserRequestHandlerTest.kt +++ b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/SwitchBrowserRequestHandlerTest.kt @@ -23,10 +23,8 @@ package com.microsoft.identity.common.internal.ui.webview.challengehandlers import android.app.Activity -import android.content.Context import android.content.Intent import android.net.Uri -import com.microsoft.identity.common.internal.ui.browser.CustomTabsManager import com.microsoft.identity.common.internal.ui.webview.switchbrowser.SwitchBrowserUriHelper import com.microsoft.identity.common.java.browser.Browser import com.microsoft.identity.common.java.browser.IBrowserSelector @@ -58,14 +56,12 @@ class SwitchBrowserRequestHandlerTest { activityExecuted = true null }.whenever(mockActivity).startActivity(any()) - val context = mock(Context::class.java) - val customTabsManager = mock(CustomTabsManager::class.java) val challenge = mock(SwitchBrowserChallenge::class.java) `when`(challenge.processUri).thenReturn(Uri.parse("https://login.microsoft.com?state=123")) `when`(challenge.authorizationUrl).thenReturn("https://auth.com?state=123") val browserSelector = // Browser available IBrowserSelector { _, _ -> Browser("fakeBrowser", emptySet(), "browser", false) } - val handler = SwitchBrowserRequestHandler(mockActivity, context, customTabsManager, browserSelector, null) + val handler = SwitchBrowserRequestHandler(mockActivity, browserSelector, null) handler.processChallenge(challenge) Assert.assertTrue(activityExecuted) } @@ -80,14 +76,12 @@ class SwitchBrowserRequestHandlerTest { activityExecuted = true null }.whenever(mockActivity).startActivity(any()) - val context = mock(Context::class.java) - val customTabsManager = mock(CustomTabsManager::class.java) val challenge = mock(SwitchBrowserChallenge::class.java) `when`(challenge.processUri).thenReturn(Uri.parse("https://login.microsoft.com")) `when`(challenge.authorizationUrl).thenReturn("https://auth.com") val browserSelector = // Browser available IBrowserSelector { _, _ -> Browser("fakeBrowser", emptySet(), "browser", false) } - val handler = SwitchBrowserRequestHandler(mockActivity, context, customTabsManager, browserSelector, null) + val handler = SwitchBrowserRequestHandler(mockActivity, browserSelector, null) handler.processChallenge(challenge) Assert.assertTrue(activityExecuted) } @@ -97,13 +91,11 @@ class SwitchBrowserRequestHandlerTest { // Mock parameters val activity = mock(Activity::class.java) doNothing().`when`(activity).startActivity(Intent()) - val context = mock(Context::class.java) - val customTabsManager = mock(CustomTabsManager::class.java) val challenge = mock(SwitchBrowserChallenge::class.java) `when`(challenge.processUri).thenReturn(Uri.parse("https://login.microsoft.com?state=123")) `when`(challenge.authorizationUrl).thenReturn("https://auth.com?state=123") val browserSelector = IBrowserSelector { _, _ -> null } // No browser available - val handler = SwitchBrowserRequestHandler(activity, context, customTabsManager, browserSelector, null) + val handler = SwitchBrowserRequestHandler(activity, browserSelector, null) val exception = Assert.assertThrows(ClientException::class.java) { handler.processChallenge(challenge) } @@ -116,14 +108,12 @@ class SwitchBrowserRequestHandlerTest { isStateRequired(true) // Mock parameters val mockActivity = mock() - val context = mock(Context::class.java) - val customTabsManager = mock(CustomTabsManager::class.java) val challenge = mock(SwitchBrowserChallenge::class.java) `when`(challenge.processUri).thenReturn(Uri.parse("https://login.microsoft.com?state=123")) `when`(challenge.authorizationUrl).thenReturn("https://auth.com?state=456") val browserSelector = // Browser available IBrowserSelector { _, _ -> Browser("fakeBrowser", emptySet(), "browser", false) } - val handler = SwitchBrowserRequestHandler(mockActivity, context, customTabsManager, browserSelector, null) + val handler = SwitchBrowserRequestHandler(mockActivity, browserSelector, null) val exception = Assert.assertThrows(ClientException::class.java) { handler.processChallenge(challenge) } diff --git a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserProtocolCoordinatorTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserProtocolCoordinatorTest.kt index d0bee03e11..c319d48bed 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserProtocolCoordinatorTest.kt +++ b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserProtocolCoordinatorTest.kt @@ -152,7 +152,7 @@ class SwitchBrowserProtocolCoordinatorTest { fun `test isExpectingSwitchBrowserResume with handler true`() { // Mock parameters val mockSwitchBrowserRequestHandler = mock(SwitchBrowserRequestHandler::class.java) - `when`(mockSwitchBrowserRequestHandler.isChallengeHandled).then { true } + `when`(mockSwitchBrowserRequestHandler.isSwitchBrowserChallengeActive).then { true } // Create an instance of SwitchBrowserProtocolCoordinator val coordinator = SwitchBrowserProtocolCoordinator(mockSwitchBrowserRequestHandler) @@ -167,7 +167,7 @@ class SwitchBrowserProtocolCoordinatorTest { fun `test isExpectingSwitchBrowserResume with handler false`() { // Mock parameters val mockSwitchBrowserRequestHandler = mock(SwitchBrowserRequestHandler::class.java) - `when`(mockSwitchBrowserRequestHandler.isChallengeHandled).then { false } + `when`(mockSwitchBrowserRequestHandler.isSwitchBrowserChallengeActive).then { false } // Create an instance of SwitchBrowserProtocolCoordinator val coordinator = SwitchBrowserProtocolCoordinator(mockSwitchBrowserRequestHandler) @@ -210,8 +210,12 @@ class SwitchBrowserProtocolCoordinatorTest { val mockContext = mock(Context::class.java) val actionUri = "mock-action-uri" val code = "mock-code" - val intentDataString = "${Broker.NEW_BROKER_REDIRECT_URI}/${SWITCH_BROWSER.RESUME_PATH}?" + - "${SWITCH_BROWSER.ACTION_URI}=$actionUri&${SWITCH_BROWSER.CODE}=$code" + val state = "mock-state" + val intentDataString = + "${Broker.NEW_BROKER_REDIRECT_URI}/${SWITCH_BROWSER.RESUME_PATH}?" + + "${SWITCH_BROWSER.ACTION_URI}=$actionUri&" + + "${SWITCH_BROWSER.CODE}=$code&" + + "${SWITCH_BROWSER.STATE}=$state" // Call the method to be tested val intent = SwitchBrowserProtocolCoordinator @@ -219,15 +223,12 @@ class SwitchBrowserProtocolCoordinatorTest { // Verify the result Assert.assertEquals( - Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP, + 0, intent.flags ) Assert.assertEquals(actionUri, intent.getStringExtra(SWITCH_BROWSER.ACTION_URI)) Assert.assertEquals(code, intent.getStringExtra(SWITCH_BROWSER.CODE)) - Assert.assertEquals( - AuthorizationAgent.WEBVIEW, - intent.getSerializableExtra(AUTHORIZATION_AGENT) as AuthorizationAgent - ) + Assert.assertEquals(state, intent.getStringExtra(SWITCH_BROWSER.STATE)) } @Test