From c220cd608a3068da364ad9a9c30cb8006ce5786c Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Mon, 16 Sep 2024 15:24:54 -0600 Subject: [PATCH] @W-16362973: [iOS] Add QR Code Login Support in MSDK (iOS P.O.C) --- .../SFLoginViewController+QrCodeLogin.swift | 156 ++++++++++++++++++ .../Classes/Login/SFLoginViewController.h | 3 + .../Classes/Login/SFLoginViewController.m | 2 +- .../Login/SFSDKLoginViewControllerConfig.h | 2 +- 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController+QrCodeLogin.swift diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController+QrCodeLogin.swift b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController+QrCodeLogin.swift new file mode 100644 index 0000000000..aac233386f --- /dev/null +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController+QrCodeLogin.swift @@ -0,0 +1,156 @@ +// +// SFLoginViewController+QrCodeLogin.swift +// SalesforceSDKCore +// +// Created by Eric Johnson on 9/5/24. +// + +import Foundation + +public extension SalesforceLoginViewController { + + // MARK: QR Code Login Via UI Bridge API Public Implementation + + /** + * Automatically log in with a UI Bridge API login QR code. + * - Parameters + * - loginQrCodeContent: The login QR code content. This should be either a URL or URL + * query containing the UI Bridge API JSON parameter. The UI Bridge API JSON parameter + * should contain URL-encoded JSON with two values: + * - frontdoor_bridge_url + * - pkce_code_verifier + * If pkce_code_verifier is not specified then the user agent flow is used + * - Returns: Boolean true if a log in attempt is possible using the provided QR code content, false + * otherwise + */ + func loginFromQrCode( + loginQrCodeContent: String? + ) -> Bool { + if let uiBridgeApiParameters = uiBridgeApiParametersFromLoginQrCodeContent( + loginQrCodeContent + ) { + loginWithFrontdoorBridgeUrl( + uiBridgeApiParameters.frontdoorBridgeUrl, + pkceCodeVerifier: uiBridgeApiParameters.pkceCodeVerifier + ) + return true + } else { + return false + } + } + + /** + * Automatically log in with a UI Bridge API front door bridge URL and PKCE code verifier. + * - Parameters + * - frontdoorBridgeUrl: The UI Bridge API front door bridge URL + * - pkceCodeVerifier: The PKCE code verifier + */ + func loginWithFrontdoorBridgeUrl( + _ frontdoorBridgeUrlString: String, + pkceCodeVerifier: String? + ) { + print("Login With Frontdoor Bridge URL: '\(frontdoorBridgeUrlString)'/'\(String(describing: pkceCodeVerifier))'.") + + // TODO: W-16171402: Integrate With Existing Login Logic And Resolve Use Of PKCE Code Verifier. ECJ20240912 + + guard let frontdoorBridgeUrl = URL(string: frontdoorBridgeUrlString) else { return } + guard let webView = oauthView as? WKWebView else { return } + webView.load(URLRequest(url: frontdoorBridgeUrl)) + } + + // MARK: QR Code Login Via UI Bridge API Private Implementation + +// /** +// * Determines if QR code login is enabled for the provided intent. +// * @param intent The intent to determine QR code login enablement for +// * @return Boolean true if QR code login is enabled for the the intent or +// * false otherwise +// */ +// private fun isDeepLinkedQrCodeLogin( +// intent: Intent +// ) = SalesforceSDKManager.getInstance().isQrCodeLoginEnabled +// && intent.data?.path?.contains(LOGIN_QR_PATH) == true +// + /** + * Parses UI Bridge API parameters from the provided login QR code content. + * - Parameters + * - loginQrCodeContent: The login QR code content string + * - UiBridgeApiParameters: The UI Bridge API parameters or null if the QR code content cannot + * provide them for any reason + */ + private func uiBridgeApiParametersFromLoginQrCodeContent( + _ loginQrCodeContent: String? + ) -> UiBridgeApiParameters? { + guard let loginQrCodeContentUnwrapped = loginQrCodeContent else { return nil } + guard let uiBridgeApiJson = uiBridgeApiJsonFromQrCodeContent(loginQrCodeContentUnwrapped) else { return nil } + return uiBridgeApiParametersFromUiBridgeApiJson(uiBridgeApiJson) + } + + /** + * Parses UI Bridge API parameters JSON from the provided string, which may be formatted to match + * either QR code content provided by app's QR code library or a custom app deep link from an external + * QR code reader. + * + * TODO: W-16171402: Verify this after developing template app's external QR code deep-link support. ECJ20240912 + * + * 1. From external QR reader: ?bridgeJson={...} + * 2. From the app's QR reader: ?bridgeJson=%7B...%7D + * + * - Parameters + * - qrCodeContent: The QR code content string + * - Returns: String: The UI Bridge API parameter JSON or null if the string cannot provide the + * JSON for any reason + */ + private func uiBridgeApiJsonFromQrCodeContent( + _ qrCodeContent: String + ) -> String? { + return try? NSRegularExpression( + pattern: "^.*\\?bridgeJson=").stringByReplacingMatches( + in: qrCodeContent, + range: NSRange( + location: 0, + length: qrCodeContent.utf16.count), + withTemplate: "").removingPercentEncoding + } + + /** + * Creates UI Bridge API parameters from the provided JSON string. + * - Parameters + * - uiBridgeApiParameterJsonString: The UI Bridge API parameters JSON string + * - Returns: The UI Bridge API parameters + */ + private func uiBridgeApiParametersFromUiBridgeApiJson( + _ uiBridgeApiParameterJsonString: String + ) -> UiBridgeApiParameters? { + guard let uiBridgeApiParameterJsonData = uiBridgeApiParameterJsonString.data( + using: .utf8 + ) else { return nil } + + do { return try JSONDecoder().decode( + UiBridgeApiParameters.self, + from: uiBridgeApiParameterJsonData) + } catch let error { + SFSDKCoreLogger().e( + classForCoder, + message: "Cannot JSON encode start password reset request body due to an encoding error with description '\(error.localizedDescription)'.") + return nil + } + } + + /** + * A struct representing UI Bridge API parameters provided by a login QR code. + */ + private struct UiBridgeApiParameters: Codable { + + /** The front door bridge URL provided by the login QR code */ + let frontdoorBridgeUrl: String + + /** The PKCE code verifier provided by the login QR code */ + let pkceCodeVerifier: String? + + enum CodingKeys: String, CodingKey { + case frontdoorBridgeUrl = "frontdoor_bridge_url" + case pkceCodeVerifier = "pkce_code_verifier" + } + } +} diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.h index 9cd9b5bc59..a279f638cc 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.h @@ -66,6 +66,9 @@ NS_SWIFT_NAME(SalesforceLoginViewController) */ @property (nonatomic, strong, nullable) IBOutlet UIView *oauthView; +/** The biometric log in button */ +@property (nonatomic, strong, readonly, nullable) UIButton *biometricButton; + /** Specify the font to use for navigation bar header text.*/ @property (nonatomic, strong, nullable) UIFont * navBarFont NS_SWIFT_NAME(navigationBarFont); diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.m index 81b572cd4f..2eca6a0876 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.m @@ -336,7 +336,7 @@ - (SFSDKLoginHostListViewController *)loginHostListViewController { return _loginHostListViewController; } -#pragma mark - Properties` +#pragma mark - Properties - (void)setOauthView:(UIView *)oauthView { if (![oauthView isEqual:_oauthView]) { diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFSDKLoginViewControllerConfig.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFSDKLoginViewControllerConfig.h index d5b7726485..524613b142 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFSDKLoginViewControllerConfig.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFSDKLoginViewControllerConfig.h @@ -53,6 +53,6 @@ NS_SWIFT_NAME(SalesforceLoginViewControllerConfig) /** Specifiy a delegate for LoginViewController. */ @property (nonatomic, weak, nullable) id delegate; -@property (nonatomic, copy, nullable) SFLoginViewControllerCreationBlock loginViewControllerCreationBlock; +@property (nonatomic, copy, nullable) SFLoginViewControllerCreationBlock loginViewControllerCreationBlock; @end