Skip to content
Closed
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
2 changes: 2 additions & 0 deletions libs/SalesforceSDK/res/values/sf__strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
<string name="sf__setup_biometric_unlock">Setup Biometric Unlock</string>
<string name="sf__back_button_content_description">Back</string>
<string name="sf__loading_indicator">Loading</string>
<string name="sf__cannot_use_another_apps_login_qr_code">Cannot use another app\'s login QR Code. Please log in to this app.</string>
<string name="sf__cannot_use_another_login_hosts_login_qr_code">Cannot use another login host\'s login QR Code. Please log in to this app.</string>

<!-- Server picker -->
<string name="sf__auth_login_production">Production</string>
Expand Down
1 change: 0 additions & 1 deletion libs/SalesforceSDK/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<string name="app_name">SalesforceSDK</string>
<string name="account_type">com.salesforce.androidsdk</string>
<string name="app_package">com.salesforce.androidsdk</string>
<string name="cannot_use_another_apps_login_qr_code">Cannot use another app\'s login QR Code. Please log in to this app.</string>

<!-- 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 -->
<string name="oauth_display_type">touch</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
*
* @author bhariharan
*/
public class LoginServerManager {
public class LoginServerManager implements LoginServerManaging {
Copy link
Contributor Author

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.

// LiveData representation of the users current selected server.
public MutableLiveData<LoginServer> selectedServer = new MutableLiveData<>();

Expand Down Expand Up @@ -271,7 +271,7 @@ public List<LoginServer> getLoginServers() {
} else {
allServers = getLoginServersFromPreferences(runtimePrefs);
}
return allServers;
return allServers; // TODO: Evaluate this new warning. ECJ20250911
}

/**
Expand Down
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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no reason for this to exist. loginServerAtIndex and numberOfLoginServers could be replaced with loginServers.forEach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -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;
Expand Down Expand Up @@ -69,7 +73,8 @@ public enum ConfigKey {

private static RuntimeConfig INSTANCE = null;

RuntimeConfig(Context ctx) {
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce Sep 11, 2025

Choose a reason for hiding this comment

The 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);

Expand Down
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(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@wmathurin - This is nearly identical to the iOS version we created.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: Something like mdmControlledServers might be better?


/**
* 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 addingAndSwitchingLoginServerOverride which lets the test choose the test value. An alternate approach could be to pop and interface on RuntimeConfig and mock it much as we did for LoginServerManager.

) {

/**
* 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be .contains(".my.salesforce.com"), right?

Copy link
Contributor

Choose a reason for hiding this comment

The 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"?

}
Loading
Loading