Skip to content
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

@W-16362973: [iOS] Add QR Code Login Support in MSDK #3759

Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ NS_SWIFT_NAME(SalesforceManager)
*/
@property (nonatomic, assign) BOOL isLoginWebviewInspectable;

/*** Indicates if login via QR Code and UI bridge API is enabled */
@property (nonatomic, assign) BOOL isQrCodeLoginEnabled;
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved

/** The type of cache used for the shared URL cache, defaults to kSFURLCacheTypeEncrypted.
*/
@property (nonatomic, assign) SFURLCacheType URLCacheType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//
// SFLoginViewController+QrCodeLogin.swift
// SalesforceSDKCore
//
// Created by Eric Johnson on 9/5/24.
// Copyright (c) 2024-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.

import Foundation

public extension SalesforceLoginViewController {
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved

// 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?
) {
guard let frontdoorBridgeUrl = URL(string: frontdoorBridgeUrlString) else { return }

// Stop current authentication attempt, if applicable, before starting the new one.
UserAccountManager.shared.stopCurrentAuthentication { result in

DispatchQueue.main.async {
// Login using front door bridge URL and PKCE code verifier provided by the QR code.
AuthHelper.loginIfRequired(nil,
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
frontDoorBridgeUrl: frontdoorBridgeUrl,
codeVerifier: pkceCodeVerifier) {
}
}
}
}

// MARK: - QR Code Login Via UI Bridge API Private Implementation

/**
* 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.
*
* 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 decode UI bridge API parameters due to a decoding 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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
/** Specify the font to use for navigation bar header text.*/
@property (nonatomic, strong, nullable) UIFont * navBarFont NS_SWIFT_NAME(navigationBarFont);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ - (SFSDKLoginHostListViewController *)loginHostListViewController {
return _loginHostListViewController;
}

#pragma mark - Properties`
#pragma mark - Properties

