Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/SalesforceSDK/res/values/sf__strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
<string name="sf__login_options_webserver_toggle_content_description">Toggle Web Server</string>
<string name="sf__login_options_hybrid_toggle_content_description">Toggle Hybrid Token</string>
<string name="sf__login_options_force_advanced_auth_toggle_content_description">Toggle Force Advanced Authentication</string>
<string name="sf__login_options_ephemeral_session_toggle_content_description">Toggle Ephemeral Advanced Authentication Session</string>
<string name="sf__login_options_dpop_toggle_content_description">Toggle DPoP</string>
<string name="sf__login_options_dynamic_config_toggle_content_description">Toggle Dynamic Config</string>
<string name="sf__login_options_consumer_key_field_content_description">Consumer Key Field</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,15 @@ open class SalesforceSDKManager protected constructor(
@set:Synchronized
var customTabBrowser: String? = "com.android.chrome"

/**
* Whether advanced authentication requests an ephemeral Custom Tab. A browser that does not
* support ephemeral browsing may use a regular Custom Tab instead. Defaults to true.
*/
@get:JvmName("shouldUseEphemeralSessionForAdvancedAuth")
@set:Synchronized
@Volatile
var useEphemeralSessionForAdvancedAuth = true

// Backing field for [useWebServerAuthentication]. The SDK reads this directly so its own
// internal use of the flag doesn't trigger the deprecation warning on the public property.
@Volatile
Expand Down Expand Up @@ -1980,6 +1989,7 @@ open class SalesforceSDKManager protected constructor(
"Use Web Server Authentication" to "$_useWebServerAuthentication",
"Use Hybrid Authentication Token" to "$useHybridAuthentication",
"Force Advanced Authentication" to "$_forceAdvancedAuthentication",
"Use Ephemeral Session for Advanced Authentication" to "$useEphemeralSessionForAdvancedAuth",
"My Domain Browser Login Enabled" to "$isBrowserLoginEnabled",
"IDP Enabled" to "$isIDPLoginFlowEnabled",
"Identity Provider" to "$isIdentityProvider",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import android.app.AlertDialog
import android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE
import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_FACE
import android.content.pm.PackageManager.FEATURE_IRIS
import android.content.pm.PackageManager.MATCH_DEFAULT_ONLY
Expand Down Expand Up @@ -97,6 +96,7 @@ import androidx.biometric.BiometricPrompt.AuthenticationCallback
import androidx.biometric.BiometricPrompt.AuthenticationResult
import androidx.biometric.BiometricPrompt.PromptInfo
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.OPEN_IN_BROWSER_STATE_OFF
import androidx.browser.customtabs.ExperimentalInitialNavigationCanLeaveBrowser
Expand Down Expand Up @@ -1139,10 +1139,18 @@ open class LoginActivity : FragmentActivity() {

@OptIn(ExperimentalInitialNavigationCanLeaveBrowser::class)
@VisibleForTesting
internal fun loadLoginPageInCustomTab(loginUrl: String, customTabLauncher: ActivityResultLauncher<Intent>) {
internal fun loadLoginPageInCustomTab(
loginUrl: String,
customTabLauncher: ActivityResultLauncher<Intent>,
callbackSchemeRegistered: (String) -> Boolean = ::isCallbackSchemeRegistered,
) {
completedViaBrowserTab = true
registerAuthTypeFeatureGlobal()
SalesforceSDKManager.getInstance().registerUsedAppFeature(FEATURE_BROWSER_LOGIN)
val sdkManager = SalesforceSDKManager.getInstance()
sdkManager.registerUsedAppFeature(FEATURE_BROWSER_LOGIN)

val customTabBrowser = sdkManager.customTabBrowser
val customTabBrowserExists = doesBrowserExist(customTabBrowser)
val customTabsIntent = CustomTabsIntent.Builder().apply {
/*
* Set a custom animation to slide in and out for Chrome custom tab
Expand All @@ -1169,6 +1177,10 @@ open class LoginActivity : FragmentActivity() {
setOpenInBrowserButtonState(OPEN_IN_BROWSER_STATE_OFF)
setInstantAppsEnabled(false)
setBackgroundInteractionEnabled(false)
configureEphemeralBrowsing(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integration wiring is untested. The three configureEphemeralBrowsing_* tests exercise the extension in isolation (mocked LoginActivity + real CustomTabsIntent.Builder via callOriginal()). Nothing verifies that loadLoginPageInCustomTab actually calls it here with enabled = sdkManager.useEphemeralSessionForAdvancedAuth and the resolved provider. So reading the SDK flag and the takeIf provider resolution have no coverage — the test-plan’s "Custom Tab enabled/disabled" rows are satisfied only at the helper level, not at the launch path. Suggest one test asserting the wiring.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an ActivityScenario integration test that invokes the real loadLoginPageInCustomTab method twice, captures the launched AndroidX intents, and verifies the current manager setting plus configured browser package. The test bypasses only callback registration through a narrow injected check, so no test AndroidManifest change is required.

enabled = sdkManager.useEphemeralSessionForAdvancedAuth,
requestedBrowser = customTabBrowser.takeIf { customTabBrowserExists },
)
}.build()

/*
Expand All @@ -1178,14 +1190,13 @@ open class LoginActivity : FragmentActivity() {
* - If getCustomTabBrowser() returns null
* - Or if the specified browser is not installed
*/
val customTabBrowser = SalesforceSDKManager.getInstance().customTabBrowser
if (doesBrowserExist(customTabBrowser)) {
if (customTabBrowserExists) {
customTabsIntent.intent.setPackage(customTabBrowser)
}

val urlString = buildCustomTabAuthorizeUrl(loginUrl, completedViaAdminCustomTab)

if (!isCallbackSchemeRegistered(viewModel.oAuthConfig.redirectUri)) {
if (!callbackSchemeRegistered(viewModel.oAuthConfig.redirectUri)) {
val scheme = viewModel.oAuthConfig.redirectUri.toUri().scheme
?: viewModel.oAuthConfig.redirectUri
e(TAG, "Advanced auth misconfiguration: redirect URI scheme '$scheme' has no " +
Expand Down Expand Up @@ -1213,6 +1224,25 @@ open class LoginActivity : FragmentActivity() {
}
}

@VisibleForTesting
internal fun CustomTabsIntent.Builder.configureEphemeralBrowsing(
enabled: Boolean,
requestedBrowser: String?,
isSupported: (Context, String) -> Boolean = CustomTabsClient::isEphemeralBrowsingSupported,
) {
if (!enabled) return

val provider = requestedBrowser ?: CustomTabsClient.getPackageName(this@LoginActivity, null)
if (provider != null && !isSupported(this@LoginActivity, provider)) {
w(
TAG,
"Ephemeral browsing was requested, but Custom Tabs provider '$provider' does not " +
"advertise support. The browser may use a regular session.",
)
}
setEphemeralBrowsingEnabled(true)
}

/**
* Appends `sdkInfo` and `auth_trigger` to the authorize URL for the native browser (Custom
* Tab) path only — the WebView path's real `User-Agent` header already carries this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ class LoginOptionsActivity: ComponentActivity() {
// suppress the deprecation nudge here (it fires on the public property from outside the SDK).
@Suppress("DEPRECATION")
val forceAdvancedAuth = MutableLiveData(SalesforceSDKManager.getInstance().forceAdvancedAuthentication)
val useEphemeralSessionForAdvancedAuth = MutableLiveData(
SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth,
)
val useDPoP = MutableLiveData(SalesforceSDKManager.getInstance().useDPoP)

@OptIn(ExperimentalMaterial3Api::class)
Expand Down Expand Up @@ -133,6 +136,12 @@ class LoginOptionsActivity: ComponentActivity() {
value -> SalesforceSDKManager.getInstance().forceAdvancedAuthentication = value
},
)
useEphemeralSessionForAdvancedAuth.observe(
/* owner = */ this,
Observer<Boolean> {
value -> SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = value
},
)
useDPoP.observe(
/* owner = */ this,
Observer<Boolean> {
Expand All @@ -158,6 +167,7 @@ class LoginOptionsActivity: ComponentActivity() {
useWebServer,
useHybridToken,
forceAdvancedAuth,
useEphemeralSessionForAdvancedAuth,
useDPoP,
SalesforceSDKManager.getInstance().debugOverrideAppConfig,
)
Expand Down Expand Up @@ -337,6 +347,7 @@ fun LoginOptionsScreen(
useWebServer: MutableLiveData<Boolean>,
useHybridToken: MutableLiveData<Boolean>,
forceAdvancedAuth: MutableLiveData<Boolean>,
useEphemeralSessionForAdvancedAuth: MutableLiveData<Boolean>,
useDPoP: MutableLiveData<Boolean>,
overrideConfig: OAuthConfig?,
bootConfig: BootConfig = BootConfig.getBootConfig(LocalContext.current),
Expand Down Expand Up @@ -365,6 +376,11 @@ fun LoginOptionsScreen(
stringResource(R.string.sf__login_options_force_advanced_auth_toggle_content_description),
forceAdvancedAuth,
)
OptionToggle(
"Use Ephemeral Session",
stringResource(R.string.sf__login_options_ephemeral_session_toggle_content_description),
useEphemeralSessionForAdvancedAuth,
)
OptionToggle(
"Use DPoP",
stringResource(R.string.sf__login_options_dpop_toggle_content_description),
Expand Down Expand Up @@ -587,6 +603,7 @@ fun LoginOptionsScreenPreview() {
useWebServer = MutableLiveData(true),
useHybridToken = MutableLiveData(false),
forceAdvancedAuth = MutableLiveData(true),
useEphemeralSessionForAdvancedAuth = MutableLiveData(true),
useDPoP = MutableLiveData(false),
overrideConfig = null,
bootConfig = object : BootConfig() {
Expand All @@ -595,4 +612,4 @@ fun LoginOptionsScreenPreview() {
},
sdkManager = null,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@ public void testDefaultTheme() {
SalesforceSDKTestManager.getInstance().isDarkTheme());
}

/**
* Test the default and Java API for ephemeral advanced-auth sessions.
*/
@Test
public void testUseEphemeralSessionForAdvancedAuthDefaultsToTrueAndCanBeUpdated() {
SalesforceSDKTestManager.resetInstance();
SalesforceSDKTestManager.init(getInstrumentation().getTargetContext(), MainActivity.class);
final SalesforceSDKManager sdkManager = SalesforceSDKTestManager.getInstance();

try {
Assert.assertTrue(sdkManager.shouldUseEphemeralSessionForAdvancedAuth());

sdkManager.setUseEphemeralSessionForAdvancedAuth(false);

Assert.assertFalse(sdkManager.shouldUseEphemeralSessionForAdvancedAuth());
} finally {
SalesforceSDKTestManager.resetInstance();
}
}

/**
* Test setting dark theme.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ package com.salesforce.androidsdk.ui

import android.content.Intent
import android.webkit.WebView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.app.ActivityOptionsCompat
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.lifecycle.Lifecycle.State.STARTED
Expand All @@ -38,6 +43,7 @@ import com.salesforce.androidsdk.app.Features
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.salesforce.androidsdk.config.LoginServerManager.PRODUCTION_LOGIN_URL
import com.salesforce.androidsdk.config.LoginServerManager.WELCOME_LOGIN_URL
import com.salesforce.androidsdk.config.OAuthConfig
import com.salesforce.androidsdk.security.BiometricAuthenticationManager
import com.salesforce.androidsdk.ui.LoginActivity.Companion.EXTRA_KEY_LOGIN_HINT
import com.salesforce.androidsdk.ui.LoginActivity.Companion.EXTRA_KEY_LOGIN_HOST
Expand Down Expand Up @@ -126,6 +132,91 @@ class LoginActivityScenarioTest {
}
}

@Test
@Suppress("DEPRECATION")
fun loadLoginPageInCustomTab_usesCurrentEphemeralSettingAndConfiguredBrowser() {
val sdkManager = SalesforceSDKManager.getInstance()
val originalForceAdvancedAuth = sdkManager.forceAdvancedAuthentication
val originalCustomTabBrowser = sdkManager.customTabBrowser
val originalUseEphemeralSession = sdkManager.useEphemeralSessionForAdvancedAuth
val affectedFeatures = listOf(
Features.FEATURE_BROWSER_LOGIN,
Features.FEATURE_AUTH_TYPE_WEB_SERVER_NON_HYBRID,
Features.FEATURE_AUTH_TYPE_WEB_SERVER_HYBRID,
Features.FEATURE_AUTH_TYPE_USER_AGENT_NON_HYBRID,
Features.FEATURE_AUTH_TYPE_USER_AGENT_HYBRID,
Features.FEATURE_AUTH_TYPE_NATIVE,
)
val originalFeatureState = affectedFeatures.associateWith(sdkManager::isGlobalFeatureRegistered)
val configuredBrowser = getApplicationContext<android.content.Context>().packageName
val launchedIntents = mutableListOf<Intent>()
val launcher = object : ActivityResultLauncher<Intent>() {
override fun launch(input: Intent, options: ActivityOptionsCompat?) {
launchedIntents += input
}

override fun unregister() = Unit

override val contract: ActivityResultContract<Intent, *> = StartActivityForResult()
}

try {
sdkManager.forceAdvancedAuthentication = false
sdkManager.customTabBrowser = configuredBrowser

launch<LoginActivity>(
Intent(getApplicationContext(), LoginActivity::class.java)
).use { activityScenario ->
activityScenario.onActivity { activity ->
activity.viewModel.oAuthConfig = OAuthConfig(
consumerKey = "test-consumer-key",
redirectUri = "testsfdc://success/done",
)
sdkManager.useEphemeralSessionForAdvancedAuth = true
activity.loadLoginPageInCustomTab(
"https://example.com/services/oauth2/authorize?client_id=abc",
launcher,
callbackSchemeRegistered = { true },
)

sdkManager.useEphemeralSessionForAdvancedAuth = false
activity.loadLoginPageInCustomTab(
"https://example.com/services/oauth2/authorize?client_id=abc",
launcher,
callbackSchemeRegistered = { true },
)
}
}

assertEquals(2, launchedIntents.size)
assertEquals(configuredBrowser, launchedIntents[0].`package`)
assertTrue(
launchedIntents[0].getBooleanExtra(
CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING,
false,
)
)
assertEquals(configuredBrowser, launchedIntents[1].`package`)
assertFalse(
launchedIntents[1].getBooleanExtra(
CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING,
false,
)
)
} finally {
sdkManager.forceAdvancedAuthentication = originalForceAdvancedAuth
sdkManager.customTabBrowser = originalCustomTabBrowser
sdkManager.useEphemeralSessionForAdvancedAuth = originalUseEphemeralSession
originalFeatureState.forEach { (feature, wasRegistered) ->
if (wasRegistered) {
sdkManager.registerUsedAppFeature(feature)
} else {
sdkManager.unregisterUsedAppFeature(feature)
}
}
}
}

@Test
fun viewModelFrontDoorBridgeCodeVerifier_UpdatesOn_onCreateWithQrCodeLoginIntent() {
val uri = "app://android/login/qr/?bridgeJson=%7B%22pkce_code_verifier%22%3A%22__CODE_VERIFIER__%22%2C%22frontdoor_bridge_url%22%3A%22https%3A%2F%2Fmobilesdk.my.salesforce.com%2Fsecur%2Ffrontdoor.jsp%3Fotp%3D__OTP__%26startURL%3D%252Fservices%252Foauth2%252Fauthorize%253Fresponse_type%253Dcode%2526client_id%253D__CONSUMER_KEY__%2526redirect_uri%253Dtestsfdc%25253A%25252F%25252F%25252Fmobilesdk%25252Fdetect%25252Foauth%25252Fdone%2526code_challenge%253D__CODE_CHALLENGE__%26cshc%3D__CSHC__%22%7D".toUri()
Expand Down
Loading
Loading