-
Notifications
You must be signed in to change notification settings - Fork 392
@W-19461869: [M1][MSDK13.1] Enable Flexible Server Matching for QR Code Login (Android) #2762
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a92e929
250fe94
f4ba7a9
de898b2
aa5ea4d
6717f3a
2577239
d9a6f19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Copyright (c) 2025-present, salesforce.com, inc. | ||
* All rights reserved. | ||
* Redistribution and use of this software in source and binary forms, with or | ||
* without modification, are permitted provided that the following conditions | ||
* are met: | ||
* - Redistributions of source code must retain the above copyright notice, this | ||
* list of conditions and the following disclaimer. | ||
* - Redistributions in binary form must reproduce the above copyright notice, | ||
* this list of conditions and the following disclaimer in the documentation | ||
* and/or other materials provided with the distribution. | ||
* - Neither the name of salesforce.com, inc. nor the names of its contributors | ||
* may be used to endorse or promote products derived from this software without | ||
* specific prior written permission of salesforce.com, inc. | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | ||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
* POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
|
||
package com.salesforce.androidsdk.config | ||
|
||
import com.salesforce.androidsdk.config.LoginServerManager.LoginServer | ||
|
||
/** | ||
* An object that can manage Salesforce Mobile SDK's list of login server. This | ||
* is functionally equivalent to MSDK's iOS `SFSDKLoginHostStoring` protocol. | ||
*/ | ||
internal interface LoginServerManaging { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no reason for this to exist. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. We can replace these two methods which were a carry-over from the iOS implementation. |
||
|
||
/** | ||
* Returns the list of login servers. | ||
* @return The list of login servers | ||
*/ | ||
val loginServers: List<LoginServer> | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,10 +26,14 @@ | |
*/ | ||
package com.salesforce.androidsdk.config; | ||
|
||
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE; | ||
|
||
import android.content.Context; | ||
import android.content.RestrictionsManager; | ||
import android.os.Bundle; | ||
|
||
import androidx.annotation.VisibleForTesting; | ||
|
||
import com.salesforce.androidsdk.analytics.EventBuilderHelper; | ||
import com.salesforce.androidsdk.app.Features; | ||
import com.salesforce.androidsdk.app.SalesforceSDKManager; | ||
|
@@ -69,7 +73,8 @@ public enum ConfigKey { | |
|
||
private static RuntimeConfig INSTANCE = null; | ||
|
||
RuntimeConfig(Context ctx) { | ||
@VisibleForTesting(otherwise = PACKAGE_PRIVATE) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not know you could could use the Kotlin style named parameter syntax in a Java file. Must be because it is an annotation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I recall using named parameters with Java annotations all the way back to Java 5 and Hibernate ORM (2005), which used it a lot. That's an interesting place where named parameters snuck in early and I'd never thought about it 🙂 |
||
public RuntimeConfig(Context ctx) { | ||
configurations = getRestrictions(ctx); | ||
isManaged = hasRestrictionsProvider(ctx); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
/* | ||
* Copyright (c) 2025-present, salesforce.com, inc. | ||
* All rights reserved. | ||
* Redistribution and use of this software in source and binary forms, with or | ||
* without modification, are permitted provided that the following conditions | ||
* are met: | ||
* - Redistributions of source code must retain the above copyright notice, this | ||
* list of conditions and the following disclaimer. | ||
* - Redistributions in binary form must reproduce the above copyright notice, | ||
* this list of conditions and the following disclaimer in the documentation | ||
* and/or other materials provided with the distribution. | ||
* - Neither the name of salesforce.com, inc. nor the names of its contributors | ||
* may be used to endorse or promote products derived from this software without | ||
* specific prior written permission of salesforce.com, inc. | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | ||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
* POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
|
||
package com.salesforce.androidsdk.ui | ||
|
||
import android.net.Uri | ||
import androidx.core.net.toUri | ||
import com.salesforce.androidsdk.app.SalesforceSDKManager | ||
import com.salesforce.androidsdk.config.BootConfig | ||
import com.salesforce.androidsdk.config.LoginServerManaging | ||
import com.salesforce.androidsdk.config.RuntimeConfig.ConfigKey.AppServiceHosts | ||
import com.salesforce.androidsdk.config.RuntimeConfig.ConfigKey.OnlyShowAuthorizedHosts | ||
import com.salesforce.androidsdk.config.RuntimeConfig.getRuntimeConfig | ||
|
||
/** | ||
* For Salesforce Identity UI Bridge API support, an overriding front door | ||
* bridge URL to use in place of the default initial URL. | ||
*/ | ||
internal class FrontdoorBridgeLoginOverride( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @wmathurin - This is nearly identical to the iOS version we created. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class encapsulates the otherwise separate front-door bridge properties that were attached to unrelated classes along with all the logic and tests that has accumulated around them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really don't love the very overly documented generated code :/ |
||
/** | ||
* For Salesforce Identity UI Bridge API support, an overriding front door | ||
* bridge URL to use in place of the default initial URL | ||
*/ | ||
val frontdoorBridgeUrl: Uri, | ||
|
||
/** | ||
* For Salesforce Identity UI Bridge API support, the optional web server | ||
* flow code verifier accompanying the front door bridge URL | ||
*/ | ||
val codeVerifier: String? = null, | ||
|
||
/** | ||
* The selected app login server. This is intended for test automation only | ||
*/ | ||
selectedAppLoginServer: String = SalesforceSDKManager.getInstance().loginServerManager.selectedLoginServer.url, | ||
|
||
/** | ||
* The preference for using mobile device management preferences for | ||
* allowing the addition and switching of app login servers. This is | ||
* intended for test automation only | ||
*/ | ||
addingAndSwitchingLoginServersPerMdm: Boolean = true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: Something like |
||
|
||
/** | ||
* The preference for allowing the addition and switching of app login | ||
* servers when the MDM preference is ignored. This is intended for test | ||
* automation only | ||
*/ | ||
addingAndSwitchingLoginServerOverride: Boolean = false | ||
Comment on lines
+68
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why (even for testing) would we ever want to ignore a hard requirement from an MDM? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is never set outside of tests why does it exists. Why test both true and false in tests if it can only ever be false in production? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is just there for testing, true. It's use is paired with |
||
) { | ||
|
||
/** | ||
* For Salesforce Identity UI Bridge API support, indicates if the | ||
* overriding front door bridge URL has a consumer key value that matches | ||
* the app config. | ||
*/ | ||
var matchesConsumerKey: Boolean = false | ||
private set | ||
|
||
/** | ||
* For Salesforce Identity UI Bridge API support, indicates if the | ||
* overriding front door bridge URL has a host that matches the app's | ||
* selected login server. | ||
*/ | ||
var matchesLoginHost: Boolean = false | ||
private set | ||
|
||
init { | ||
val startUrlParam = frontdoorBridgeUrl.getQueryParameter("startURL") | ||
|
||
// Check if the client_id matches the app's consumer key | ||
startUrlParam?.let { startUrlString -> | ||
val startUri = startUrlString.toUri() | ||
val frontdoorBridgeUrlClientId = startUri.getQueryParameter("client_id") | ||
|
||
frontdoorBridgeUrlClientId?.let { clientId -> | ||
val appConsumerKey = BootConfig.getBootConfig(SalesforceSDKManager.getInstance().appContext).remoteAccessConsumerKey | ||
matchesConsumerKey = clientId == appConsumerKey | ||
} | ||
} | ||
|
||
// Check if the front door URL host matches the app's selected login server | ||
val addingAndSwitchingLoginServersAllowedResolved = if (addingAndSwitchingLoginServersPerMdm) { | ||
addingAndSwitchingLoginServersAllowed | ||
} else { | ||
addingAndSwitchingLoginServerOverride | ||
} | ||
|
||
val frontdoorBridgeUrlAppLoginServerMatch = appLoginServerForFrontdoorBridgeUrl( | ||
frontdoorBridgeUrl = frontdoorBridgeUrl, | ||
loginServerManaging = loginServerManager, | ||
addingAndSwitchingLoginServersAllowed = addingAndSwitchingLoginServersAllowedResolved, | ||
selectedAppLoginServer = selectedAppLoginServer | ||
) | ||
|
||
var appLoginServer = frontdoorBridgeUrlAppLoginServerMatch | ||
if (appLoginServer == null && addingAndSwitchingLoginServersAllowedResolved) { | ||
appLoginServer = frontdoorBridgeUrl.host | ||
} | ||
|
||
appLoginServer?.let { server -> | ||
matchesLoginHost = true | ||
// Set the login server on the server manager | ||
val loginServerManager = SalesforceSDKManager.getInstance().loginServerManager | ||
val loginUrl = "https://$server" | ||
loginServerManager.addCustomLoginServer(loginUrl, loginUrl) | ||
} | ||
} | ||
|
||
private val addingAndSwitchingLoginServersAllowed: Boolean | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: Why are these private vals at the bottom of the file? |
||
get() { | ||
val runtimeConfig = getRuntimeConfig(SalesforceSDKManager.getInstance().appContext) | ||
// If true, prevents users from modifying the list of hosts that the Salesforce mobile app can connect to. | ||
val onlyShowAuthorizedHosts = runtimeConfig.getBoolean(OnlyShowAuthorizedHosts) | ||
val mdmLoginServers = try { | ||
runtimeConfig.getStringArrayStoredAsArrayOrCSV(AppServiceHosts) | ||
} catch (_: Exception) { | ||
null | ||
} | ||
return !onlyShowAuthorizedHosts && (mdmLoginServers?.isEmpty() != false) | ||
} | ||
|
||
private val loginServerManager: LoginServerManaging | ||
get() = SalesforceSDKManager.getInstance().loginServerManager | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
/* | ||
* Copyright (c) 2025-present, salesforce.com, inc. | ||
* All rights reserved. | ||
* Redistribution and use of this software in source and binary forms, with or | ||
* without modification, are permitted provided that the following conditions | ||
* are met: | ||
* - Redistributions of source code must retain the above copyright notice, this | ||
* list of conditions and the following disclaimer. | ||
* - Redistributions in binary form must reproduce the above copyright notice, | ||
* this list of conditions and the following disclaimer in the documentation | ||
* and/or other materials provided with the distribution. | ||
* - Neither the name of salesforce.com, inc. nor the names of its contributors | ||
* may be used to endorse or promote products derived from this software without | ||
* specific prior written permission of salesforce.com, inc. | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | ||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
* POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
package com.salesforce.androidsdk.ui | ||
|
||
import android.net.Uri | ||
import com.salesforce.androidsdk.config.LoginServerManaging | ||
import java.net.URL | ||
|
||
internal fun appLoginServerForFrontdoorBridgeUrl( | ||
frontdoorBridgeUrl: Uri, | ||
loginServerManaging: LoginServerManaging, | ||
addingAndSwitchingLoginServersAllowed: Boolean, | ||
selectedAppLoginServer: String | ||
): String? { | ||
val frontdoorBridgeUrlHost = frontdoorBridgeUrl.host ?: return null | ||
|
||
val eligibleAppLoginServers = eligibleAppLoginServersForFrontdoorBridgeUrl( | ||
loginServerManaging, | ||
addingAndSwitchingLoginServersAllowed, | ||
selectedAppLoginServer | ||
) | ||
|
||
// TODO: "Would be more efficient to combine this so you aren't iterating through the list twice." ECJ20250911 | ||
for (eligibleAppLoginServer in eligibleAppLoginServers) { | ||
if (frontdoorBridgeUrlHost == eligibleAppLoginServer) { | ||
return eligibleAppLoginServer | ||
} | ||
} | ||
|
||
// TODO: Complete review of both versions of login host soft-matching logic below. ECJ20250911 | ||
// Original Notes From Slack | ||
// Let me recap what I have, soft matching is defined as: | ||
// if QR is not a my domain, existing login server must match exactly | ||
// if QR is a my domain, existing login server must either match exactly or match everything after the .my. | ||
// (a) If adding and switching are disallowed, only let the QR through if its login server "soft-matches" the currently selected login server. | ||
// (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. | ||
// (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. | ||
|
||
// Newer Notes From Github | ||
// [Soft match] | ||
// 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 | ||
// also as long as the currently selected login server does not have .my, itself. | ||
// When the currently login server has a .my. the whole hostname should match. | ||
// So mydomain.my.salesforce.com would be allowed if login.salesforce.com is currently selected | ||
// but not if myotherdomain.my.salesforce.com is selected. | ||
|
||
|
||
if (frontdoorBridgeUrl.isMyDomain()) { | ||
val frontdoorBridgeUrlMyDomainSuffix = "my.${frontdoorBridgeUrlHost.split(".my.").last()}" | ||
for (eligibleAppLoginServer in eligibleAppLoginServers) { | ||
if (eligibleAppLoginServer.endsWith(frontdoorBridgeUrlMyDomainSuffix)) { | ||
return eligibleAppLoginServer | ||
} | ||
} | ||
} | ||
|
||
return null | ||
} | ||
|
||
private fun eligibleAppLoginServersForFrontdoorBridgeUrl( | ||
loginHostStore: LoginServerManaging, | ||
addingAndSwitchingLoginHostsAllowed: Boolean, | ||
selectedAppLoginHost: String | ||
): List<String> { | ||
val results = mutableListOf<String>() | ||
if (addingAndSwitchingLoginHostsAllowed) { | ||
for (loginServer in loginHostStore.loginServers ?: return emptyList()) { | ||
runCatching { | ||
val url = URL(loginServer.url) | ||
results.add(url.host) | ||
} | ||
} | ||
} else { | ||
runCatching { | ||
val url = URL(selectedAppLoginHost) | ||
results.add(url.host) | ||
} | ||
} | ||
return results | ||
} | ||
|
||
private fun Uri.isMyDomain(): Boolean { | ||
return host?.contains(".my.") == true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apparently this is not true for internal test environments. Perhaps we should use regex to check with a wildcard for something like ".my.*salesforce.com"? |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wmathurin - Here's a comparable interface for the protocol we added on iOS. It does really help the tests and logic objects remain consistent.