Skip to content

Commit 2577239

Browse files
@W-19461869: [M1][MSDK13.1] Enable Flexible Server Matching for QR Code Login (Android) (W.I.P.: Code Review Updates w/Remaining To-Dos And Disabling Test That Only Fail In CI)
1 parent 6717f3a commit 2577239

File tree

9 files changed

+182
-284
lines changed

9 files changed

+182
-284
lines changed

libs/SalesforceSDK/src/com/salesforce/androidsdk/config/LoginServerManager.java

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public List<LoginServer> getLoginServers() {
271271
} else {
272272
allServers = getLoginServersFromPreferences(runtimePrefs);
273273
}
274-
return allServers;
274+
return allServers; // TODO: Evaluate this new warning. ECJ20250911
275275
}
276276

277277
/**
@@ -447,37 +447,6 @@ private List<LoginServer> getLoginServersFromPreferences(SharedPreferences prefs
447447
return (!allServers.isEmpty() ? allServers : null);
448448
}
449449

450-
// region Login Server Managing Implementation
451-
452-
/**
453-
* Returns the login server at the specified index.
454-
*
455-
* @param index The index of the login server to retrieve
456-
* @return The Login server instance at the specified index, or null if index is out of bounds
457-
*/
458-
@Override
459-
public LoginServer loginServerAtIndex(int index) {
460-
final List<LoginServer> servers = getLoginServers();
461-
if (servers != null && index >= 0 && index < servers.size()) {
462-
return servers.get(index);
463-
}
464-
return null;
465-
}
466-
467-
/**
468-
* Returns the total number of login servers.
469-
*
470-
* @return The number of available login servers
471-
*/
472-
@Override
473-
public int numberOfLoginServers() {
474-
final List<LoginServer> servers = getLoginServers();
475-
return servers != null ? servers.size() : 0;
476-
}
477-
478-
// endregion
479-
480-
481450
/**
482451
* Class to encapsulate a login server name, URL, index and type (custom or not).
483452
*/

