Skip to content

Commit a92e929

Browse files
@W-19461869: [M1][MSDK13.1] Enable Flexible Server Matching for QR Code Login (Android)
1 parent c9f0586 commit a92e929

File tree

9 files changed

+415
-76
lines changed

9 files changed

+415
-76
lines changed

libs/SalesforceSDK/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<string name="account_type">com.salesforce.androidsdk</string>
99
<string name="app_package">com.salesforce.androidsdk</string>
1010
<string name="cannot_use_another_apps_login_qr_code">Cannot use another app\'s login QR Code. Please log in to this app.</string>
11+
<string name="cannot_use_another_login_hosts_login_qr_code">Cannot use another login host\'s login QR Code. Please log in to this app.</string>
1112

1213
<!-- If you're only supporting recent versions of Android (e.g. 3.x and up), you can override this to be touch and get a better looking login UI -->
1314
<string name="oauth_display_type">touch</string>

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
*
5555
* @author bhariharan
5656
*/
57-
public class LoginServerManager {
57+
public class LoginServerManager implements LoginServerManaging {
5858
// LiveData representation of the users current selected server.
5959
public MutableLiveData<LoginServer> selectedServer = new MutableLiveData<>();
6060

@@ -447,6 +447,37 @@ 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+
450481
/**
451482
* Class to encapsulate a login server name, URL, index and type (custom or not).
452483
*/
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) 2025-present, salesforce.com, inc.
3+
* All rights reserved.
4+
* Redistribution and use of this software in source and binary forms, with or
5+
* without modification, are permitted provided that the following conditions
6+
* are met:
7+
* - Redistributions of source code must retain the above copyright notice, this
8+
* list of conditions and the following disclaimer.
9+
* - Redistributions in binary form must reproduce the above copyright notice,
10+
* this list of conditions and the following disclaimer in the documentation
11+
* and/or other materials provided with the distribution.
12+
* - Neither the name of salesforce.com, inc. nor the names of its contributors
13+
* may be used to endorse or promote products derived from this software without
14+
* specific prior written permission of salesforce.com, inc.
15+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
* POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
28+
package com.salesforce.androidsdk.config
29+
30+
import com.salesforce.androidsdk.config.LoginServerManager.LoginServer
31+
32+
/**
33+
* An object that can manage Salesforce Mobile SDK's list of login server. This
34+
* is functionally equivalent to MSDK's iOS `SFSDKLoginHostStoring` protocol.
35+
*/
36+
internal interface LoginServerManaging {
37+
38+
/**
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
44+
*/
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
53+
}
54+
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright (c) 2025-present, salesforce.com, inc.
3+
* All rights reserved.
4+
* Redistribution and use of this software in source and binary forms, with or
5+
* without modification, are permitted provided that the following conditions
6+
* are met:
7+
* - Redistributions of source code must retain the above copyright notice, this
8+
* list of conditions and the following disclaimer.
9+
* - Redistributions in binary form must reproduce the above copyright notice,
10+
* this list of conditions and the following disclaimer in the documentation
11+
* and/or other materials provided with the distribution.
12+
* - Neither the name of salesforce.com, inc. nor the names of its contributors
13+
* may be used to endorse or promote products derived from this software without
14+
* specific prior written permission of salesforce.com, inc.
15+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
* POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
28+
package com.salesforce.androidsdk.ui
29+
30+
import android.net.Uri
31+
import androidx.core.net.toUri
32+
import com.salesforce.androidsdk.app.SalesforceSDKManager
33+
import com.salesforce.androidsdk.config.BootConfig
34+
import com.salesforce.androidsdk.config.LoginServerManaging
35+
import com.salesforce.androidsdk.config.RuntimeConfig.ConfigKey.AppServiceHosts
36+
import com.salesforce.androidsdk.config.RuntimeConfig.ConfigKey.OnlyShowAuthorizedHosts
37+
import com.salesforce.androidsdk.config.RuntimeConfig.getRuntimeConfig
38+
39+
/**
40+
* For Salesforce Identity UI Bridge API support, an overriding front door
41+
* bridge URL to use in place of the default initial URL.
42+
*/
43+
internal class FrontdoorBridgeLoginOverride(
44+
/**
45+
* For Salesforce Identity UI Bridge API support, an overriding front door
46+
* bridge URL to use in place of the default initial URL
47+
*/
48+
val frontdoorBridgeUrl: Uri,
49+
50+
/**
51+
* For Salesforce Identity UI Bridge API support, the optional web server
52+
* flow code verifier accompanying the front door bridge URL
53+
*/
54+
val codeVerifier: String? = null,
55+
56+
/**
57+
* The selected app login server. This is intended for test automation only
58+
*/
59+
selectedAppLoginServer: String = SalesforceSDKManager.getInstance().loginServerManager.selectedLoginServer.url,
60+
61+
/**
62+
* The preference for using mobile device management preferences for
63+
* allowing the addition and switching of app login servers. This is
64+
* intended for test automation only
65+
*/
66+
addingAndSwitchingLoginServersPerMdm: Boolean = true,
67+
68+
/**
69+
* The preference for allowing the addition and switching of app login
70+
* servers when the MDM preference is ignored. This is intended for test
71+
* automation only
72+
*/
73+
addingAndSwitchingLoginServerOverride: Boolean = false
74+
) {
75+
76+
/**
77+
* For Salesforce Identity UI Bridge API support, indicates if the
78+
* overriding front door bridge URL has a consumer key value that matches
79+
* the app config.
80+
*/
81+
var matchesConsumerKey: Boolean = false
82+
private set
83+
84+
/**
85+
* For Salesforce Identity UI Bridge API support, indicates if the
86+
* overriding front door bridge URL has a host that matches the app's
87+
* selected login server.
88+
*/
89+
var matchesLoginHost: Boolean = false
90+
private set
91+
92+
init {
93+
val frontdoorBridgeUrlComponents = frontdoorBridgeUrl.toString()
94+
val frontdoorBridgeUri = frontdoorBridgeUrlComponents.toUri()
95+
val startUrlParam = frontdoorBridgeUri.getQueryParameter("startURL")
96+
97+
// Check if the client_id matches the app's consumer key
98+
startUrlParam?.let { startUrlString ->
99+
val startUri = startUrlString.toUri()
100+
val frontdoorBridgeUrlClientId = startUri.getQueryParameter("client_id")
101+
102+
frontdoorBridgeUrlClientId?.let { clientId ->
103+
val appConsumerKey = BootConfig.getBootConfig(SalesforceSDKManager.getInstance().appContext).remoteAccessConsumerKey
104+
matchesConsumerKey = clientId == appConsumerKey
105+
}
106+
}
107+
108+
// Check if the front door URL host matches the app's selected login server
109+
val addingAndSwitchingLoginServersAllowedResolved = if (!addingAndSwitchingLoginServersPerMdm) {
110+
addingAndSwitchingLoginServerOverride
111+
} else {
112+
addingAndSwitchingLoginServersAllowed
113+
}
114+
115+
val frontdoorBridgeUrlAppLoginServerMatch = FrontdoorBridgeUrlAppLoginServerMatch(
116+
frontdoorBridgeUrl = frontdoorBridgeUrl,
117+
loginServerManaging = loginServerManager,
118+
addingAndSwitchingLoginServersAllowed = addingAndSwitchingLoginServersAllowedResolved,
119+
selectedAppLoginServer = selectedAppLoginServer
120+
)
121+
122+
var appLoginServer = frontdoorBridgeUrlAppLoginServerMatch.appLoginServerMatch
123+
if (appLoginServer == null && addingAndSwitchingLoginServersAllowedResolved) {
124+
appLoginServer = frontdoorBridgeUrl.host
125+
}
126+
127+
appLoginServer?.let { appLoginServer ->
128+
matchesLoginHost = true
129+
// Set the login appLoginServer on the server manager
130+
val loginServerManager = SalesforceSDKManager.getInstance().loginServerManager
131+
val loginUrl = "https://$appLoginServer"
132+
loginServerManager.addCustomLoginServer(loginUrl, loginUrl)
133+
}
134+
}
135+
136+
private val addingAndSwitchingLoginServersAllowed: Boolean
137+
get() {
138+
val runtimeConfig = getRuntimeConfig(SalesforceSDKManager.getInstance().appContext)
139+
val onlyShowAuthorizedServers = runtimeConfig.getBoolean(OnlyShowAuthorizedHosts)
140+
val mdmLoginServers = try {
141+
runtimeConfig.getStringArrayStoredAsArrayOrCSV(AppServiceHosts)
142+
} catch (_: Exception) {
143+
null
144+
}
145+
return !onlyShowAuthorizedServers && (mdmLoginServers?.isEmpty() != false)
146+
}
147+
148+
private val loginServerManager: LoginServerManaging
149+
get() = SalesforceSDKManager.getInstance().loginServerManager
150+
}
151+
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright (c) 2025-present, salesforce.com, inc.
3+
* All rights reserved.
4+
* Redistribution and use of this software in source and binary forms, with or
5+
* without modification, are permitted provided that the following conditions
6+
* are met:
7+
* - Redistributions of source code must retain the above copyright notice, this
8+
* list of conditions and the following disclaimer.
9+
* - Redistributions in binary form must reproduce the above copyright notice,
10+
* this list of conditions and the following disclaimer in the documentation
11+
* and/or other materials provided with the distribution.
12+
* - Neither the name of salesforce.com, inc. nor the names of its contributors
13+
* may be used to endorse or promote products derived from this software without
14+
* specific prior written permission of salesforce.com, inc.
15+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
* POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package com.salesforce.androidsdk.ui
28+
29+
import android.net.Uri
30+
import com.salesforce.androidsdk.config.LoginServerManaging
31+
import java.net.URL
32+
33+
internal data class FrontdoorBridgeUrlAppLoginServerMatch(
34+
val frontdoorBridgeUrl: Uri,
35+
val loginServerManaging: LoginServerManaging,
36+
val addingAndSwitchingLoginServersAllowed: Boolean,
37+
val selectedAppLoginServer: String
38+
) {
39+
40+
internal val appLoginServerMatch: String? by lazy {
41+
appLoginServerForFrontdoorBridgeUrl(
42+
frontdoorBridgeUrl,
43+
loginServerManaging,
44+
addingAndSwitchingLoginServersAllowed,
45+
selectedAppLoginServer
46+
)
47+
}
48+
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
56+
57+
val eligibleAppLoginServers = eligibleAppLoginServersForFrontdoorBridgeUrl(
58+
loginServerManaging,
59+
addingAndSwitchingLoginServersAllowed,
60+
selectedAppLoginServer
61+
)
62+
63+
for (eligibleAppLoginServer in eligibleAppLoginServers) {
64+
if (frontdoorBridgeUrlHost == eligibleAppLoginServer) {
65+
return eligibleAppLoginServer
66+
}
67+
}
68+
69+
if (frontdoorBridgeUrl.isMyDomain()) {
70+
val frontdoorBridgeUrlMyDomainSuffix = 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
81+
}
82+
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+
results.add(selectedAppLoginHost)
104+
}
105+
return results
106+
}
107+
}
108+
109+
private fun Uri.isMyDomain(): Boolean {
110+
return host?.contains(".my.") == true
111+
}

0 commit comments

Comments
 (0)