Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
#import "SalesforceSDKManager+Internal.h"
#import <LocalAuthentication/LocalAuthentication.h>
#import "SFRestAPI+Internal.h"
#import "SFOAuthCoordinator+Internal.h"
#import "SFSDKAuthSession.h"
#import "SFSDKAuthRequest.h"
#import <SalesforceSDKCore/SalesforceSDKCore-Swift.h>
@interface SFLoginViewController () <SFSDKLoginHostDelegate, SFUserAccountManagerDelegate>

Expand Down Expand Up @@ -323,14 +326,29 @@ - (UIBarButtonItem *)createSettingsButton {
}

// Login for Admin - forces browser-based (advanced) authentication to support phishing-resistant MFA.
[menuActions addObject:[UIAction actionWithTitle:[SFSDKResourceUtils localizedString:@"LOGIN_FOR_ADMIN"]
// Wrapped in an uncached UIDeferredMenuElement so the show/hide predicate is re-evaluated each
// time the menu opens. The entry is hidden during phase 1 of Welcome Discovery (i.e. before the

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.

So we are hiding the menu during phase 1, do we expect admin to look for that menu in phase 2 or do we expect that to configure the correct login server manually if they need to login as admin?

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.

Hide in phase 1 so we don't have to deal with the discovery callback and the experience can be the same per platform (easier to document).

When the customer selects the org they want either the My Domain is configured to trigger Adv Auth for everyone or it loads in the WebView and the option to "Login for Admin" is in the menu.

// user has selected an account on welcome.salesforce.com/discovery), where we have no resolved
// My Domain to launch the browser session against.
__weak typeof(self) weakSelf = self;
UIDeferredMenuElement *loginForAdminElement = [UIDeferredMenuElement elementWithUncachedProvider:^(void (^ _Nonnull completion)(NSArray<UIMenuElement *> * _Nonnull)) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (![SFLoginViewController shouldShowLoginForAdminForSession:[strongSelf currentAuthSessionForMenu]]) {
completion(@[]);
return;
}
UIAction *action = [UIAction actionWithTitle:[SFSDKResourceUtils localizedString:@"LOGIN_FOR_ADMIN"]
image:nil
identifier:nil
handler:^(__kindof UIAction* _Nonnull action) {
if ([self.delegate respondsToSelector:@selector(loginViewControllerDidSelectLoginForAdmin:)]) {
[self.delegate loginViewControllerDidSelectLoginForAdmin:self];
}
}]];
__strong typeof(weakSelf) handlerSelf = weakSelf;
if ([handlerSelf.delegate respondsToSelector:@selector(loginViewControllerDidSelectLoginForAdmin:)]) {
[handlerSelf.delegate loginViewControllerDidSelectLoginForAdmin:handlerSelf];
}
}];
completion(@[action]);
}];
[menuActions addObject:loginForAdminElement];

UIMenu *menu = [UIMenu menuWithTitle:@"" // No title
children:menuActions];
Expand All @@ -340,6 +358,32 @@ - (UIBarButtonItem *)createSettingsButton {
return settingsButton;
}

- (nullable SFSDKAuthSession *)currentAuthSessionForMenu {
NSString *sceneId = self.view.window.windowScene.session.persistentIdentifier;
if (sceneId.length == 0) {
return nil;
}
return [[SFUserAccountManager sharedInstance].authSessions objectForKey:sceneId];
}

+ (BOOL)shouldShowLoginForAdminForSession:(nullable SFSDKAuthSession *)session {
// Default to showing the entry when we have no session/coordinator info.
// This matches the previous (always-shown) behavior on non-discovery flows
// and during early SDK lifecycle before the auth session is wired up.
SFOAuthCoordinator *coordinator = session.oauthCoordinator;
if (session == nil || coordinator == nil) {
return YES;
}

NSString *loginHost = session.oauthRequest.loginHost;
BOOL isDiscoveryHost = [SFDomainDiscoveryCoordinator isDiscoveryDomain:loginHost];
// Hide only when we are mid-discovery (no My Domain selected yet).
if (isDiscoveryHost && !coordinator.domainUpdated) {
return NO;
}
return YES;
}

- (UIView *)createTitleItem {
NSString *title = [SFSDKResourceUtils localizedString:@"TITLE_LOGIN"];
// Setup top item.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,18 @@ public class DomainDiscoveryCoordinator: NSObject {
@objc
@available(*, deprecated, renamed: "isDiscoveryDomain(domain:)")
public func isDiscoveryDomain(_ domain: String?, clientId: String?) -> Bool {
return isDiscoveryDomain(domain)
return Self.isDiscoveryDomain(domain)
}

@objc
@available(*, deprecated, message: "Deprecated in Salesforce Mobile SDK 14.0 and will be removed in 15.0. Use the class method DomainDiscoveryCoordinator.isDiscoveryDomain(_:) instead; the check is stateless and needs no instance.")
public func isDiscoveryDomain(_ domain: String?) -> Bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like the class one! Could we deprecate this one too?

return Self.isDiscoveryDomain(domain)
}

