Skip to content

Commit 29439f0

Browse files
authored
[iOS] Fix Login for Admin and Welcome Discovery incompatibility (#4078)
Fix Login for Admin and Welcome Discovery incompatibility.
1 parent 7b62013 commit 29439f0

9 files changed

Lines changed: 420 additions & 16 deletions

File tree

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Login/SFLoginViewController.m

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
#import "SalesforceSDKManager+Internal.h"
4343
#import <LocalAuthentication/LocalAuthentication.h>
4444
#import "SFRestAPI+Internal.h"
45+
#import "SFOAuthCoordinator+Internal.h"
46+
#import "SFSDKAuthSession.h"
47+
#import "SFSDKAuthRequest.h"
4548
#import <SalesforceSDKCore/SalesforceSDKCore-Swift.h>
4649
@interface SFLoginViewController () <SFSDKLoginHostDelegate, SFUserAccountManagerDelegate>
4750

@@ -323,14 +326,29 @@ - (UIBarButtonItem *)createSettingsButton {
323326
}
324327

325328
// Login for Admin - forces browser-based (advanced) authentication to support phishing-resistant MFA.
326-
[menuActions addObject:[UIAction actionWithTitle:[SFSDKResourceUtils localizedString:@"LOGIN_FOR_ADMIN"]
329+
// Wrapped in an uncached UIDeferredMenuElement so the show/hide predicate is re-evaluated each
330+
// time the menu opens. The entry is hidden during phase 1 of Welcome Discovery (i.e. before the
331+
// user has selected an account on welcome.salesforce.com/discovery), where we have no resolved
332+
// My Domain to launch the browser session against.
333+
__weak typeof(self) weakSelf = self;
334+
UIDeferredMenuElement *loginForAdminElement = [UIDeferredMenuElement elementWithUncachedProvider:^(void (^ _Nonnull completion)(NSArray<UIMenuElement *> * _Nonnull)) {
335+
__strong typeof(weakSelf) strongSelf = weakSelf;
336+
if (![SFLoginViewController shouldShowLoginForAdminForSession:[strongSelf currentAuthSessionForMenu]]) {
337+
completion(@[]);
338+
return;
339+
}
340+
UIAction *action = [UIAction actionWithTitle:[SFSDKResourceUtils localizedString:@"LOGIN_FOR_ADMIN"]
327341
image:nil
328342
identifier:nil
329343
handler:^(__kindof UIAction* _Nonnull action) {
330-
if ([self.delegate respondsToSelector:@selector(loginViewControllerDidSelectLoginForAdmin:)]) {
331-
[self.delegate loginViewControllerDidSelectLoginForAdmin:self];
332-
}
333-
}]];
344+
__strong typeof(weakSelf) handlerSelf = weakSelf;
345+
if ([handlerSelf.delegate respondsToSelector:@selector(loginViewControllerDidSelectLoginForAdmin:)]) {
346+
[handlerSelf.delegate loginViewControllerDidSelectLoginForAdmin:handlerSelf];
347+
}
348+
}];
349+
completion(@[action]);
350+
}];
351+
[menuActions addObject:loginForAdminElement];
334352

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

