From 8807238db3e07db51f8c5633607252dce04316bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:20:37 +0000 Subject: [PATCH 1/3] Initial plan From ca023ca795ca48c70b26f0fb035ed405d365def5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:34:44 +0000 Subject: [PATCH 2/3] Add AuthTab support: feature flag, fragment, factory routing, SwitchBrowserActivity, CustomTabsManager helper, AttributeNames, SpanName, and tests Co-authored-by: shahzaibj <37125644+shahzaibj@users.noreply.github.com> --- .../oauth2/AuthTabAuthorizationFragment.kt | 180 ++++++++++++++++++ .../oauth2/AuthorizationActivityFactory.kt | 5 + .../providers/oauth2/SwitchBrowserActivity.kt | 68 +++++++ .../ui/browser/CustomTabsManager.java | 20 ++ .../AuthorizationActivityFactoryTest.java | 67 +++++++ .../common/java/flighting/CommonFlight.java | 9 +- .../java/opentelemetry/AttributeName.java | 18 ++ .../common/java/opentelemetry/SpanName.java | 6 +- gradle/versions.gradle | 2 +- 9 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthTabAuthorizationFragment.kt diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthTabAuthorizationFragment.kt b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthTabAuthorizationFragment.kt new file mode 100644 index 0000000000..4d81c5d780 --- /dev/null +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthTabAuthorizationFragment.kt @@ -0,0 +1,180 @@ +// 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.net.Uri +import android.os.Bundle +import androidx.activity.result.ActivityResultLauncher +import androidx.browser.auth.AuthTabIntent +import com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REDIRECT_URI +import com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REQUEST_URL +import com.microsoft.identity.common.java.exception.ClientException +import com.microsoft.identity.common.java.exception.ErrorStrings +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.providers.RawAuthorizationResult +import com.microsoft.identity.common.logging.Logger +import io.opentelemetry.api.trace.StatusCode + +/** + * Authorization fragment that uses AuthTab (Chrome 137+) for browser-based authentication flows. + * + * AuthTab returns results via [ActivityResultLauncher] instead of intent-based redirects, + * improving security and simplifying the flow. Falls back to [BrowserAuthorizationFragment] + * behavior is not handled here; the routing decision happens in [AuthorizationActivityFactory]. + * + * The [ActivityResultLauncher] must be registered in [onCreate] before the STARTED lifecycle state. + */ +class AuthTabAuthorizationFragment : AuthorizationFragment() { + + companion object { + private val TAG: String = AuthTabAuthorizationFragment::class.java.simpleName + private const val AUTH_FLOW_STARTED = "authFlowStarted" + } + + private var requestUrl: String? = null + private var redirectUri: String? = null + private var authFlowStarted = false + + private lateinit var authTabLauncher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + // Register the ActivityResultLauncher BEFORE calling super.onCreate(), + // which transitions to STARTED state. This is required by the Activity Result API. + authTabLauncher = AuthTabIntent.registerActivityResultLauncher(this, ::handleAuthResult) + super.onCreate(savedInstanceState) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(REQUEST_URL, requestUrl) + outState.putString(REDIRECT_URI, redirectUri) + outState.putBoolean(AUTH_FLOW_STARTED, authFlowStarted) + } + + override fun extractState(state: Bundle) { + super.extractState(state) + requestUrl = state.getString(REQUEST_URL) + redirectUri = state.getString(REDIRECT_URI) + authFlowStarted = state.getBoolean(AUTH_FLOW_STARTED, false) + } + + override fun onResume() { + super.onResume() + val methodTag = "$TAG:onResume" + + if (authFlowStarted) { + // The fragment resumed after returning from AuthTab without a result + // (e.g., user pressed back). Treat this as user cancellation. + Logger.info(methodTag, "AuthTab flow already started and resumed without result - treating as cancellation.") + cancelAuthorization(true) + return + } + + val url = requestUrl + val redirect = redirectUri + if (url.isNullOrBlank() || redirect.isNullOrBlank()) { + Logger.error(methodTag, "Missing requestUrl or redirectUri - Cannot launch AuthTab.", null) + sendResult(RawAuthorizationResult.fromException( + ClientException(ErrorStrings.UNKNOWN_ERROR, "Missing requestUrl or redirectUri for AuthTab flow.") + )) + finish() + return + } + + val scheme = Uri.parse(redirect).scheme + if (scheme.isNullOrBlank()) { + Logger.error(methodTag, "Could not extract redirect scheme from redirectUri: $redirect", null) + sendResult(RawAuthorizationResult.fromException( + ClientException(ErrorStrings.UNKNOWN_ERROR, "Could not extract redirect scheme from redirectUri.") + )) + finish() + return + } + + authFlowStarted = true + Logger.info(methodTag, "Launching AuthTab for URL, redirect scheme: $scheme") + val authTabIntent = AuthTabIntent.Builder().build() + authTabIntent.launch(authTabLauncher, Uri.parse(url), scheme) + } + + private fun handleAuthResult(result: AuthTabIntent.AuthResult) { + val methodTag = "$TAG:handleAuthResult" + val span = OTelUtility.createSpan(SpanName.AuthTabAuthorization.name) + SpanExtension.makeCurrentSpan(span).use { + try { + span.setAttribute(AttributeName.is_auth_tab_used.name, true) + span.setAttribute(AttributeName.auth_tab_result_code.name, result.resultCode) + + when (result.resultCode) { + AuthTabIntent.RESULT_OK -> { + val resultUri = result.resultUri + Logger.info(methodTag, "AuthTab returned RESULT_OK.") + span.setStatus(StatusCode.OK) + if (resultUri != null) { + sendResult(RawAuthorizationResult.fromRedirectUri(resultUri.toString())) + } else { + Logger.warn(methodTag, "AuthTab RESULT_OK but resultUri is null - treating as cancellation.") + sendResult(RawAuthorizationResult.ResultCode.CANCELLED) + } + finish() + } + AuthTabIntent.RESULT_CANCELED -> { + Logger.info(methodTag, "AuthTab returned RESULT_CANCELED - user cancelled authorization.") + span.setStatus(StatusCode.OK) + cancelAuthorization(true) + } + AuthTabIntent.RESULT_VERIFICATION_FAILED -> { + Logger.error(methodTag, "AuthTab returned RESULT_VERIFICATION_FAILED.", null) + span.setStatus(StatusCode.ERROR) + sendResult(RawAuthorizationResult.fromException( + ClientException(ErrorStrings.UNKNOWN_ERROR, "AuthTab verification failed.") + )) + finish() + } + AuthTabIntent.RESULT_VERIFICATION_TIMED_OUT -> { + Logger.error(methodTag, "AuthTab returned RESULT_VERIFICATION_TIMED_OUT.", null) + span.setStatus(StatusCode.ERROR) + sendResult(RawAuthorizationResult.fromException( + ClientException(ErrorStrings.UNKNOWN_ERROR, "AuthTab verification timed out.") + )) + finish() + } + else -> { + Logger.warn(methodTag, "AuthTab returned unknown result code: ${result.resultCode}") + span.setStatus(StatusCode.OK) + cancelAuthorization(true) + } + } + } catch (t: Throwable) { + span.setStatus(StatusCode.ERROR) + span.recordException(t) + throw t + } finally { + span.end() + } + } + } +} 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 58b64c5fba..01ba915487 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 @@ -39,6 +39,8 @@ import com.microsoft.identity.common.java.AuthenticationConstants.SdkPlatformFie import com.microsoft.identity.common.java.AuthenticationConstants.SdkPlatformFields.VERSION import com.microsoft.identity.common.java.configuration.LibraryConfiguration import com.microsoft.identity.common.java.exception.ClientException +import com.microsoft.identity.common.java.flighting.CommonFlight +import com.microsoft.identity.common.java.flighting.CommonFlightsManager import com.microsoft.identity.common.java.logging.DiagnosticContext import com.microsoft.identity.common.java.opentelemetry.OtelContextExtension import com.microsoft.identity.common.java.opentelemetry.SerializableSpanContext @@ -172,6 +174,9 @@ object AuthorizationActivityFactory { } else { if (libraryConfig.isAuthorizationInCurrentTask) { CurrentTaskBrowserAuthorizationFragment() + } else if (CommonFlightsManager.INSTANCE.getFlightsProvider() + .isFlightEnabled(CommonFlight.ENABLE_AUTH_TAB)) { + AuthTabAuthorizationFragment() } else { BrowserAuthorizationFragment() } 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 index b5b8db6adb..b3c665bab1 100644 --- 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 @@ -24,10 +24,14 @@ package com.microsoft.identity.common.internal.providers.oauth2 import android.content.Intent import android.os.Bundle +import androidx.activity.result.ActivityResultLauncher +import androidx.browser.auth.AuthTabIntent 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 +import com.microsoft.identity.common.java.flighting.CommonFlight +import com.microsoft.identity.common.java.flighting.CommonFlightsManager /** @@ -65,6 +69,9 @@ class SwitchBrowserActivity : FragmentActivity() { private var cctLaunched = false private var customTabsManager = CustomTabsManager(this) + // AuthTab launcher for DUNA flows when AuthTab is supported + private lateinit var authTabLauncher: ActivityResultLauncher + companion object { private val TAG: String = SwitchBrowserActivity::class.java.simpleName @@ -91,6 +98,11 @@ class SwitchBrowserActivity : FragmentActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { val methodTag = "$TAG:onCreate" + // Register the AuthTab launcher before super.onCreate() to ensure it is registered + // before the activity reaches the STARTED state, as required by the Activity Result API. + authTabLauncher = AuthTabIntent.registerActivityResultLauncher(this) { result -> + handleAuthTabResult(result) + } super.onCreate(savedInstanceState) Logger.info(methodTag, "SwitchBrowserActivity created - Launching browser") launchBrowser() @@ -134,6 +146,20 @@ class SwitchBrowserActivity : FragmentActivity() { // Create an intent to launch the browser val browserIntent: Intent + if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_AUTH_TAB) + && CustomTabsManager.isAuthTabSupported(this, browserPackageName)) { + Logger.info(methodTag, "AuthTab is supported - launching via AuthTabIntent.") + val authTabIntent = AuthTabIntent.Builder().build() + val scheme = processUri.toUri().scheme + if (scheme.isNullOrBlank()) { + Logger.error(methodTag, "Could not extract scheme from processUri: $processUri - Cannot launch AuthTab", null) + finish() + return + } + authTabIntent.launch(authTabLauncher, processUri.toUri(), scheme) + return + } + if (browserSupportsCustomTabs) { Logger.info(methodTag, "CustomTabsService is supported.") //create customTabsIntent @@ -152,6 +178,48 @@ class SwitchBrowserActivity : FragmentActivity() { startActivity(browserIntent) } + /** + * Handles the result from AuthTab for DUNA authentication flows. + * + * @param result The [AuthTabIntent.AuthResult] returned by AuthTab. + */ + private fun handleAuthTabResult(result: AuthTabIntent.AuthResult) { + val methodTag = "$TAG:handleAuthTabResult" + Logger.info(methodTag, "AuthTab result received, resultCode: ${result.resultCode}") + when (result.resultCode) { + AuthTabIntent.RESULT_OK -> { + val resultUri = result.resultUri + if (resultUri != null) { + val bundle = Bundle().apply { + putString(com.microsoft.identity.common.adal.internal.AuthenticationConstants.SWITCH_BROWSER.ACTION_URI, + resultUri.getQueryParameter(com.microsoft.identity.common.adal.internal.AuthenticationConstants.SWITCH_BROWSER.ACTION_URI)) + putString(com.microsoft.identity.common.adal.internal.AuthenticationConstants.SWITCH_BROWSER.CODE, + resultUri.getQueryParameter(com.microsoft.identity.common.adal.internal.AuthenticationConstants.SWITCH_BROWSER.CODE)) + putString(com.microsoft.identity.common.adal.internal.AuthenticationConstants.SWITCH_BROWSER.STATE, + resultUri.getQueryParameter(com.microsoft.identity.common.adal.internal.AuthenticationConstants.SWITCH_BROWSER.STATE)) + putBoolean(RESUME_REQUEST, true) + } + WebViewAuthorizationFragment.setSwitchBrowserBundle(bundle) + } else { + Logger.warn(methodTag, "AuthTab RESULT_OK but resultUri is null.") + } + } + AuthTabIntent.RESULT_CANCELED -> { + Logger.info(methodTag, "AuthTab result: RESULT_CANCELED - user cancelled.") + } + AuthTabIntent.RESULT_VERIFICATION_FAILED -> { + Logger.error(methodTag, "AuthTab result: RESULT_VERIFICATION_FAILED.", null) + } + AuthTabIntent.RESULT_VERIFICATION_TIMED_OUT -> { + Logger.error(methodTag, "AuthTab result: RESULT_VERIFICATION_TIMED_OUT.", null) + } + else -> { + Logger.warn(methodTag, "AuthTab result: unknown resultCode ${result.resultCode}.") + } + } + finishAndRemoveTask() + } + /** * Handles the redirect back from the browser after DUNA authentication completion. * diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/browser/CustomTabsManager.java b/common/src/main/java/com/microsoft/identity/common/internal/ui/browser/CustomTabsManager.java index e252dfea7b..8acd750266 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/browser/CustomTabsManager.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/browser/CustomTabsManager.java @@ -177,6 +177,26 @@ private CustomTabsClient getClient() { return mCustomTabsClient.get(); } + /** + * Checks whether the given browser package supports AuthTab. + * + * @param context The application context. + * @param browserPackage The package name of the browser to check. + * @return {@code true} if the browser supports AuthTab; {@code false} otherwise. + */ + public static boolean isAuthTabSupported(@NonNull final Context context, + @NonNull final String browserPackage) { + final String methodTag = TAG + ":isAuthTabSupported"; + try { + final boolean supported = CustomTabsClient.isAuthTabSupported(context, browserPackage); + Logger.info(methodTag, "AuthTab supported by " + browserPackage + ": " + supported); + return supported; + } catch (final Exception e) { + Logger.warn(methodTag, "Exception checking AuthTab support for " + browserPackage + ": " + e.getMessage()); + return false; + } + } + /** * Method to unbind custom tabs service {@link androidx.browser.customtabs.CustomTabsService}. */ diff --git a/common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationActivityFactoryTest.java b/common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationActivityFactoryTest.java index 25771929f1..6332717619 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationActivityFactoryTest.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationActivityFactoryTest.java @@ -45,11 +45,16 @@ import androidx.fragment.app.Fragment; +import com.microsoft.identity.common.internal.mocks.MockCommonFlightsManager; import com.microsoft.identity.common.internal.msafederation.google.SignInWithGoogleApi; import com.microsoft.identity.common.internal.msafederation.google.SignInWithGoogleCredential; import com.microsoft.identity.common.internal.msafederation.google.SignInWithGoogleParameters; +import com.microsoft.identity.common.java.flighting.CommonFlight; +import com.microsoft.identity.common.java.flighting.CommonFlightsManager; +import com.microsoft.identity.common.java.flighting.IFlightsProvider; import com.microsoft.identity.common.java.ui.AuthorizationAgent; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -93,6 +98,11 @@ public class AuthorizationActivityFactoryTest { sourceLibraryVersion ); + @After + public void tearDown() { + CommonFlightsManager.INSTANCE.resetFlightsManager(); + } + @SneakyThrows @Test public void testGetAuthorizationActivityIntent() { @@ -256,4 +266,61 @@ public void testGetAuthorizationFragmentFromStartIntentWithSilentFlowNonWebView( // Verify it creates BrowserAuthorizationFragment even with silent flow when not WebView assertEquals(BrowserAuthorizationFragment.class, fragment.getClass()); } + + @Test + public void testGetAuthorizationFragmentFromStartIntent_whenAuthTabFlagEnabled_returnsAuthTabFragment() { + // Arrange: enable ENABLE_AUTH_TAB flight + final IFlightsProvider mockFlightsProvider = mock(IFlightsProvider.class); + when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_AUTH_TAB)).thenReturn(true); + final MockCommonFlightsManager mockFlightsManager = new MockCommonFlightsManager(); + mockFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); + CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockFlightsManager); + + final Intent intent = new Intent(); + intent.putExtra(AUTHORIZATION_AGENT, AuthorizationAgent.BROWSER); + + // Act + final Fragment fragment = AuthorizationActivityFactory.getAuthorizationFragmentFromStartIntent(intent); + + // Assert: should return AuthTabAuthorizationFragment when flag is on + assertEquals(AuthTabAuthorizationFragment.class, fragment.getClass()); + } + + @Test + public void testGetAuthorizationFragmentFromStartIntent_whenAuthTabFlagDisabled_returnsBrowserFragment() { + // Arrange: ENABLE_AUTH_TAB flight is disabled (default) + final IFlightsProvider mockFlightsProvider = mock(IFlightsProvider.class); + when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_AUTH_TAB)).thenReturn(false); + final MockCommonFlightsManager mockFlightsManager = new MockCommonFlightsManager(); + mockFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); + CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockFlightsManager); + + final Intent intent = new Intent(); + intent.putExtra(AUTHORIZATION_AGENT, AuthorizationAgent.BROWSER); + + // Act + final Fragment fragment = AuthorizationActivityFactory.getAuthorizationFragmentFromStartIntent(intent); + + // Assert: should still use BrowserAuthorizationFragment when flag is off + assertEquals(BrowserAuthorizationFragment.class, fragment.getClass()); + } + + @Test + public void testGetAuthorizationFragmentFromStartIntent_whenAuthTabFlagEnabled_butWebView_returnsWebViewFragment() { + // Arrange: enable ENABLE_AUTH_TAB flight but use WebView agent + final IFlightsProvider mockFlightsProvider = mock(IFlightsProvider.class); + when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_AUTH_TAB)).thenReturn(true); + final MockCommonFlightsManager mockFlightsManager = new MockCommonFlightsManager(); + mockFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); + CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockFlightsManager); + + final Intent intent = new Intent(); + intent.putExtra(AUTHORIZATION_AGENT, AuthorizationAgent.WEBVIEW); + + // Act + final Fragment fragment = AuthorizationActivityFactory.getAuthorizationFragmentFromStartIntent(intent); + + // Assert: WebView agent should always use WebViewAuthorizationFragment regardless of flag + assertEquals(WebViewAuthorizationFragment.class, fragment.getClass()); + } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java b/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java index 37deeff5ff..5715e71323 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java +++ b/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java @@ -211,7 +211,14 @@ public enum CommonFlight implements IFlightConfig { * Flight to enable increased thread pool size for silent requests. * When true, uses 12 threads. When false, uses legacy 5 threads. */ - USE_INCREASED_DEFAULT_SILENT_REQUEST_THREAD_POOL_SIZE("UseIncreasedSilentRequestThreadPoolSize", false); + USE_INCREASED_DEFAULT_SILENT_REQUEST_THREAD_POOL_SIZE("UseIncreasedSilentRequestThreadPoolSize", false), + + /** + * Flight to enable Chrome AuthTab (Chrome 137+) for browser-based auth flows. + * When enabled, AuthTabIntent is used instead of Custom Tabs for interactive authorization, + * improving security by returning results via ActivityResultCallback instead of intent redirects. + */ + ENABLE_AUTH_TAB("EnableAuthTab", false); private String key; private Object defaultValue; diff --git a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java index b6c2880aa2..49dc4642c2 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java +++ b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java @@ -597,4 +597,22 @@ public enum AttributeName { secret_key_serialization_duration, //endregion + + /** + * Indicates whether AuthTab was used for the authorization flow. + * Note: Any changes to this enum should also be made in the corresponding enum in Broker. + */ + is_auth_tab_used, + + /** + * Records the result code returned by the AuthTab authorization flow. + * Note: Any changes to this enum should also be made in the corresponding enum in Broker. + */ + auth_tab_result_code, + + /** + * Indicates whether the browser supports AuthTab. + * Note: Any changes to this enum should also be made in the corresponding enum in Broker. + */ + auth_tab_supported, } diff --git a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java index d3cc70edaa..8c95d66d6a 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java +++ b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java @@ -97,5 +97,9 @@ public enum SpanName { /** * Span name for secret key retrieval operations. */ - SecretKeyRetrieval + SecretKeyRetrieval, + /** + * Span name for AuthTab authorization fragment operations. + */ + AuthTabAuthorization } diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 740a606b89..121a432078 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -25,7 +25,7 @@ ext { androidxCoreVersion = "1.5.0" annotationVersion = "1.0.0" appCompatVersion = "1.1.0" - browserVersion = "1.7.0" + browserVersion = "1.9.0" constraintLayoutVersion = "1.1.3" dexmakerMockitoVersion = "2.19.0" espressoCoreVersion = "3.1.0" From 43683155410940390b7c1c068c62a514d9d9e3a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:40:43 +0000 Subject: [PATCH 3/3] Fix double-result issue in AuthTabAuthorizationFragment, improve CustomTabsManager Javadoc, add changelog entry Co-authored-by: shahzaibj <37125644+shahzaibj@users.noreply.github.com> --- changelog.txt | 1 + .../oauth2/AuthTabAuthorizationFragment.kt | 11 +++++++---- .../internal/ui/browser/CustomTabsManager.java | 16 ++++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index f8c41d0e3e..3cc008b14a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -3,6 +3,7 @@ vNext - [MINOR] Remove LruCache from SharedPreferencesFileManager (#2910) - [MINOR] Edge TB: Claims (#2925) +- [MINOR] Add AuthTab (Chrome 137+) support for browser-based authentication flows via ENABLE_AUTH_TAB feature flag (#3533538) Version 24.0.0 ---------- diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthTabAuthorizationFragment.kt b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthTabAuthorizationFragment.kt index 4d81c5d780..6225fe7b31 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthTabAuthorizationFragment.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthTabAuthorizationFragment.kt @@ -86,10 +86,13 @@ class AuthTabAuthorizationFragment : AuthorizationFragment() { val methodTag = "$TAG:onResume" if (authFlowStarted) { - // The fragment resumed after returning from AuthTab without a result - // (e.g., user pressed back). Treat this as user cancellation. - Logger.info(methodTag, "AuthTab flow already started and resumed without result - treating as cancellation.") - cancelAuthorization(true) + // The fragment resumed after returning from AuthTab. + // If a result was already sent (via handleAuthResult), do nothing further. + // If no result was sent yet, it means the user backed out without completing auth. + if (!mAuthResultSent) { + Logger.info(methodTag, "AuthTab flow already started and resumed without result - treating as cancellation.") + cancelAuthorization(true) + } return } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/browser/CustomTabsManager.java b/common/src/main/java/com/microsoft/identity/common/internal/ui/browser/CustomTabsManager.java index 8acd750266..ffaf35b6b1 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/browser/CustomTabsManager.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/browser/CustomTabsManager.java @@ -178,11 +178,19 @@ private CustomTabsClient getClient() { } /** - * Checks whether the given browser package supports AuthTab. + * Checks whether the given browser package supports AuthTab (Chrome 137+). * - * @param context The application context. - * @param browserPackage The package name of the browser to check. - * @return {@code true} if the browser supports AuthTab; {@code false} otherwise. + *

AuthTab is a specialized Custom Tab variant for authentication that returns results + * via {@link androidx.activity.result.ActivityResultLauncher} instead of intent-based redirects. + * It requires Chrome 137 or higher (or an equivalent browser that supports the AuthTab protocol). + * + *

This method is safe to call from any thread. It does not perform any blocking I/O. + * + * @param context The application context; must not be null. + * @param browserPackage The package name of the browser to query; must not be null. + * @return {@code true} if the given browser package supports AuthTab; {@code false} otherwise, + * including if the browser is not installed, the package name is invalid, or any + * exception occurs during the check. */ public static boolean isAuthTabSupported(@NonNull final Context context, @NonNull final String browserPackage) {