-
Notifications
You must be signed in to change notification settings - Fork 424
@W-17964653 Discovery endpoint to postpone OAuth flow #3892
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
a7281f7
6c86e77
4686e83
5d38d6a
a2f14b2
7270da6
133e32d
bfd6456
a52b773
3117d97
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,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> = [ | ||
|
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 { | ||
|
Contributor
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. That will not work with test environment.
Contributor
Author
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 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) | ||
| } | ||
| } | ||
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 This what you hand in mind for the disco comain?