libs/SalesforceSDK/src/com/salesforce/androidsdk/config/LoginServerManaging.kt

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,9 @@ import com.salesforce.androidsdk.config.LoginServerManager.LoginServer
3636
internal interface LoginServerManaging {
3737

3838
/**
39-
* Returns the login server at the specified index.
40-
*
41-
* @param index The index of the login server to retrieve
42-
* @return The login server instance at the specified index, or null if
43-
* index is out of bounds
39+
* Returns the list of login servers.
40+
* @return The list of login servers
4441
*/
45-
fun loginServerAtIndex(index: Int): LoginServer?
46-
47-
/**
48-
* Returns the total number of login servers.
49-
*
50-
* @return The number of available login servers
51-
*/
52-
fun numberOfLoginServers(): Int
42+
val loginServers: List<LoginServer>
5343
}
5444

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ internal class FrontdoorBridgeLoginOverride(
9090
private set
9191

9292
init {
93-
val frontdoorBridgeUrlComponents = frontdoorBridgeUrl.toString()
94-
val frontdoorBridgeUri = frontdoorBridgeUrlComponents.toUri()
95-
val startUrlParam = frontdoorBridgeUri.getQueryParameter("startURL")
93+
val startUrlParam = frontdoorBridgeUrl.getQueryParameter("startURL")
9694

9795
// Check if the client_id matches the app's consumer key
9896
startUrlParam?.let { startUrlString ->
@@ -106,20 +104,20 @@ internal class FrontdoorBridgeLoginOverride(
106104
}
107105

108106
// Check if the front door URL host matches the app's selected login server
109-
val addingAndSwitchingLoginServersAllowedResolved = if (!addingAndSwitchingLoginServersPerMdm) {
110-
addingAndSwitchingLoginServerOverride
111-
} else {
107+
val addingAndSwitchingLoginServersAllowedResolved = if (addingAndSwitchingLoginServersPerMdm) {
112108
addingAndSwitchingLoginServersAllowed
109+
} else {
110+
addingAndSwitchingLoginServerOverride
113111
}
114112

115-
val frontdoorBridgeUrlAppLoginServerMatch = FrontdoorBridgeUrlAppLoginServerMatch(
113+
val frontdoorBridgeUrlAppLoginServerMatch = appLoginServerForFrontdoorBridgeUrl(
116114
frontdoorBridgeUrl = frontdoorBridgeUrl,
117115
loginServerManaging = loginServerManager,
118116
addingAndSwitchingLoginServersAllowed = addingAndSwitchingLoginServersAllowedResolved,
119117
selectedAppLoginServer = selectedAppLoginServer
120118
)
121119

122-
var appLoginServer = frontdoorBridgeUrlAppLoginServerMatch.appLoginServerMatch
120+
var appLoginServer = frontdoorBridgeUrlAppLoginServerMatch
123121
if (appLoginServer == null && addingAndSwitchingLoginServersAllowedResolved) {
124122
appLoginServer = frontdoorBridgeUrl.host
125123
}
@@ -136,13 +134,14 @@ internal class FrontdoorBridgeLoginOverride(
136134
private val addingAndSwitchingLoginServersAllowed: Boolean
137135
get() {
138136
val runtimeConfig = getRuntimeConfig(SalesforceSDKManager.getInstance().appContext)
139-
val onlyShowAuthorizedServers = runtimeConfig.getBoolean(OnlyShowAuthorizedHosts)
137+
// If true, prevents users from modifying the list of hosts that the Salesforce mobile app can connect to.
138+
val onlyShowAuthorizedHosts = runtimeConfig.getBoolean(OnlyShowAuthorizedHosts)
140139
val mdmLoginServers = try {
141140
runtimeConfig.getStringArrayStoredAsArrayOrCSV(AppServiceHosts)
142141
} catch (_: Exception) {
143142
null
144143
}
145-
return !onlyShowAuthorizedServers && (mdmLoginServers?.isEmpty() != false)
144+
return !onlyShowAuthorizedHosts && (mdmLoginServers?.isEmpty() != false)
146145
}
147146

148147
private val loginServerManager: LoginServerManaging

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

Lines changed: 56 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -30,86 +30,77 @@ import android.net.Uri
3030
import com.salesforce.androidsdk.config.LoginServerManaging
3131
import java.net.URL
3232

33-
internal data class FrontdoorBridgeUrlAppLoginServerMatch(
34-
val frontdoorBridgeUrl: Uri,
35-
val loginServerManaging: LoginServerManaging,
36-
val addingAndSwitchingLoginServersAllowed: Boolean,
37-
val selectedAppLoginServer: String
38-
) {
33+
internal fun appLoginServerForFrontdoorBridgeUrl(
34+
frontdoorBridgeUrl: Uri,
35+
loginServerManaging: LoginServerManaging,
36+
addingAndSwitchingLoginServersAllowed: Boolean,
37+
selectedAppLoginServer: String
38+
): String? {
39+
val frontdoorBridgeUrlHost = frontdoorBridgeUrl.host ?: return null
3940

40-
internal val appLoginServerMatch: String? by lazy {
41-
appLoginServerForFrontdoorBridgeUrl(
42-
frontdoorBridgeUrl,
43-
loginServerManaging,
44-
addingAndSwitchingLoginServersAllowed,
45-
selectedAppLoginServer
46-
)
41+
val eligibleAppLoginServers = eligibleAppLoginServersForFrontdoorBridgeUrl(
42+
loginServerManaging,
43+
addingAndSwitchingLoginServersAllowed,
44+
selectedAppLoginServer
45+
)
46+
47+
// TODO: "Would be more efficient to combine this so you aren't iterating through the list twice." ECJ20250911
48+
for (eligibleAppLoginServer in eligibleAppLoginServers) {
49+
if (frontdoorBridgeUrlHost == eligibleAppLoginServer) {
50+
return eligibleAppLoginServer
51+
}
4752
}
4853

49-
private fun appLoginServerForFrontdoorBridgeUrl(
50-
frontdoorBridgeUrl: Uri,
51-
loginServerManaging: LoginServerManaging,
52-
addingAndSwitchingLoginServersAllowed: Boolean,
53-
selectedAppLoginServer: String
54-
): String? {
55-
val frontdoorBridgeUrlHost = frontdoorBridgeUrl.host ?: return null
54+
// TODO: Complete review of both versions of login host soft-matching logic below. ECJ20250911
55+
// Original Notes From Slack
56+
// Let me recap what I have, soft matching is defined as:
57+
// if QR is not a my domain, existing login server must match exactly
58+
// if QR is a my domain, existing login server must either match exactly or match everything after the .my.
59+
// (a) If adding and switching are disallowed, only let the QR through if its login server "soft-matches" the currently selected login server.
60+
// (b) If adding is disallowed but switching is allowed, let the QR through if its login server "soft-matches" any of the login server and switch to it.
61+
// (c) If adding is allowed and switching is allowed, try (b) first, but if no match are found add the QR login server and switch to it.
62+
63+
// Newer Notes From Github
64+
// [Soft match]
65+
// Look at part of the hostname in the QR code that comes after .my. and make sure it appears in the currently selected login server
66+
// also as long as the currently selected login server does not have .my, itself.
67+
// When the currently login server has a .my. the whole hostname should match.
68+
// So mydomain.my.salesforce.com would be allowed if login.salesforce.com is currently selected
69+
// but not if myotherdomain.my.salesforce.com is selected.
5670

57-
val eligibleAppLoginServers = eligibleAppLoginServersForFrontdoorBridgeUrl(
58-
loginServerManaging,
59-
addingAndSwitchingLoginServersAllowed,
60-
selectedAppLoginServer
61-
)
6271

72+
if (frontdoorBridgeUrl.isMyDomain()) {
73+
val frontdoorBridgeUrlMyDomainSuffix = "my.${frontdoorBridgeUrlHost.split(".my.").last()}"
6374
for (eligibleAppLoginServer in eligibleAppLoginServers) {
64-
if (frontdoorBridgeUrlHost == eligibleAppLoginServer) {
75+
if (eligibleAppLoginServer.endsWith(frontdoorBridgeUrlMyDomainSuffix)) {
6576
return eligibleAppLoginServer
6677
}
6778
}
68-
69-
if (frontdoorBridgeUrl.isMyDomain()) {
70-
val frontdoorBridgeUrlMyDomainSuffix = "my.${frontdoorBridgeUrlHost.split(".my.").last()}"
71-
if (frontdoorBridgeUrlMyDomainSuffix.isNotEmpty()) {
72-
for (eligibleAppLoginServer in eligibleAppLoginServers) {
73-
if (eligibleAppLoginServer.endsWith(frontdoorBridgeUrlMyDomainSuffix)) {
74-
return eligibleAppLoginServer
75-
}
76-
}
77-
}
78-
}
79-
80-
return null
8179
}
8280

83-
private fun eligibleAppLoginServersForFrontdoorBridgeUrl(
84-
loginHostStore: LoginServerManaging,
85-
addingAndSwitchingLoginHostsAllowed: Boolean,
86-
selectedAppLoginHost: String
87-
): List<String> {
88-
val results = mutableListOf<String>()
89-
if (addingAndSwitchingLoginHostsAllowed) {
90-
val numberOfHosts = loginHostStore.numberOfLoginServers()
91-
for (i in 0 until numberOfHosts) {
92-
val server = loginHostStore.loginServerAtIndex(i)
93-
server?.let {
94-
try {
95-
val url = URL(it.url)
96-
results.add(url.host)
97-
} catch (_: Exception) {
98-
// Skip invalid URLs
99-
}
100-
}
101-
}
102-
} else {
103-
try {
104-
val url = URL(selectedAppLoginHost)
81+
return null
82+
}
83+
84+
private fun eligibleAppLoginServersForFrontdoorBridgeUrl(
85+
loginHostStore: LoginServerManaging,
86+
addingAndSwitchingLoginHostsAllowed: Boolean,
87+
selectedAppLoginHost: String
88+
): List<String> {
89+
val results = mutableListOf<String>()
90+
if (addingAndSwitchingLoginHostsAllowed) {
91+
for (loginServer in loginHostStore.loginServers ?: return emptyList()) {
92+
runCatching {
93+
val url = URL(loginServer.url)
10594
results.add(url.host)
106-
} catch (_: Exception) {
107-
// If parsing fails, try to use as-is (might already be just a host)
108-
results.add(selectedAppLoginHost)
10995
}
11096
}
111-
return results
97+
} else {
98+
runCatching {
99+
val url = URL(selectedAppLoginHost)
100+
results.add(url.host)
101+
}
112102
}
103+
return results
113104
}
114105

115106
private fun Uri.isMyDomain(): Boolean {

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ import com.salesforce.androidsdk.R.color.sf__background
109109
import com.salesforce.androidsdk.R.color.sf__background_dark
110110
import com.salesforce.androidsdk.R.color.sf__primary_color
111111
import com.salesforce.androidsdk.R.drawable.sf__action_back
112+
import com.salesforce.androidsdk.R.string.sf__biometric_opt_in_title
112113
import com.salesforce.androidsdk.R.string.sf__cannot_use_another_apps_login_qr_code
113114
import com.salesforce.androidsdk.R.string.sf__cannot_use_another_login_hosts_login_qr_code
114-
import com.salesforce.androidsdk.R.string.sf__biometric_opt_in_title
115115
import com.salesforce.androidsdk.R.string.sf__generic_authentication_error_title
116116
import com.salesforce.androidsdk.R.string.sf__jwt_authentication_error
117117
import com.salesforce.androidsdk.R.string.sf__login_with_biometric
@@ -310,7 +310,8 @@ open class LoginActivity : FragmentActivity() {
310310
with(SalesforceSDKManager.getInstance()) {
311311
// Fetch well known config and load in custom tab if required.
312312
fetchAuthenticationConfiguration {
313-
if (isBrowserLoginEnabled && viewModel.frontdoorBridgeLoginOverride == null /* Browser-based authentication is applicable when not authenticating with a front-door bridge URL */) {
313+
/* Browser-based authentication is applicable when not authenticating with a front-door bridge URL */
314+
if (isBrowserLoginEnabled && viewModel.frontdoorBridgeLoginOverride == null) {
314315
if (useWebServerAuthentication) {
315316
viewModel.loginUrl.value?.let { url -> loadLoginPageInCustomTab(url, customTabLauncher) }
316317
} else {
@@ -430,7 +431,7 @@ open class LoginActivity : FragmentActivity() {
430431
*/
431432
@Suppress("MemberVisibilityCanBePrivate")
432433
fun loginWithFrontdoorBridgeUrl(
433-
frontdoorBridgeUrl: Uri,
434+
frontdoorBridgeUrl: String,
434435
pkceCodeVerifier: String?,
435436
) = viewModel.loginWithFrontDoorBridgeUrl(frontdoorBridgeUrl, pkceCodeVerifier)
436437

@@ -878,15 +879,15 @@ open class LoginActivity : FragmentActivity() {
878879
uiBridgeApiParametersFromQrCodeLoginUrl(intent.data?.toString())
879880
} else intent.getStringExtra(EXTRA_KEY_FRONTDOOR_BRIDGE_URL)?.let { frontdoorBridgeUrl ->
880881
UiBridgeApiParameters(
881-
frontdoorBridgeUrl.toUri(),
882+
frontdoorBridgeUrl,
882883
intent.getStringExtra(EXTRA_KEY_PKCE_CODE_VERIFIER)
883884
)
884885
}
885886

886887
// Apply the intent's front door bridge URL parameters to the view model.
887888
val frontdoorBridgeLoginOverride = uiBridgeApiParameters?.frontdoorBridgeUrl?.let { frontdoorBridgeUrl ->
888889
val result = FrontdoorBridgeLoginOverride(
889-
frontdoorBridgeUrl = frontdoorBridgeUrl,
890+
frontdoorBridgeUrl = frontdoorBridgeUrl.toUri(),
890891
codeVerifier = uiBridgeApiParameters.pkceCodeVerifier
891892
)
892893
if (result.matchesConsumerKey && result.matchesLoginHost) {
@@ -1353,7 +1354,7 @@ open class LoginActivity : FragmentActivity() {
13531354
data class UiBridgeApiParameters(
13541355

13551356
/** The front door bridge URL */
1356-
val frontdoorBridgeUrl: Uri,
1357+
val frontdoorBridgeUrl: String,
13571358

13581359
/** The PKCE code verifier */
13591360
val pkceCodeVerifier: String?,
@@ -1408,7 +1409,7 @@ open class LoginActivity : FragmentActivity() {
14081409
uiBridgeApiParameterJsonString: String,
14091410
) = JSONObject(uiBridgeApiParameterJsonString).let { uiBridgeApiParameterJson ->
14101411
UiBridgeApiParameters(
1411-
uiBridgeApiParameterJson.getString(qrCodeLoginUrlJsonFrontdoorBridgeUrlKey).toUri(),
1412+
uiBridgeApiParameterJson.getString(qrCodeLoginUrlJsonFrontdoorBridgeUrlKey),
14121413
when (uiBridgeApiParameterJson.has(qrCodeLoginUrlJsonPkceCodeVerifierKey)) {
14131414
true -> uiBridgeApiParameterJson.optString(qrCodeLoginUrlJsonPkceCodeVerifierKey)
14141415
else -> null

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
*/
2727
package com.salesforce.androidsdk.ui
2828

29-
import android.net.Uri
3029
import android.webkit.CookieManager
3130
import android.webkit.URLUtil
3231
import android.webkit.WebView
@@ -39,6 +38,7 @@ import androidx.compose.ui.graphics.Color
3938
import androidx.compose.ui.graphics.Color.Companion.Black
4039
import androidx.compose.ui.graphics.Color.Companion.White
4140
import androidx.compose.ui.graphics.luminance
41+
import androidx.core.net.toUri
4242
import androidx.lifecycle.MediatorLiveData
4343
import androidx.lifecycle.ViewModel
4444
import androidx.lifecycle.ViewModelProvider
@@ -224,6 +224,11 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() {
224224
/** Reloads the WebView with a newly generated authorization URL. */
225225
open fun reloadWebView() {
226226
if (frontdoorBridgeLoginOverride == null) {
227+
// The Web Server Flow code challenge makes the authorization url unique each time,
228+
// which triggers recomposition. For User Agent Flow, change it to blank.
229+
if (!SalesforceSDKManager.getInstance().useWebServerAuthentication) {
230+
loginUrl.value = ABOUT_BLANK
231+
}
227232
loginUrl.value = getAuthorizationUrl(selectedServer.value ?: return)
228233
}
229234
}
@@ -243,18 +248,17 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() {
243248
* @param pkceCodeVerifier The PKCE code verifier
244249
*/
245250
fun loginWithFrontDoorBridgeUrl(
246-
frontdoorBridgeUrl: Uri,
251+
frontdoorBridgeUrl: String,
247252
pkceCodeVerifier: String?,
248253
) {
249-
val frontdoorBridgeLoginOverride = FrontdoorBridgeLoginOverride(
250-
frontdoorBridgeUrl = frontdoorBridgeUrl,
254+
FrontdoorBridgeLoginOverride(
255+
frontdoorBridgeUrl = frontdoorBridgeUrl.toUri(),
251256
codeVerifier = pkceCodeVerifier
252-
)
253-
254-
// Only assign if both consumer key and login host match
255-
if (frontdoorBridgeLoginOverride.matchesConsumerKey && frontdoorBridgeLoginOverride.matchesLoginHost) {
256-
this@LoginViewModel.frontdoorBridgeLoginOverride = frontdoorBridgeLoginOverride
257-
loginUrl.value = frontdoorBridgeUrl.toString()
257+
).let {
258+
if (it.matchesConsumerKey && it.matchesLoginHost) {
259+
frontdoorBridgeLoginOverride = it
260+
loginUrl.value = frontdoorBridgeUrl
261+
}
258262
}
259263
}
260264

0 commit comments

Comments
 (0)