Skip to content

Commit cf39cab

Browse files
@W-20161958: [MSDK 13.1][Android] Cannot login GUS using Welcome endpoint (Squashed From 13.2)
1 parent 318e75c commit cf39cab

File tree

3 files changed

+324
-111
lines changed

3 files changed

+324
-111
lines changed

libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt

Lines changed: 148 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import android.accounts.AccountAuthenticatorResponse
3232
import android.accounts.AccountManager.ERROR_CODE_CANCELED
3333
import android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE
3434
import android.annotation.SuppressLint
35-
import android.app.Activity
3635
import android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD
3736
import android.content.Context
3837
import android.content.Intent
@@ -74,8 +73,9 @@ import androidx.activity.addCallback
7473
import androidx.activity.compose.setContent
7574
import androidx.activity.enableEdgeToEdge
7675
import androidx.activity.result.ActivityResult
76+
import androidx.activity.result.ActivityResultCallback
7777
import androidx.activity.result.ActivityResultLauncher
78-
import androidx.activity.result.contract.ActivityResultContracts
78+
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
7979
import androidx.activity.viewModels
8080
import androidx.annotation.VisibleForTesting
8181
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
@@ -105,7 +105,7 @@ import androidx.core.content.ContextCompat.getMainExecutor
105105
import androidx.core.net.toUri
106106
import androidx.core.view.WindowCompat
107107
import androidx.fragment.app.FragmentActivity
108-
import androidx.lifecycle.lifecycleScope
108+
import androidx.lifecycle.Observer
109109
import com.salesforce.androidsdk.R.color.sf__background
110110
import com.salesforce.androidsdk.R.color.sf__background_dark
111111
import com.salesforce.androidsdk.R.color.sf__primary_color
@@ -147,7 +147,6 @@ import com.salesforce.androidsdk.util.SalesforceSDKLogger.e
147147
import com.salesforce.androidsdk.util.SalesforceSDKLogger.w
148148
import com.salesforce.androidsdk.util.UriFragmentParser
149149
import kotlinx.coroutines.CoroutineScope
150-
import kotlinx.coroutines.Dispatchers
151150
import kotlinx.coroutines.Dispatchers.Default
152151
import kotlinx.coroutines.Dispatchers.IO
153152
import kotlinx.coroutines.launch
@@ -175,6 +174,10 @@ import java.security.cert.X509Certificate
175174
* them.
176175
*/
177176
open class LoginActivity : FragmentActivity() {
177+
178+
/** The activity result launcher used when browser-based authentication loads the OAuth authorization URL in the external browser custom tab activity */
179+
private val customTabLauncher = registerForActivityResult(StartActivityForResult(), CustomTabActivityResult())
180+
178181
// View Model
179182
@VisibleForTesting(otherwise = PROTECTED)
180183
open val viewModel: LoginViewModel
@@ -183,6 +186,7 @@ open class LoginActivity : FragmentActivity() {
183186
// Webview and Clients
184187
@VisibleForTesting(otherwise = PROTECTED)
185188
open val webViewClient = AuthWebViewClient()
189+
186190
@VisibleForTesting(otherwise = PROTECTED)
187191
open val webChromeClient = WebChromeClient()
188192
open val webView: WebView by lazy {
@@ -226,7 +230,7 @@ open class LoginActivity : FragmentActivity() {
226230
SalesforceSDKManager.getInstance().setViewNavigationVisibility(this)
227231
}
228232

229-
applyIntent(intent)
233+
applyIntent()
230234

231235
// Don't let sharedBrowserSession org setting stop a new user from logging in.
232236
if (intent.extras?.getBoolean(NEW_USER) == true) {
@@ -272,62 +276,9 @@ open class LoginActivity : FragmentActivity() {
272276
onBackPressedDispatcher.addCallback { handleBackBehavior() }
273277
}
274278

275-
val customTabLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
276-
ActivityResultContracts.StartActivityForResult()
277-
) { result: ActivityResult ->
278-
// Check if the user backed out of the custom tab.
279-
if (result.resultCode == Activity.RESULT_CANCELED) {
280-
if (viewModel.singleServerCustomTabActivity) {
281-
// Show blank page and spinner until PKCE is done.
282-
viewModel.loginUrl.value = ABOUT_BLANK
283-
} else {
284-
// Don't show server picker if we are re-authenticating with cookie.
285-
clearWebView(showServerPicker = !sharedBrowserSession)
286-
}
287-
}
288-
}
289-
290-
// Take action on selected server change.
291-
viewModel.selectedServer.observe(this) { selectedServer ->
292-
293-
// Guard against observing a selected server already provided by the intent data, such as a Salesforce Welcome Discovery mobile URL.
294-
val selectedServerUri = selectedServer.toUri()
295-
if (intent.data?.host == selectedServerUri.host) {
296-
return@observe
297-
}
298-
299-
// Use the URL to switch between default or Salesforce Welcome Discovery log in, if applicable.
300-
if (switchDefaultOrSalesforceWelcomeDiscoveryLogin(selectedServerUri)) {
301-
return@observe
302-
}
303-
304-
if (viewModel.singleServerCustomTabActivity) {
305-
// Skip fetching authorization and show custom tab immediately.
306-
viewModel.reloadWebView()
307-
viewModel.loginUrl.value?.let { url ->
308-
loadLoginPageInCustomTab(url, customTabLauncher)
309-
}
310-
} else {
311-
with(SalesforceSDKManager.getInstance()) {
312-
// Fetch well known config and load in custom tab if required.
313-
fetchAuthenticationConfiguration {
314-
/* Browser-based authentication is applicable when not authenticating with a front-door bridge URL */
315-
if (isBrowserLoginEnabled && !viewModel.isUsingFrontDoorBridge) {
316-
if (useWebServerAuthentication) {
317-
viewModel.loginUrl.value?.let { url -> loadLoginPageInCustomTab(url, customTabLauncher) }
318-
} else {
319-
/* Reload the webview now that isBrowserLoginEnabled has been set
320-
to true so that we generate an authorization URL with PKCE values. */
321-
lifecycleScope.launch(Dispatchers.Main) {
322-
viewModel.reloadWebView()
323-
viewModel.loginUrl.value?.let { url -> loadLoginPageInCustomTab(url, customTabLauncher) }
324-
}
325-
}
326-
}
327-
}
328-
}
329-
}
330-
}
279+
// Add view model observers.
280+
viewModel.browserCustomTabUrl.observe(this, BrowserCustomTabUrlObserver())
281+
viewModel.pendingServer.observe(this, PendingServerObserver())
331282

332283
// Support magic links
333284
if (viewModel.jwt != null) {
@@ -359,7 +310,10 @@ open class LoginActivity : FragmentActivity() {
359310
return
360311
}
361312

362-
applyIntent(intent)
313+
// Store the new intent and apply it to the activity.
314+
setIntent(intent)
315+
applyIntent()
316+
viewModel.applyPendingServer(pendingLoginServer = viewModel.pendingServer.value)
363317
}
364318

365319
private fun clearWebView(showServerPicker: Boolean = true) {
@@ -984,52 +938,38 @@ open class LoginActivity : FragmentActivity() {
984938
)
985939
.build()
986940

987-
/**
988-
* Determines if the provided proposed selected server URL is a switch from
989-
* Salesforce Welcome Discovery back to default log in.
990-
* @param proposedSelectedServerUrl The proposed selected server URL
991-
* @return Boolean true if the provided proposed selected server URL is a
992-
* switch from Salesforce Welcome Discovery back to the default log in,
993-
* false otherwise.
994-
* */
995-
private fun isSwitchFromSalesforceWelcomeDiscoveryToDefaultLogin(
996-
proposedSelectedServerUrl: Uri
997-
) = viewModel.loginUrl.value?.toUri()?.let { loginUrl ->
998-
isSalesforceWelcomeDiscoveryMobileUrl(this, loginUrl) && !(isSalesforceWelcomeDiscoveryMobileUrl(this, proposedSelectedServerUrl))
999-
} ?: false
1000-
1001941
/**
1002942
* Switches between default or Salesforce Welcome Discovery log in as needed
1003-
* using the provided proposed selected server URL.
1004-
* @param uri The proposed selected server URL
943+
* using the provided pending login server URL.
944+
* @param pendingLoginServerUri The pending login server URL
1005945
* @return Boolean true if a switch between default or Salesforce Welcome
1006946
* Discovery log is made, false otherwise.
1007947
*/
1008-
private fun switchDefaultOrSalesforceWelcomeDiscoveryLogin(uri: Uri) =
948+
private fun switchDefaultOrSalesforceWelcomeDiscoveryLogin(pendingLoginServerUri: Uri) =
1009949

1010-
// If the selected server has changed to a new Salesforce Welcome Discovery URL and host.
1011-
if (isSalesforceWelcomeDiscoveryUrlPath(uri)) {
950+
// If the pending login server is a change to a new Salesforce Welcome Discovery URL and host.
951+
if (isSalesforceWelcomeDiscoveryUrlPath(pendingLoginServerUri)) {
1012952

1013953
// Navigate to Salesforce Welcome Discovery.
1014954
startActivity(
1015955
Intent(
1016956
this,
1017-
LoginActivity::class.java
957+
SalesforceSDKManager.getInstance().webViewLoginActivityClass
1018958
).apply {
1019-
data = generateSalesforceWelcomeDiscoveryMobileUrl(uri)
959+
data = generateSalesforceWelcomeDiscoveryMobileUrl(pendingLoginServerUri)
1020960
flags = FLAG_ACTIVITY_SINGLE_TOP
1021961
})
1022962
true
1023963
}
1024964

1025-
// If the new selected server isn't a Salesforce Welcome Discovery URL but the previous was...
1026-
else if (isSwitchFromSalesforceWelcomeDiscoveryToDefaultLogin(uri)) {
965+
// If the pending login server isn't a Salesforce Welcome Discovery URL but the previous was...
966+
else if (viewModel.isSwitchFromSalesforceWelcomeDiscoveryToDefaultLogin(pendingLoginServerUri)) {
1027967

1028-
// Navigate to login.
968+
// Navigate to default login.
1029969
startActivity(
1030970
Intent(
1031971
this,
1032-
LoginActivity::class.java
972+
SalesforceSDKManager.getInstance().webViewLoginActivityClass
1033973
).apply {
1034974
flags = FLAG_ACTIVITY_SINGLE_TOP
1035975
})
@@ -1075,11 +1015,10 @@ open class LoginActivity : FragmentActivity() {
10751015
// endregion
10761016

10771017
/**
1078-
* Applies a new intent to the activity, for instance when the activity is
1079-
* created or receives a new intent.
1080-
* @param intent The new intent
1018+
* (Re-)applies the intent to the activity, for instance when the activity
1019+
* is created or receives a new intent.
10811020
*/
1082-
private fun applyIntent(intent: Intent) {
1021+
private fun applyIntent() {
10831022

10841023
// If the intent is for Salesforce Welcome Discovery, apply it to the activity.
10851024
applySalesforceWelcomeDiscoveryIntent(intent)
@@ -1088,6 +1027,34 @@ open class LoginActivity : FragmentActivity() {
10881027
applyUiBridgeApiFrontDoorUrl(intent)
10891028
}
10901029

1030+
/**
1031+
* Starts a browser custom tab for the OAuth authorization URL according to
1032+
* the authentication configuration. The activity only takes action when
1033+
* browser-based authentication requires a browser custom tab to be started.
1034+
* UI front-door bridge use bypasses the need for browser custom tab.
1035+
* @param authorizationUrl The selected login server's OAuth authorization
1036+
* URL
1037+
* @param activityResultLauncher The activity result launcher to use when
1038+
* browser-based authentication requires a browser custom tab
1039+
* @param isBrowserLoginEnabled Indicates if browser-based authentication is
1040+
* enabled
1041+
* @param isUsingFrontDoorBridge Indicates if a UI bridge API front door
1042+
* bridge URL is in use
1043+
* @param singleServerCustomTabActivity Indicates single server custom
1044+
* browser tab authentication is active
1045+
*/
1046+
private fun startBrowserCustomTabAuthorization(
1047+
authorizationUrl: String,
1048+
activityResultLauncher: ActivityResultLauncher<Intent>,
1049+
isBrowserLoginEnabled: Boolean = SalesforceSDKManager.getInstance().isBrowserLoginEnabled,
1050+
isUsingFrontDoorBridge: Boolean = viewModel.isUsingFrontDoorBridge,
1051+
singleServerCustomTabActivity: Boolean = viewModel.singleServerCustomTabActivity,
1052+
) {
1053+
if ((singleServerCustomTabActivity.or(isBrowserLoginEnabled)).and(!isUsingFrontDoorBridge)) {
1054+
loadLoginPageInCustomTab(authorizationUrl, activityResultLauncher)
1055+
}
1056+
}
1057+
10911058
/**
10921059
* A web view client which intercepts the redirect to the OAuth callback URL. That redirect marks the end of
10931060
* the user facing portion of the authentication flow.
@@ -1478,13 +1445,15 @@ open class LoginActivity : FragmentActivity() {
14781445
): Boolean {
14791446
if (!uri.isHierarchical) return false
14801447
val clientIdParameter = uri.getQueryParameter(SALESFORCE_WELCOME_DISCOVERY_MOBILE_URL_QUERY_PARAMETER_KEY_CLIENT_ID)
1481-
return isSalesforceWelcomeDiscoveryUrlPath(uri) && uri.queryParameterNames?.contains(
1448+
val isDiscovery = isSalesforceWelcomeDiscoveryUrlPath(uri)
1449+
1450+
return isDiscovery && uri.queryParameterNames.contains(
14821451
SALESFORCE_WELCOME_DISCOVERY_MOBILE_URL_QUERY_PARAMETER_KEY_CLIENT_ID
1483-
) != null && uri.queryParameterNames?.contains(
1452+
) && uri.queryParameterNames.contains(
14841453
SALESFORCE_WELCOME_DISCOVERY_MOBILE_URL_QUERY_PARAMETER_KEY_CLIENT_VERSION
1485-
) != null && uri.queryParameterNames?.contains(
1454+
) && uri.queryParameterNames.contains(
14861455
SALESFORCE_WELCOME_DISCOVERY_MOBILE_URL_QUERY_PARAMETER_KEY_CALLBACK_URL
1487-
) != null && clientIdParameter == getBootConfig(context).remoteAccessConsumerKey &&
1456+
) && clientIdParameter == getBootConfig(context).remoteAccessConsumerKey &&
14881457
(clientIdParameter == "SfdcMobileChatterAndroid" || clientIdParameter == "SfdcMobileChatteriOS") // TODO: Keep this list of client ids up to date with those supported by Salesforce Welcome Discovery or remove it when no longer required.
14891458
}
14901459

@@ -1511,7 +1480,7 @@ open class LoginActivity : FragmentActivity() {
15111480
loginHint: String,
15121481
loginHost: String,
15131482
) {
1514-
Intent(context, LoginActivity::class.java).apply {
1483+
Intent(context, SalesforceSDKManager.getInstance().webViewLoginActivityClass).apply {
15151484
putExtra(EXTRA_KEY_LOGIN_HINT, loginHint)
15161485
putExtra(EXTRA_KEY_LOGIN_HOST, loginHost)
15171486
flags = FLAG_ACTIVITY_SINGLE_TOP
@@ -1521,4 +1490,87 @@ open class LoginActivity : FragmentActivity() {
15211490

15221491
// endregion
15231492
}
1493+
1494+
// region Activity Result Callback Classes
1495+
1496+
/**
1497+
* An activity result callback used when browser-based authentication loads
1498+
* the OAuth authorization URL in the external browser custom tab activity.
1499+
* @param activity The login activity. This parameter is intended for
1500+
* testing purposes only. Defaults to this inner class receiver
1501+
*/
1502+
private inner class CustomTabActivityResult(
1503+
private val activity: LoginActivity = this@LoginActivity
1504+
) : ActivityResultCallback<ActivityResult> {
1505+
1506+
override fun onActivityResult(result: ActivityResult) {
1507+
// Check if the user backed out of the custom tab.
1508+
if (result.resultCode == RESULT_CANCELED) {
1509+
if (activity.viewModel.singleServerCustomTabActivity) {
1510+
// Show blank page and spinner until PKCE is done.
1511+
activity.viewModel.loginUrl.value = ABOUT_BLANK
1512+
} else {
1513+
// Don't show server picker if we are re-authenticating with cookie.
1514+
activity.clearWebView(showServerPicker = !activity.sharedBrowserSession)
1515+
}
1516+
}
1517+
}
1518+
}
1519+
1520+
// endregion
1521+
// region Observer Classes
1522+
1523+
/**
1524+
* An observer for browser custom tab URL that continues the authentication
1525+
* flow by loading the login URL in a web browser custom tab when browser-
1526+
* based authentication is required.
1527+
* @param activity The login activity. This parameter is intended for
1528+
* testing purposes only. Defaults to this inner class receiver
1529+
*/
1530+
internal inner class BrowserCustomTabUrlObserver(
1531+
private val activity: LoginActivity = this@LoginActivity
1532+
) : Observer<String> {
1533+
override fun onChanged(value: String) {
1534+
if (value == "about:blank") {
1535+
return
1536+
}
1537+
1538+
activity.startBrowserCustomTabAuthorization(
1539+
authorizationUrl = value,
1540+
activityResultLauncher = activity.customTabLauncher,
1541+
isBrowserLoginEnabled = SalesforceSDKManager.getInstance().isBrowserLoginEnabled,
1542+
)
1543+
}
1544+
}
1545+
1546+
/**
1547+
* An observer for pending login server that continues the authentication
1548+
* flow by determining the switch between default login and Salesforce
1549+
* Welcome Discovery before applying the pending login server to the
1550+
* activity.
1551+
* @param activity The login activity. This parameter is intended for
1552+
* testing purposes only. Defaults to this inner class receiver
1553+
*/
1554+
private inner class PendingServerObserver(
1555+
private val activity: LoginActivity = this@LoginActivity
1556+
) : Observer<String> {
1557+
override fun onChanged(value: String) {
1558+
// Guard against observing a pending login server already provided by the intent data, such as a Salesforce Welcome Discovery mobile URL.
1559+
val pendingServerUri = value.toUri()
1560+
if (activity.intent.data?.host == pendingServerUri.host || activity.intent.getStringExtra(EXTRA_KEY_LOGIN_HOST) == pendingServerUri.host) {
1561+
activity.viewModel.previousPendingServer = value
1562+
return
1563+
}
1564+
1565+
// Use the URL to switch between default or Salesforce Welcome Discovery log in, if applicable.
1566+
if (activity.switchDefaultOrSalesforceWelcomeDiscoveryLogin(pendingServerUri)) {
1567+
activity.viewModel.previousPendingServer = value
1568+
return
1569+
}
1570+
1571+
activity.viewModel.applyPendingServer(pendingLoginServer = value)
1572+
}
1573+
}
1574+
1575+
// endregion
15241576
}

0 commit comments

Comments
 (0)