361+
- (nullable SFSDKAuthSession *)currentAuthSessionForMenu {
362+
NSString *sceneId = self.view.window.windowScene.session.persistentIdentifier;
363+
if (sceneId.length == 0) {
364+
return nil;
365+
}
366+
return [[SFUserAccountManager sharedInstance].authSessions objectForKey:sceneId];
367+
}
368+
369+
+ (BOOL)shouldShowLoginForAdminForSession:(nullable SFSDKAuthSession *)session {
370+
// Default to showing the entry when we have no session/coordinator info.
371+
// This matches the previous (always-shown) behavior on non-discovery flows
372+
// and during early SDK lifecycle before the auth session is wired up.
373+
SFOAuthCoordinator *coordinator = session.oauthCoordinator;
374+
if (session == nil || coordinator == nil) {
375+
return YES;
376+
}
377+
378+
NSString *loginHost = session.oauthRequest.loginHost;
379+
BOOL isDiscoveryHost = [SFDomainDiscoveryCoordinator isDiscoveryDomain:loginHost];
380+
// Hide only when we are mid-discovery (no My Domain selected yet).
381+
if (isDiscoveryHost && !coordinator.domainUpdated) {
382+
return NO;
383+
}
384+
return YES;
385+
}
386+
343387
- (UIView *)createTitleItem {
344388
NSString *title = [SFSDKResourceUtils localizedString:@"TITLE_LOGIN"];
345389
// Setup top item.

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/DomainDiscoveryCoordinator.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,18 @@ public class DomainDiscoveryCoordinator: NSObject {
9595
@objc
9696
@available(*, deprecated, renamed: "isDiscoveryDomain(domain:)")
9797
public func isDiscoveryDomain(_ domain: String?, clientId: String?) -> Bool {
98-
return isDiscoveryDomain(domain)
98+
return Self.isDiscoveryDomain(domain)
9999
}
100-
100+
101101
@objc
102+
@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.")
102103
public func isDiscoveryDomain(_ domain: String?) -> Bool {
104+
return Self.isDiscoveryDomain(domain)
105+
}
106+
107+
/// Whether the given login host is a My Domain discovery host (e.g.`welcome.salesforce.com/discovery`).
108+
@objc
109+
public class func isDiscoveryDomain(_ domain: String?) -> Bool {
103110
guard let domain = domain else { return false }
104111
let isDiscovery = domain.lowercased().contains(DomainDiscovery.URLComponent.path.rawValue)
105112
return isDiscovery

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ - (void)authenticate {
216216

217217
- (void)authenticateWithCredentials:(SFOAuthCredentials *)credentials {
218218
self.credentials = credentials;
219-
if ([self.domainDiscoveryCoordinator isDiscoveryDomain:self.credentials.domain]) {
219+
if ([SFDomainDiscoveryCoordinator isDiscoveryDomain:self.credentials.domain]) {
220220
[SFSDKAppFeatureMarkers registerAppFeature:kSFAppFeatureWelcomeDiscovery];
221221
[self runMyDomainDiscoveryAndAuthenticate];
222222
return;

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFSDKAuthRequest.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ NS_ASSUME_NONNULL_BEGIN
3737
/// When YES, cancelling the browser session returns to the WebView login instead of showing the server picker.
3838
@property (nonatomic, assign) BOOL loginAsAdmin;
3939

40+
/// Login-for-Admin override: the My Domain to authenticate against, set when
41+
/// LFA is invoked from phase 2 of Welcome Discovery. Consulted only while
42+
/// `loginAsAdmin == YES`; the request's `loginHost` is left unchanged so that
43+
/// other settings actions (Reload, Clear Cache) and the post-cancel restart
44+
/// continue to operate against the originally configured login host.
45+
/// Cleared together with `loginAsAdmin` when the LFA browser session is cancelled.
46+
@property (nonatomic, copy, nullable) NSString *loginAsAdminMyDomain;
47+
48+
/// Login-for-Admin override: the login_hint OAuth parameter to pass to the
49+
/// browser session. Same scoping rules as `loginAsAdminMyDomain`.
50+
@property (nonatomic, copy, nullable) NSString *loginAsAdminLoginHint;
51+
4052
@property (nonatomic, strong) NSArray<NSString *> *additionalOAuthParameterKeys;
4153
@property (nonatomic, strong) NSDictionary<NSString *,id> * additionalTokenRefreshParams;
4254
@property (nonatomic, copy) NSString *loginHost;

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,12 @@ Use this method to stop/clear any authentication which is has already been start
651651
2. Walk the key window's view hierarchy and locate the `SFLoginViewController` presented
652652
inside the SDK's navigation controller.
653653
654+
@note Welcome Discovery: when the active login host is a Welcome Discovery host
655+
(e.g. `welcome.salesforce.com/discovery`) and the user has not yet selected an
656+
account, "Login for Admin" is a no-op (there is no resolved My Domain to switch
657+
to). After the user picks an account on the discovery page, this method may be
658+
called again and will use the resolved My Domain for the browser session.
659+
654660
@param loginViewController The login view controller whose scene's active auth session should
655661
switch to "Login for Admin". Its window's scene is used to locate the session.
656662
*/

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -608,26 +608,38 @@ - (BOOL)authenticateWithRequest:(SFSDKAuthRequest *)request
608608
initWithFrontdoorBridgeUrl:frontDoorBridgeUrl
609609
codeVerifier:codeVerifier];
610610
}
611-
authSession.oauthCoordinator.loginHint = loginHint;
611+
// Login for Admin: when the request carries a My Domain override (set by
612+
// loginViewControllerDidSelectLoginForAdmin: in phase-2 Welcome Discovery),
613+
// route the browser session to the resolved My Domain and forward the
614+
// captured login hint, while leaving request.loginHost — and therefore
615+
// every other restart path — pointed at the originally configured host.
616+
BOOL useLfaOverride = request.loginAsAdmin && request.loginAsAdminMyDomain.length > 0;
617+
if (useLfaOverride) {
618+
authSession.credentials.domain = request.loginAsAdminMyDomain;
619+
authSession.oauthCoordinator.loginHint = request.loginAsAdminLoginHint;
620+
} else {
621+
authSession.oauthCoordinator.loginHint = loginHint;
622+
}
623+
NSString *appConfigLoginHost = useLfaOverride ? request.loginAsAdminMyDomain : request.loginHost;
612624
NSString *sceneId = authSession.sceneId;
613625
self.authSessions[sceneId] = authSession;
614-
626+
615627
if (self.nativeLoginEnabled && !self.shouldFallbackToWebAuthentication) {
616628
authSession.oauthCoordinator.useNativeAuth = YES;
617629
}
618-
630+
619631
dispatch_async(dispatch_get_main_queue(), ^{
620632
[SFSDKWebViewStateManager removeSessionForcefullyWithCompletionHandler:^{
621633
// Get app config for the login host. If appConfigRuntimeSelectorBlock is set,
622634
// it will be invoked to select the appropriate config. Otherwise, returns the default appConfig.
623-
[[SalesforceSDKManager sharedManager] appConfigForLoginHost:request.loginHost callback:^(SFSDKAppConfig* appConfig) {
635+
[[SalesforceSDKManager sharedManager] appConfigForLoginHost:appConfigLoginHost callback:^(SFSDKAppConfig* appConfig) {
624636
authSession.credentials.clientId = appConfig.remoteAccessConsumerKey;
625637
authSession.credentials.redirectUri = appConfig.oauthRedirectURI;
626638
authSession.credentials.scopes = [appConfig.oauthScopes allObjects];
627639
[authSession.oauthCoordinator authenticateWithCredentials:authSession.credentials];
628640
}];
629641
}];
630-
642+
631643
});
632644
return self.authSessions[sceneId].isAuthenticating;
633645
}
@@ -702,6 +714,9 @@ - (void)restartAuthentication:(SFSDKAuthSession *)session {
702714
[self dismissAuthViewControllerIfPresentForScene:scene completion:^{
703715
__strong typeof(weakSelf) strongSelf = weakSelf;
704716
strongSelf.authSessions[scene.session.persistentIdentifier].isAuthenticating = NO;
717+
// LFA passes its hint via the request's loginAsAdminLoginHint override
718+
// (consulted in authenticateWithRequest:); other restart paths intentionally
719+
// pass nil so a hint set on a prior session does not bleed across server changes.
705720
[strongSelf authenticateWithRequest:session.oauthRequest
706721
loginHint:nil
707722
completion:session.authSuccessCallback
@@ -1032,9 +1047,14 @@ - (void)oauthCoordinatorDidCancelBrowserAuthentication:(SFOAuthCoordinator *)coo
10321047
}
10331048

10341049
// When "Login for Admin" initiated the browser auth, clear the flag and
1035-
// restart the WebView login flow instead of showing the server picker.
1050+
// its My Domain / login hint overrides, then restart the WebView login
1051+
// flow against the originally configured host instead of showing the
1052+
// server picker. For Welcome Discovery, this means the user lands back
1053+
// on the discovery page and re-picks an account.
10361054
if (coordinator.authSession.oauthRequest.loginAsAdmin) {
10371055
coordinator.authSession.oauthRequest.loginAsAdmin = NO;
1056+
coordinator.authSession.oauthRequest.loginAsAdminMyDomain = nil;
1057+
coordinator.authSession.oauthRequest.loginAsAdminLoginHint = nil;
10381058
[self restartAuthentication:coordinator.authSession];
10391059
return;
10401060
}
@@ -1125,6 +1145,25 @@ - (void)loginViewControllerDidReload:(SFLoginViewController *)loginViewControlle
11251145
- (void)loginViewControllerDidSelectLoginForAdmin:(SFLoginViewController *)loginViewController {
11261146
NSString *sceneId = loginViewController.view.window.windowScene.session.persistentIdentifier;
11271147
SFSDKAuthSession *session = self.authSessions[sceneId];
1148+
SFOAuthCoordinator *coordinator = session.oauthCoordinator;
1149+
1150+
// Phase-1 Welcome Discovery: a discovery host is loaded but the user has not
1151+
// yet picked an account, so credentials.domain is still the discovery host
1152+
// and we have no My Domain to advance to. Switching to ASWebAuthenticationSession
1153+
// here would launch the browser against welcome.salesforce.com — wrong UX.
1154+
// No-op until phase 2 lands.
1155+
if ([SFDomainDiscoveryCoordinator isDiscoveryDomain:session.oauthRequest.loginHost] && !coordinator.domainUpdated) {
1156+
[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)];
1157+
return;
1158+
}
1159+
1160+
// Phase-2 Welcome Discovery (or a non-discovery host): record the resolved
1161+
// My Domain and login hint as LFA-scoped overrides on the request. The
1162+
// request's loginHost is left untouched so that Reload / Clear Cache /
1163+
// post-cancel restart continue to use the originally configured host.
1164+
// These overrides are in-memory only and are cleared on LFA cancel.
1165+
session.oauthRequest.loginAsAdminMyDomain = coordinator.credentials.domain.length > 0 ? coordinator.credentials.domain : nil;
1166+
session.oauthRequest.loginAsAdminLoginHint = coordinator.loginHint.length > 0 ? coordinator.loginHint : nil;
11281167
session.oauthRequest.loginAsAdmin = YES;
11291168
[self restartAuthenticationForViewController:loginViewController];
11301169
}
@@ -1710,7 +1749,7 @@ - (void)setCurrentUserInternal:(SFUserAccount*)user {
17101749
// next login is web based it should not try to use that url.
17111750
// Also skip if the app uses a Welcome/Discovery domain — persisting the My Domain
17121751
// would pollute the server picker and prevent returning to the discovery page on logout.
1713-
BOOL isDiscoveryLogin = [[[SFDomainDiscoveryCoordinator alloc] init] isDiscoveryDomain:self.loginHost];
1752+
BOOL isDiscoveryLogin = [SFDomainDiscoveryCoordinator isDiscoveryDomain:self.loginHost];
17141753
if (user.credentials.domain && !isNativeLogin && !isDiscoveryLogin)
17151754
self.loginHost = user.credentials.domain;
17161755
[self didChangeValueForKey:@"currentUser"];

0 commit comments

Comments
 (0)