Skip to content

Commit b3dc482

Browse files
brandonpageclaude
andcommitted
Fix Login for Admin and Welcome Discovery incompatability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 26a5133 commit b3dc482

7 files changed

Lines changed: 456 additions & 90 deletions

File tree

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

Lines changed: 50 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,33 @@ - (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+
SFDomainDiscoveryCoordinator *discoveryCoordinator = [[SFDomainDiscoveryCoordinator alloc] init];
380+
BOOL isDiscoveryHost = [discoveryCoordinator isDiscoveryDomain:loginHost];
381+
// Hide only when we are mid-discovery (no My Domain selected yet).
382+
if (isDiscoveryHost && !coordinator.domainUpdated) {
383+
return NO;
384+
}
385+
return YES;
386+
}
387+
343388
- (UIView *)createTitleItem {
344389
NSString *title = [SFSDKResourceUtils localizedString:@"TITLE_LOGIN"];
345390
// Setup top item.

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,19 @@ NS_ASSUME_NONNULL_BEGIN
3535

3636
/// Indicates that browser auth was initiated by the "Login for Admin" action.
3737
/// When YES, cancelling the browser session returns to the WebView login instead of showing the server picker.
38-
@property (nonatomic, assign) BOOL loginAsAdmin;
38+
@property (nonatomic, assign) BOOL loginForAdmin;
39+
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+
/// `loginForAdmin == 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 `loginForAdmin` when the LFA browser session is cancelled.
46+
@property (nonatomic, copy, nullable) NSString *loginForAdminMyDomain;
47+
48+
/// Login-for-Admin override: the login_hint OAuth parameter to pass to the
49+
/// browser session. Same scoping rules as `loginForAdminMyDomain`.
50+
@property (nonatomic, copy, nullable) NSString *loginForAdminLoginHint;
3951

4052
@property (nonatomic, strong) NSArray<NSString *> *additionalOAuthParameterKeys;
4153
@property (nonatomic, strong) NSDictionary<NSString *,id> * additionalTokenRefreshParams;

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFSDKAuthSession.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ -(void)initCoordinator {
6060
self.oauthCoordinator.additionalTokenRefreshParams = self.oauthRequest.additionalTokenRefreshParams;
6161
self.oauthCoordinator.scopes = self.oauthRequest.scopes;
6262
self.oauthCoordinator.brandLoginPath = self.oauthRequest.brandLoginPath;
63-
self.oauthCoordinator.useBrowserAuth = self.oauthRequest.useBrowserAuth || self.oauthRequest.loginAsAdmin;
63+
self.oauthCoordinator.useBrowserAuth = self.oauthRequest.useBrowserAuth || self.oauthRequest.loginForAdmin;
6464
if (_spAppCredentials && _spAppCredentials.domain) {
6565
self.oauthCoordinator.credentials.domain = _spAppCredentials.domain;
6666
}

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: 49 additions & 9 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.loginForAdmin && request.loginForAdminMyDomain.length > 0;
617+
if (useLfaOverride) {
618+
authSession.credentials.domain = request.loginForAdminMyDomain;
619+
authSession.oauthCoordinator.loginHint = request.loginForAdminLoginHint;
620+
} else {
621+
authSession.oauthCoordinator.loginHint = loginHint;
622+
}
623+
NSString *appConfigLoginHost = useLfaOverride ? request.loginForAdminMyDomain : 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 loginForAdminLoginHint 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.
1036-
if (coordinator.authSession.oauthRequest.loginAsAdmin) {
1037-
coordinator.authSession.oauthRequest.loginAsAdmin = NO;
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.
1054+
if (coordinator.authSession.oauthRequest.loginForAdmin) {
1055+
coordinator.authSession.oauthRequest.loginForAdmin = NO;
1056+
coordinator.authSession.oauthRequest.loginForAdminMyDomain = nil;
1057+
coordinator.authSession.oauthRequest.loginForAdminLoginHint = nil;
10381058
[self restartAuthentication:coordinator.authSession];
10391059
return;
10401060
}
@@ -1125,7 +1145,27 @@ - (void)loginViewControllerDidReload:(SFLoginViewController *)loginViewControlle
11251145
- (void)loginViewControllerDidSelectLoginForAdmin:(SFLoginViewController *)loginViewController {
11261146
NSString *sceneId = loginViewController.view.window.windowScene.session.persistentIdentifier;
11271147
SFSDKAuthSession *session = self.authSessions[sceneId];
1128-
session.oauthRequest.loginAsAdmin = YES;
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+
SFDomainDiscoveryCoordinator *discoveryCoordinator = [[SFDomainDiscoveryCoordinator alloc] init];
1156+
if ([discoveryCoordinator isDiscoveryDomain:session.oauthRequest.loginHost] && !coordinator.domainUpdated) {
1157+
[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)];
1158+
return;
1159+
}
1160+
1161+
// Phase-2 Welcome Discovery (or a non-discovery host): record the resolved
1162+
// My Domain and login hint as LFA-scoped overrides on the request. The
1163+
// request's loginHost is left untouched so that Reload / Clear Cache /
1164+
// post-cancel restart continue to use the originally configured host.
1165+
// These overrides are in-memory only and are cleared on LFA cancel.
1166+
session.oauthRequest.loginForAdminMyDomain = coordinator.credentials.domain.length > 0 ? coordinator.credentials.domain : nil;
1167+
session.oauthRequest.loginForAdminLoginHint = coordinator.loginHint.length > 0 ? coordinator.loginHint : nil;
1168+
session.oauthRequest.loginForAdmin = YES;
11291169
[self restartAuthenticationForViewController:loginViewController];
11301170
}
11311171

0 commit comments

Comments
 (0)