Skip to content
16 changes: 16 additions & 0 deletions libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@
23CAB1312DD515B500B8929B /* SFLoginViewController+Deep-Linking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23CAB1302DD515B500B8929B /* SFLoginViewController+Deep-Linking.swift */; };
23CE44212DEE258800ADC770 /* RestClient+WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23CE44202DEE257900ADC770 /* RestClient+WebSocket.swift */; };
23D626992DF9DF2D00B898D0 /* URLSessionWebSocketTask+WebSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D626982DF9DF1E00B898D0 /* URLSessionWebSocketTask+WebSocketClient.swift */; };
23D96B6F2E145AC20004B06A /* DomainDiscoveryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D96B6E2E145AC20004B06A /* DomainDiscoveryCoordinator.swift */; };
23D96B762E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D96B752E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift */; };
23EDDEFE2DE0F7620024AD39 /* URLRequest+RestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EDDEF82DE0F7480024AD39 /* URLRequest+RestRequest.swift */; };
23EDDF022DE0F9EF0024AD39 /* URLRequest+RestRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EDDF012DE0F9EF0024AD39 /* URLRequest+RestRequestTests.swift */; };
23EED88A2E2ACD3300646B10 /* SFOAuthCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EED8892E2ACD3300646B10 /* SFOAuthCoordinatorTests.swift */; };
23EED8912E2ACF3900646B10 /* MockNavigationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EED8902E2ACF3100646B10 /* MockNavigationAction.swift */; };
444B95D01E83251900908C61 /* UIColor+SFColorsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 444B95CF1E83251900908C61 /* UIColor+SFColorsTests.m */; };
4F06AF731C49A16A00F70798 /* NSURL+SFStringUtilsTests.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F06AF5D1C49A16A00F70798 /* NSURL+SFStringUtilsTests.h */; };
4F06AF751C49A16A00F70798 /* SalesforceOAuthUnitTests.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F06AF5F1C49A16A00F70798 /* SalesforceOAuthUnitTests.h */; };
Expand Down Expand Up @@ -559,8 +563,12 @@
23CAB1302DD515B500B8929B /* SFLoginViewController+Deep-Linking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "SFLoginViewController+Deep-Linking.swift"; path = "Login/SFLoginViewController+Deep-Linking.swift"; sourceTree = "<group>"; };
23CE44202DEE257900ADC770 /* RestClient+WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RestClient+WebSocket.swift"; sourceTree = "<group>"; };
23D626982DF9DF1E00B898D0 /* URLSessionWebSocketTask+WebSocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionWebSocketTask+WebSocketClient.swift"; sourceTree = "<group>"; };
23D96B6E2E145AC20004B06A /* DomainDiscoveryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDiscoveryCoordinator.swift; sourceTree = "<group>"; };
23D96B752E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DomainDiscoveryCoordinatorTests.swift; path = SalesforceSDKCoreTests/DomainDiscoveryCoordinatorTests.swift; sourceTree = SOURCE_ROOT; };
23EDDEF82DE0F7480024AD39 /* URLRequest+RestRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+RestRequest.swift"; sourceTree = "<group>"; };
23EDDF012DE0F9EF0024AD39 /* URLRequest+RestRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "URLRequest+RestRequestTests.swift"; path = "SalesforceSDKCoreTests/URLRequest+RestRequestTests.swift"; sourceTree = SOURCE_ROOT; };
23EED8892E2ACD3300646B10 /* SFOAuthCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SFOAuthCoordinatorTests.swift; path = SalesforceSDKCoreTests/SFOAuthCoordinatorTests.swift; sourceTree = SOURCE_ROOT; };
23EED8902E2ACF3100646B10 /* MockNavigationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNavigationAction.swift; sourceTree = "<group>"; };
444B95CF1E83251900908C61 /* UIColor+SFColorsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIColor+SFColorsTests.m"; path = "SalesforceSDKCoreTests/UIColor+SFColorsTests.m"; sourceTree = SOURCE_ROOT; };
4F06AF5D1C49A16A00F70798 /* NSURL+SFStringUtilsTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSURL+SFStringUtilsTests.h"; path = "SalesforceSDKCoreTests/NSURL+SFStringUtilsTests.h"; sourceTree = SOURCE_ROOT; };
4F06AF5E1C49A16A00F70798 /* NSURL+SFStringUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSURL+SFStringUtilsTests.m"; path = "SalesforceSDKCoreTests/NSURL+SFStringUtilsTests.m"; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -1016,6 +1024,7 @@
23CAB0F82DCBE51F00B8929B /* Mocks */ = {
isa = PBXGroup;
children = (
23EED8902E2ACF3100646B10 /* MockNavigationAction.swift */,
23CAB0F72DCBE51F00B8929B /* MockRestClient.swift */,
);
name = Mocks;
Expand All @@ -1025,6 +1034,8 @@
4F7EB3F61BFFC84700768720 /* SalesforceSDKCoreTests */ = {
isa = PBXGroup;
children = (
23EED8892E2ACD3300646B10 /* SFOAuthCoordinatorTests.swift */,
23D96B752E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift */,
230834872DF8A8F300C7CBF7 /* WebSocketClientTests.swift */,
230834852DF8938D00C7CBF7 /* URLSessionTask+RetryPolicyTests.swift */,
23EDDF012DE0F9EF0024AD39 /* URLRequest+RestRequestTests.swift */,
Expand Down Expand Up @@ -1187,6 +1198,7 @@
4F96FCC41BFD32130022F021 /* OAuth */ = {
isa = PBXGroup;
children = (
23D96B6E2E145AC20004B06A /* DomainDiscoveryCoordinator.swift */,
4F8A3B002CEC202F00ECDC76 /* JwtAccessToken.swift */,
4F96FCC61BFD32130022F021 /* SFOAuthCoordinator.h */,
4F96FCC71BFD32130022F021 /* SFOAuthCoordinator.m */,
Expand Down Expand Up @@ -2192,13 +2204,15 @@
B7E1A50E1F43B0FE007AC36A /* SFSDKWindowManagerTests.m in Sources */,
4F06AF8A1C49A18E00F70798 /* SalesforceOAuthUnitTests.m in Sources */,
23A4C7462D0CAFA000DF55EB /* ScreenLockManagerTests.swift in Sources */,
23EED88A2E2ACD3300646B10 /* SFOAuthCoordinatorTests.swift in Sources */,
B7A901BE228E4DFB0036D749 /* SFSDKLogoutBlocker.m in Sources */,
693E623B24A29B6B0017B222 /* SFSDKKeyValueEncryptedFileStoreTests.m in Sources */,
8214D955205316BA0007349E /* SFSDKSalesforceAnalyticsManagerTests.m in Sources */,
4F06AF891C49A18E00F70798 /* NSURL+SFStringUtilsTests.m in Sources */,
B759CD891F8BDBAC0081AA87 /* SDSDKAlertMessageTest.m in Sources */,
4F06AF8C1C49A18E00F70798 /* SalesforceSDKIdentityTests.m in Sources */,
4F06AF921C49A18E00F70798 /* SFPreferencesTests.m in Sources */,
23EED8912E2ACF3900646B10 /* MockNavigationAction.swift in Sources */,
4F755F5820D48F8600CE4E0E /* NSString+SFAdditionsTests.m in Sources */,
6990CBC029F5AF56004A5F8D /* SFSDKIDPAuthCodeLoginRequestCommandTest.m in Sources */,
CEB98EE01F86E7CF0083AB9C /* SFSDKAuthRequestCommandTest.m in Sources */,
Expand All @@ -2209,6 +2223,7 @@
23CAB0F92DCBE51F00B8929B /* MockRestClient.swift in Sources */,
B7E66AE923763278005A652E /* RestClientPublisherTests.swift in Sources */,
697F5C4F267BE29A00F382A9 /* EncryptionTests.swift in Sources */,
23D96B762E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift in Sources */,
B7A4AE4922E8CA780060E737 /* SFSDKAuthUtilTests.swift in Sources */,
69DFE06C2B969C25000906E4 /* PushNotificationDecryptionTests.swift in Sources */,
4F7EB4161BFFC8D700768720 /* SDKCommonNSDataTests.m in Sources */,
Expand Down Expand Up @@ -2330,6 +2345,7 @@
CE4CE3A21C0E5279009F6029 /* NSURL+SFStringUtils.m in Sources */,
69BDD82F26F90AA400C26D77 /* DecryptStream.swift in Sources */,
23A4C74E2D0CB68B00DF55EB /* NativeLoginManagerInternal.swift in Sources */,
23D96B6F2E145AC20004B06A /* DomainDiscoveryCoordinator.swift in Sources */,
23A4C74F2D0CB68B00DF55EB /* NativeLoginManager.swift in Sources */,
B7CD6D671F79CFC900F99F81 /* SFUserAccountManager+URLHandlers.m in Sources */,
69848CB62363FA3E00893E57 /* SFSDKPushNotificationError.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ - (id)init {
SFManagedPreferences *managedPreferences = [SFManagedPreferences sharedPreferences];
SFSDKLoginHost *production = [SFSDKLoginHost hostWithName:[SFSDKResourceUtils localizedString:@"LOGIN_SERVER_PRODUCTION"] host:@"login.salesforce.com" deletable:NO];
SFSDKLoginHost *sandbox = [SFSDKLoginHost hostWithName:[SFSDKResourceUtils localizedString:@"LOGIN_SERVER_SANDBOX"] host:@"test.salesforce.com" deletable:NO];
SFSDKLoginHost *welcome = [SFSDKLoginHost hostWithName:[SFSDKResourceUtils localizedString:@"LOGIN_SERVER_WELCOME"] host:@"welcome.salesforce.com" deletable:NO];
SFSDKLoginHost *welcome = [SFSDKLoginHost hostWithName:[SFSDKResourceUtils localizedString:@"LOGIN_SERVER_WELCOME"] host:@"welcome.salesforce.com/discovery" deletable:NO];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@wmathurin This what you hand in mind for the disco comain?


// Add the Production, Sandbox and Welcome login hosts, unless an MDM policy explicitly forbids this.
if (!(managedPreferences.hasManagedPreferences && managedPreferences.onlyShowAuthorizedHosts)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Foundation
import WebKit

enum DomainDiscovery: String {
/// The callback URL used for domain discovery.
case callbackURL = "sfdc://discocallback"

enum URLComponent: String {
case path = "/discovery"
case scheme = "https"

enum QueryItem: String {
case clientID = "client_id"
case clientVersion = "client_version"
case callbackURL = "callback_url"
}
}

/* TODO: Keep this list of client ids up to date with those
* supported by Salesforce Welcome Discovery or remove it
* when no longer required.
*/
static let supportedClientIds: Set<String> = [
Comment thread
Crebs marked this conversation as resolved.
"SfdcMobileChatterAndroid",
"SfdcMobileChatteriOS"
]
}

/// Represents the result of a domain discovery operation.
@objc(SFDomainDiscoveryResult)
public class DomainDiscoveryResult: NSObject {
/// The login hint returned from domain discovery.
@objc public let loginHint: String
/// The discovered My Domain value.
@objc public let myDomain: String

init(loginHint: String, myDomain: String) {
self.loginHint = loginHint
self.myDomain = myDomain
}
}

/// Coordinator for My Domain discovery via WKWebView before OAuth login.
///
/// This class loads a discovery URL in a WKWebView and waits for a callback URL to be hit, returning the discovered domain and login hint.
@objc(SFDomainDiscoveryCoordinator)
public class DomainDiscoveryCoordinator: NSObject {
/// Starts the domain discovery process by loading the discovery URL in the provided WKWebView.
///
/// - Parameters:
/// - webview: The WKWebView in which to load the discovery URL for domain discovery.
/// - credentials: The OAuth credentials containing clientId and domain.
///
/// This method loads the discovery URL in the given webview. The result of the discovery should be handled by monitoring navigation actions and calling `handle(webAction:)`.
@MainActor
@objc public func runMyDomainsDiscovery(on webview: WKWebView,
with credentials: OAuthCredentials) {
guard let clientId = credentials.clientId,
let clientVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let domain = credentials.domain else {
SFSDKCoreLogger.e(classForCoder, message: "Missing required credentials")
return
}
guard let url = Self.buildDiscoveryURL(clientId: clientId, clientVersion: clientVersion, domain: domain) as URL? else {
SFSDKCoreLogger.e(classForCoder, message: "Failed to construct discovery URL")
return
}
webview.load(URLRequest(url: url))
}

/// Handles a navigation action and checks if it matches the domain discovery callback URL.
///
/// - Parameter action: The action to inspect.
/// - Returns: A `DomainDiscoveryResult` if the callback URL is detected and parsed; otherwise, `nil`.
///
/// Call this from your WKNavigationDelegate when a navigation action occurs to detect and extract the result from the domain discovery callback URL.
@objc(handleWithWebAction:)
public func handle(action: WKNavigationAction) -> DomainDiscoveryResult? {
guard let url = action.request.url else {
return nil
}
if isDomainDiscoveryCallbackURL(url) {
let result = parseDiscoveryCallbackURL(url)
return result
}
return nil
}

@objc
public func isDiscoveryDomain(_ domain: String?, clientId: String?) -> Bool {
guard let domain = domain, let clientId = clientId else { return false }
let isDiscovery = domain.lowercased().contains(DomainDiscovery.URLComponent.path.rawValue)
let isSupportedClient = DomainDiscovery.supportedClientIds.contains(clientId)
if isDiscovery && !isSupportedClient {
SFSDKCoreLogger.e(classForCoder, message: "\(domain) is a discovery domain, but client ID '\(clientId)' is not supported.")
}
return isDiscovery && isSupportedClient
}
}

extension DomainDiscoveryCoordinator {
private static func buildDiscoveryURL(clientId: String, clientVersion: String, domain: String, callbackURL: String = DomainDiscovery.callbackURL.rawValue) -> NSURL? {
var components = URLComponents()
components.scheme = DomainDiscovery.URLComponent.scheme.rawValue
components.host = domain.components(separatedBy: "/").first
components.path = DomainDiscovery.URLComponent.path.rawValue
components.queryItems = [
URLQueryItem(name: DomainDiscovery.URLComponent.QueryItem.clientID.rawValue, value: clientId),
URLQueryItem(name: DomainDiscovery.URLComponent.QueryItem.clientVersion.rawValue, value: clientVersion),
URLQueryItem(name: DomainDiscovery.URLComponent.QueryItem.callbackURL.rawValue, value: callbackURL)
]
return components.url as NSURL?
}

private func isDomainDiscoveryCallbackURL(_ url: URL?) -> Bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That will not work with test environment.
@JohnsonEricAtSalesforce took a different approach and is checking for /discovery in the path of the login server. See https://github.com/forcedotcom/SalesforceMobileSDK-Android/pull/2736/files#diff-25c822f0f7f00bf2d6a9fec0477b811f0264e0c9a22312378ba605ccfa194c17R1427

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@wmathurin do you mean the "isDisoveryDomain" method, not the "isDomainDiscoveryCallbackURL" right? I'm pretty sure that's what you mean there. But want to make sure I'm not missing something.

guard let url = url as URL? else { return false }
return url.absoluteString.lowercased().hasPrefix(DomainDiscovery.callbackURL.rawValue.lowercased())
}

private func parseDiscoveryCallbackURL(_ url: URL?) -> DomainDiscoveryResult? {
guard let url = url else { return nil }
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let loginHint = components?.queryItems?.first(where: { $0.name == "login_hint" })?.value,
let myDomainRaw = components?.queryItems?.first(where: { $0.name == "my_domain" })?.value else {
SFSDKCoreLogger.e(classForCoder, message: "Domain discovery callback URL is missing required parameter(s): login_hint and/or my_domain.")
return nil
}
let myDomain = myDomainRaw.hasPrefix("https://") ? String(myDomainRaw.dropFirst("https://".count)) : myDomainRaw
return DomainDiscoveryResult(loginHint: loginHint, myDomain: myDomain)
}
}
Loading
Loading