/// Whether the given login host is a My Domain discovery host (e.g.`welcome.salesforce.com/discovery`).
@objc
public class func isDiscoveryDomain(_ domain: String?) -> Bool {
guard let domain = domain else { return false }
let isDiscovery = domain.lowercased().contains(DomainDiscovery.URLComponent.path.rawValue)
return isDiscovery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ - (void)authenticate {

- (void)authenticateWithCredentials:(SFOAuthCredentials *)credentials {
self.credentials = credentials;
if ([self.domainDiscoveryCoordinator isDiscoveryDomain:self.credentials.domain]) {
if ([SFDomainDiscoveryCoordinator isDiscoveryDomain:self.credentials.domain]) {
[SFSDKAppFeatureMarkers registerAppFeature:kSFAppFeatureWelcomeDiscovery];
[self runMyDomainDiscoveryAndAuthenticate];
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ NS_ASSUME_NONNULL_BEGIN
/// When YES, cancelling the browser session returns to the WebView login instead of showing the server picker.
@property (nonatomic, assign) BOOL loginAsAdmin;

/// Login-for-Admin override: the My Domain to authenticate against, set when
/// LFA is invoked from phase 2 of Welcome Discovery. Consulted only while
/// `loginAsAdmin == YES`; the request's `loginHost` is left unchanged so that
/// other settings actions (Reload, Clear Cache) and the post-cancel restart
/// continue to operate against the originally configured login host.
/// Cleared together with `loginAsAdmin` when the LFA browser session is cancelled.
@property (nonatomic, copy, nullable) NSString *loginAsAdminMyDomain;

/// Login-for-Admin override: the login_hint OAuth parameter to pass to the
/// browser session. Same scoping rules as `loginAsAdminMyDomain`.
@property (nonatomic, copy, nullable) NSString *loginAsAdminLoginHint;

@property (nonatomic, strong) NSArray<NSString *> *additionalOAuthParameterKeys;
@property (nonatomic, strong) NSDictionary<NSString *,id> * additionalTokenRefreshParams;
@property (nonatomic, copy) NSString *loginHost;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,12 @@ Use this method to stop/clear any authentication which is has already been start
2. Walk the key window's view hierarchy and locate the `SFLoginViewController` presented
inside the SDK's navigation controller.
@note Welcome Discovery: when the active login host is a Welcome Discovery host
(e.g. `welcome.salesforce.com/discovery`) and the user has not yet selected an
account, "Login for Admin" is a no-op (there is no resolved My Domain to switch
to). After the user picks an account on the discovery page, this method may be
called again and will use the resolved My Domain for the browser session.
@param loginViewController The login view controller whose scene's active auth session should
switch to "Login for Admin". Its window's scene is used to locate the session.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,26 +608,38 @@ - (BOOL)authenticateWithRequest:(SFSDKAuthRequest *)request
initWithFrontdoorBridgeUrl:frontDoorBridgeUrl
codeVerifier:codeVerifier];
}
authSession.oauthCoordinator.loginHint = loginHint;
// Login for Admin: when the request carries a My Domain override (set by
// loginViewControllerDidSelectLoginForAdmin: in phase-2 Welcome Discovery),
// route the browser session to the resolved My Domain and forward the
// captured login hint, while leaving request.loginHost — and therefore
// every other restart path — pointed at the originally configured host.
BOOL useLfaOverride = request.loginAsAdmin && request.loginAsAdminMyDomain.length > 0;
if (useLfaOverride) {
authSession.credentials.domain = request.loginAsAdminMyDomain;
authSession.oauthCoordinator.loginHint = request.loginAsAdminLoginHint;
} else {
authSession.oauthCoordinator.loginHint = loginHint;
}
NSString *appConfigLoginHost = useLfaOverride ? request.loginAsAdminMyDomain : request.loginHost;
NSString *sceneId = authSession.sceneId;
self.authSessions[sceneId] = authSession;

if (self.nativeLoginEnabled && !self.shouldFallbackToWebAuthentication) {
authSession.oauthCoordinator.useNativeAuth = YES;
}

dispatch_async(dispatch_get_main_queue(), ^{
[SFSDKWebViewStateManager removeSessionForcefullyWithCompletionHandler:^{
// Get app config for the login host. If appConfigRuntimeSelectorBlock is set,
// it will be invoked to select the appropriate config. Otherwise, returns the default appConfig.
[[SalesforceSDKManager sharedManager] appConfigForLoginHost:request.loginHost callback:^(SFSDKAppConfig* appConfig) {
[[SalesforceSDKManager sharedManager] appConfigForLoginHost:appConfigLoginHost callback:^(SFSDKAppConfig* appConfig) {
authSession.credentials.clientId = appConfig.remoteAccessConsumerKey;
authSession.credentials.redirectUri = appConfig.oauthRedirectURI;
authSession.credentials.scopes = [appConfig.oauthScopes allObjects];
[authSession.oauthCoordinator authenticateWithCredentials:authSession.credentials];
}];
}];

});
return self.authSessions[sceneId].isAuthenticating;
}
Expand Down Expand Up @@ -702,6 +714,9 @@ - (void)restartAuthentication:(SFSDKAuthSession *)session {
[self dismissAuthViewControllerIfPresentForScene:scene completion:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.authSessions[scene.session.persistentIdentifier].isAuthenticating = NO;
// LFA passes its hint via the request's loginAsAdminLoginHint override
// (consulted in authenticateWithRequest:); other restart paths intentionally
// pass nil so a hint set on a prior session does not bleed across server changes.
[strongSelf authenticateWithRequest:session.oauthRequest
loginHint:nil
completion:session.authSuccessCallback
Expand Down Expand Up @@ -1032,9 +1047,14 @@ - (void)oauthCoordinatorDidCancelBrowserAuthentication:(SFOAuthCoordinator *)coo
}

// When "Login for Admin" initiated the browser auth, clear the flag and
// restart the WebView login flow instead of showing the server picker.
// its My Domain / login hint overrides, then restart the WebView login
// flow against the originally configured host instead of showing the
// server picker. For Welcome Discovery, this means the user lands back
// on the discovery page and re-picks an account.
if (coordinator.authSession.oauthRequest.loginAsAdmin) {
coordinator.authSession.oauthRequest.loginAsAdmin = NO;
coordinator.authSession.oauthRequest.loginAsAdminMyDomain = nil;
coordinator.authSession.oauthRequest.loginAsAdminLoginHint = nil;
[self restartAuthentication:coordinator.authSession];
return;
}
Expand Down Expand Up @@ -1125,6 +1145,25 @@ - (void)loginViewControllerDidReload:(SFLoginViewController *)loginViewControlle
- (void)loginViewControllerDidSelectLoginForAdmin:(SFLoginViewController *)loginViewController {
NSString *sceneId = loginViewController.view.window.windowScene.session.persistentIdentifier;
SFSDKAuthSession *session = self.authSessions[sceneId];
SFOAuthCoordinator *coordinator = session.oauthCoordinator;

// Phase-1 Welcome Discovery: a discovery host is loaded but the user has not
// yet picked an account, so credentials.domain is still the discovery host
// and we have no My Domain to advance to. Switching to ASWebAuthenticationSession
// here would launch the browser against welcome.salesforce.com — wrong UX.
// No-op until phase 2 lands.
if ([SFDomainDiscoveryCoordinator isDiscoveryDomain:session.oauthRequest.loginHost] && !coordinator.domainUpdated) {
[SFSDKCoreLogger w:[self class] format:@"%@: Login for Admin is not available before a My Domain has been selected on the Welcome Discovery page; ignoring.", NSStringFromSelector(_cmd)];
return;
}

// Phase-2 Welcome Discovery (or a non-discovery host): record the resolved
// My Domain and login hint as LFA-scoped overrides on the request. The
// request's loginHost is left untouched so that Reload / Clear Cache /
// post-cancel restart continue to use the originally configured host.
// These overrides are in-memory only and are cleared on LFA cancel.
session.oauthRequest.loginAsAdminMyDomain = coordinator.credentials.domain.length > 0 ? coordinator.credentials.domain : nil;
session.oauthRequest.loginAsAdminLoginHint = coordinator.loginHint.length > 0 ? coordinator.loginHint : nil;
session.oauthRequest.loginAsAdmin = YES;
[self restartAuthenticationForViewController:loginViewController];
}
Expand Down Expand Up @@ -1710,7 +1749,7 @@ - (void)setCurrentUserInternal:(SFUserAccount*)user {
// next login is web based it should not try to use that url.
// Also skip if the app uses a Welcome/Discovery domain — persisting the My Domain
// would pollute the server picker and prevent returning to the discovery page on logout.
BOOL isDiscoveryLogin = [[[SFDomainDiscoveryCoordinator alloc] init] isDiscoveryDomain:self.loginHost];
BOOL isDiscoveryLogin = [SFDomainDiscoveryCoordinator isDiscoveryDomain:self.loginHost];
if (user.credentials.domain && !isNativeLogin && !isDiscoveryLogin)
self.loginHost = user.credentials.domain;
[self didChangeValueForKey:@"currentUser"];
Expand Down
Loading
Loading