- (void)setOauthView:(UIView *)oauthView {
if (![oauthView isEqual:_oauthView]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ NS_SWIFT_NAME(SalesforceLoginViewControllerConfig)
/** Specifiy a delegate for LoginViewController. */
@property (nonatomic, weak, nullable) id<SFLoginViewControllerDelegate> delegate;

@property (nonatomic, copy, nullable) SFLoginViewControllerCreationBlock loginViewControllerCreationBlock;
@property (nonatomic, copy, nullable) SFLoginViewControllerCreationBlock loginViewControllerCreationBlock;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong ,nullable) SFOAuthCredentials *spAppCredentials;
@property (nonatomic, weak, nullable) SFSDKAuthSession *authSession;

/// For Salesforce Identity UI Bridge API support, an overriding front door bridge URL to use in place of the default initial URL.
@property (nonatomic, strong, nullable) NSURL *overrideWithFrontDoorBridgeUrl;

/// For Salesforce Identity UI Bridge API support, the optional web server flow code verififer accompaning the front door bridge URL. This can only be used with `overrideWithfrontDoorBridgeUrl`.
@property (nonatomic, strong, nullable) NSString *overrideWithCodeVerifier;

- (instancetype)initWithAuthSession:(SFSDKAuthSession *)authSession;

/** UpdateCredentials and record changes to instanceUrl,accessToken,communityId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ - (void)notifyDelegateOfFailure:(NSError*)error authInfo:(SFOAuthInfo *)info
});
}
self.authInfo = nil;
[self resetFrontDoorBridgeUrl];
}

- (void)notifyDelegateOfSuccess:(SFOAuthInfo *)authInfo
Expand All @@ -353,6 +354,7 @@ - (void)notifyDelegateOfSuccess:(SFOAuthInfo *)authInfo
[self.delegate oauthCoordinatorDidAuthenticate:self authInfo:authInfo];
}
self.authInfo = nil;
[self resetFrontDoorBridgeUrl];
}

- (void)notifyDelegateOfBeginAuthentication
Expand Down Expand Up @@ -547,7 +549,13 @@ - (void)loadWebViewWithUrlString:(NSString *)urlString cookie:(BOOL)enableCookie
[request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; // don't use cache
[SFSDKCoreLogger d:[self class] format:@"%@ Loading web view for '%@' auth flow, with URL: %@", NSStringFromSelector(_cmd), self.authInfo.authTypeDescription, [urlToLoad sfsdk_redactedAbsoluteString:@[ @"sid" ]]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.view loadRequest:request];
// If an overriding Salesforce Identity API UI Bridge front door bridge is present, load it.
if (self.overrideWithFrontDoorBridgeUrl) {
[self.view loadRequest:[NSURLRequest requestWithURL:self.overrideWithFrontDoorBridgeUrl]];

} else {
[self.view loadRequest:request];
}
});
}
- (void)updateCredentials:(NSDictionary *) params {
Expand All @@ -568,8 +576,8 @@ - (void)beginTokenEndpointFlow {
if (self.approvalCode) {
[SFSDKCoreLogger i:[self class] format:@"%@: Initiating authorization code flow.", NSStringFromSelector(_cmd)];
request.approvalCode = self.approvalCode;
request.codeVerifier = self.codeVerifier;

// Choose either the default generated code verifier or the code verifier matching the overriding Salesforce Identity API UI Bridge front door bridge.
request.codeVerifier = self.overrideWithCodeVerifier ? self.overrideWithCodeVerifier : self.codeVerifier;
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
[self.authClient accessTokenForApprovalCode:request completion:^(SFSDKOAuthTokenEndpointResponse * response) {
__strong typeof (weakSelf) strongSelf = weakSelf;
[strongSelf handleResponse:response];
Expand Down Expand Up @@ -750,8 +758,8 @@ - (NSString *)approvalURLForEndpoint:(NSString *)authorizeEndpoint

if (!codeChallenge) {
// Code verifier challenge:
// - self.codeVerifier is a base64url-encoded random data string
// - The code challenge sent here is an SHA-256 hash of self.codeVerifier, also base64url-encoded
// - self.codeVerifier is a Base64 URL-Safe encoded (Note, not URL encoded) random data string
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
// - The code challenge sent here is an SHA-256 hash of self.codeVerifier, also Base64 URL-Safe encoded
// - Later, self.codeVerifier will be sent to the service, to be used to compare against the initial code challenge sent here.
self.codeVerifier = [[SFSDKCryptoUtils randomByteDataWithLength:kSFOAuthCodeVerifierByteLength] sfsdk_base64UrlString];
codeChallenge = [[[self.codeVerifier dataUsingEncoding:NSUTF8StringEncoding] sfsdk_sha256Data] sfsdk_base64UrlString];
Expand All @@ -770,6 +778,15 @@ - (NSString *)approvalURLForEndpoint:(NSString *)authorizeEndpoint
return approvalUrlString;
}

/**
* Resets all state related to Salesforce Identity API UI Bridge front door bridge URL log in to its default
* inactive state.
*/
-(void) resetFrontDoorBridgeUrl {
self.overrideWithFrontDoorBridgeUrl = nil;
self.overrideWithCodeVerifier = nil;
}

- (NSString *)scopeQueryParamString {
NSMutableSet *scopes = (self.scopes.count > 0 ? [NSMutableSet setWithSet:self.scopes] : [NSMutableSet set]);
[scopes addObject:kSFOAuthRefreshToken];
Expand All @@ -791,10 +808,12 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati
NSURL *url = navigationAction.request.URL;
NSString *requestUrl = [url absoluteString];
if ([self isRedirectURL:requestUrl]) {
if ([[SalesforceSDKManager sharedManager] useWebServerAuthentication]) {
[self handleWebServerResponse:url];
// Determine if presence of override parameters require the user agent flow.
BOOL overrideWithUserAgentFlow = self.overrideWithFrontDoorBridgeUrl && !self.overrideWithCodeVerifier;
if ( [[SalesforceSDKManager sharedManager] useWebServerAuthentication] && !overrideWithUserAgentFlow) {
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
[self handleWebServerResponse:url]; // Web server flow/URLs with query string parameters.
} else {
[self handleUserAgentResponse:url];
[self handleUserAgentResponse:url]; // User agent flow/URLs with the fragment component.
}
decisionHandler(WKNavigationActionPolicyCancel);
} else if ([self isSPAppRedirectURL:requestUrl]){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,24 @@ Set this block to handle presentation of the Authentication View Controller.

- (BOOL)authenticateUsingIDP:(SFSDKAuthRequest *)request completion:(SFUserAccountManagerSuccessCallbackBlock)completionBlock failure:(SFUserAccountManagerFailureCallbackBlock)failureBlock;

- (BOOL)authenticateWithRequest:(SFSDKAuthRequest *)request completion:(SFUserAccountManagerSuccessCallbackBlock)completionBlock failure:(SFUserAccountManagerFailureCallbackBlock)failureBlock;
- (BOOL)authenticateWithRequest:(SFSDKAuthRequest *)request
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
completion:(SFUserAccountManagerSuccessCallbackBlock)completionBlock
failure:(SFUserAccountManagerFailureCallbackBlock)failureBlock
frontDoorBridgeUrl:(nullable NSURL * )frontDoorBridgeUrl
codeVerifier:(nullable NSString *)codeVerifier;

- (SFSDKAuthRequest *)defaultAuthRequest;

- (BOOL)loginWithCompletion:(nullable SFUserAccountManagerSuccessCallbackBlock)completionBlock
failure:(nullable SFUserAccountManagerFailureCallbackBlock)failureBlock
scene:(nullable UIScene *)scene;

- (BOOL)loginWithCompletion:(nullable SFUserAccountManagerSuccessCallbackBlock)completionBlock
failure:(nullable SFUserAccountManagerFailureCallbackBlock)failureBlock
scene:(UIScene *)scene
frontDoorBridgeUrl:(nullable NSURL * )frontDoorBridgeUrl
codeVerifier:(nullable NSString *)codeVerifier;

@end

NS_ASSUME_NONNULL_END
Loading