Skip to content

Commit b155f78

Browse files
authored
Merge pull request #4105 from sfdctaka/fix-ios26-login-host-classifier
Fix login-host recovery on iOS 26: broaden classifier + surface prefetch errors
2 parents 19d4436 + b5f452c commit b155f78

5 files changed

Lines changed: 351 additions & 11 deletions

File tree

libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@
193193
A3C7476129F709EB00D72B7F /* BiometricAuthenticationManagerInternal.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C7476029F709EB00D72B7F /* BiometricAuthenticationManagerInternal.swift */; };
194194
AA9154482F59E3C900A0A41C /* SFRestAPIDataTaskRaceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AA9154472F59E3C900A0A41C /* SFRestAPIDataTaskRaceTests.m */; };
195195
B70135621F87F63900995171 /* SFSDKAuthErrorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = B70135601F87F63900995171 /* SFSDKAuthErrorManager.h */; settings = {ATTRIBUTES = (Private, ); }; };
196+
B70135631F87F63900995171 /* SFSDKAuthErrorManager+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B70135651F87F63900995171 /* SFSDKAuthErrorManager+Internal.h */; };
196197
B70135641F87F63900995171 /* SFSDKAuthErrorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B70135611F87F63900995171 /* SFSDKAuthErrorManager.m */; };
197198
B711290D1F8A780800436CFB /* SFSDKAlertView.m in Sources */ = {isa = PBXBuildFile; fileRef = B71129071F8A780700436CFB /* SFSDKAlertView.m */; };
198199
B711290E1F8A780800436CFB /* SFSDKAlertMessageBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = B71129081F8A780800436CFB /* SFSDKAlertMessageBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -824,6 +825,7 @@
824825
AA9154472F59E3C900A0A41C /* SFRestAPIDataTaskRaceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SFRestAPIDataTaskRaceTests.m; path = SalesforceSDKCoreTests/SFRestAPIDataTaskRaceTests.m; sourceTree = SOURCE_ROOT; };
825826
B70135601F87F63900995171 /* SFSDKAuthErrorManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SFSDKAuthErrorManager.h; sourceTree = "<group>"; };
826827
B70135611F87F63900995171 /* SFSDKAuthErrorManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SFSDKAuthErrorManager.m; sourceTree = "<group>"; };
828+
B70135651F87F63900995171 /* SFSDKAuthErrorManager+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SFSDKAuthErrorManager+Internal.h"; sourceTree = "<group>"; };
827829
B71129071F8A780700436CFB /* SFSDKAlertView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFSDKAlertView.m; sourceTree = "<group>"; };
828830
B71129081F8A780800436CFB /* SFSDKAlertMessageBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SFSDKAlertMessageBuilder.h; sourceTree = "<group>"; };
829831
B71129091F8A780800436CFB /* SFSDKAlertView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SFSDKAlertView.h; sourceTree = "<group>"; };
@@ -1380,6 +1382,7 @@
13801382
4F96FCF41BFD32130022F021 /* SFAuthErrorHandlerList.h */,
13811383
4F96FCF51BFD32130022F021 /* SFAuthErrorHandlerList.m */,
13821384
B70135601F87F63900995171 /* SFSDKAuthErrorManager.h */,
1385+
B70135651F87F63900995171 /* SFSDKAuthErrorManager+Internal.h */,
13831386
B70135611F87F63900995171 /* SFSDKAuthErrorManager.m */,
13841387
6935FB9E23FDEC92002BEFCC /* ViewControllers */,
13851388
);
@@ -1858,6 +1861,7 @@
18581861
B7FB26E31F78096300FB25A2 /* SFSDKURLHandlerManager.h in Headers */,
18591862
CE4CE3091C0E523B009F6029 /* NSURL+SFAdditions.h in Headers */,
18601863
B70135621F87F63900995171 /* SFSDKAuthErrorManager.h in Headers */,
1864+
B70135631F87F63900995171 /* SFSDKAuthErrorManager+Internal.h in Headers */,
18611865
CE4CE32A1C0E523B009F6029 /* SalesforceSDKConstants.h in Headers */,
18621866
B7C274481F814BF500CE539D /* SFSDKSPLoginRequestCommand.h in Headers */,
18631867
CE4CE39B1C0E5272009F6029 /* SFSDKTestCredentialsData.h in Headers */,

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#import "SFOAuthCoordinator+Internal.h"
2929
#import "SFOAuthInfo.h"
3030
#import "SFSDKAuthConfigUtil.h"
31+
#import "SFSDKAuthErrorManager.h"
3132
#import "SFSDKCryptoUtils.h"
3233
#import "NSData+SFSDKUtils.h"
3334
#import "NSString+SFAdditions.h"
@@ -55,6 +56,8 @@
5556
#import <SalesforceSDKCommon/SFSDKDatasharingHelper.h>
5657
#import <LocalAuthentication/LocalAuthentication.h>
5758
#import "SFSDKResourceUtils.h"
59+
#import "SFSDKAuthErrorManager+Internal.h"
60+
5861
@interface SFOAuthCoordinator()
5962

6063
@property (nonatomic) NSString *networkIdentifier;
@@ -197,8 +200,19 @@ - (void)authenticate {
197200
[SFSDKAuthConfigUtil getMyDomainAuthConfig:^(SFOAuthOrgAuthConfiguration *authConfig, NSError *error) {
198201
__strong typeof(weakSelf) strongSelf = weakSelf;
199202
dispatch_async(dispatch_get_main_queue(), ^{
200-
// Ignore any errors why retrieving authconfig. Default to WKWebView
201-
// Errors should have already been logged.
203+
// A prefetch error against an unreachable login host is the earliest
204+
// authoritative signal that the host itself is bad. Falling through to
205+
// beginWebViewFlow would dispatch a WKWebView load against the same host
206+
// and hang silently, stranding the user on a blank screen and leaving the
207+
// bad host set as the sticky loginHost across app restarts. Surface it to
208+
// the failure delegate so the error manager's hostConnectionErrorHandler
209+
// can present an alert and roll loginHost back to the previous good host.
210+
// Non-host errors (parse failures, non-2xx responses, endpoint absent on
211+
// standard orgs) continue to fall through — auth-config is optional.
212+
if ([SFSDKAuthErrorManager errorIsHostConnectionFailure:error]) {
213+
[strongSelf notifyDelegateOfFailure:error authInfo:strongSelf.authInfo];
214+
return;
215+
}
202216
if (!self.frontdoorBridgeLoginOverride &&
203217
(authConfig.useNativeBrowserForAuth ||
204218
[SalesforceSDKManager sharedManager].sdk_forceAdvancedAuthentication)) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright (c) 2017-present, salesforce.com, inc. All rights reserved.
3+
4+
Redistribution and use of this software in source and binary forms, with or without modification,
5+
are permitted provided that the following conditions are met:
6+
* Redistributions of source code must retain the above copyright notice, this list of conditions
7+
and the following disclaimer.
8+
* Redistributions in binary form must reproduce the above copyright notice, this list of
9+
conditions and the following disclaimer in the documentation and/or other materials provided
10+
with the distribution.
11+
* Neither the name of salesforce.com, inc. nor the names of its contributors may be used to
12+
endorse or promote products derived from this software without specific prior written
13+
permission of salesforce.com, inc.
14+
15+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
16+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
17+
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
18+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
20+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
21+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
22+
WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23+
*/
24+
25+
#import "SFSDKAuthErrorManager.h"
26+
27+
NS_ASSUME_NONNULL_BEGIN
28+
29+
// Internal-only surface for SFSDKAuthErrorManager. Not published in the umbrella
30+
// header — kept off the public API but shared between SFOAuthCoordinator.m (for
31+
// the auth-config prefetch gate) and SFSDKErrorManagerTests.m (for direct
32+
// predicate coverage). A single compiler-visible declaration keeps both call
33+
// sites in sync if the signature ever changes.
34+
@interface SFSDKAuthErrorManager (Internal)
35+
36+
+ (BOOL)errorIsHostConnectionFailure:(nullable NSError *)error;
37+
38+
@end
39+
40+
NS_ASSUME_NONNULL_END

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFSDKAuthErrorManager.m

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
*/
2929

3030
#import "SFSDKAuthErrorManager.h"
31+
#import "SFSDKAuthErrorManager+Internal.h"
3132
#import "SFAuthErrorHandlerList.h"
3233
#import "SFAuthErrorHandler.h"
3334
#import "SFOAuthCoordinator+Internal.h"
@@ -135,14 +136,19 @@ - (SFAuthErrorHandlerList *)populateDefaultAuthErrorHandlerList
135136
}];
136137
[authHandlerList addAuthErrorHandler:self.networkFailureAuthErrorHandler];
137138

138-
// Host connection error handler
139+
// Host connection error handler.
140+
//
141+
// NSURLErrorTimedOut / CannotConnectToHost / NetworkConnectionLost / NotConnectedToInternet
142+
// also appear in +errorIsNetworkFailure:. NetworkFailureErrorHandler runs first in the chain
143+
// and claims those codes only on Refresh flows with an existing access token; all other
144+
// contexts return NO there and fall through to this handler. Ordering is guarded by
145+
// testNetworkFailureClaimsFirst_RefreshWithToken.
139146
self.hostConnectionErrorHandler = [[SFAuthErrorHandler alloc] initWithName:kSFHostConnectionErrorHandler
140147
authSessionBlock:^BOOL(NSError *error, SFSDKAuthSession *authSession, NSDictionary *options) {
141-
if ((error.userInfo[@"_kCFStreamErrorCodeKey"] && error.userInfo[@"_kCFStreamErrorDomainKey"]) ||
142-
([error.domain isEqualToString:kSFOAuthErrorDomain] && error.code == kSFOAuthErrorInvalidURL)) {
148+
if ([[weakSelf class] errorIsHostConnectionFailure:error]) {
143149
if (self.hostConnectionErrorHandlerBlock) {
144150
self.hostConnectionErrorHandlerBlock(error, authSession, options);
145-
return YES;
151+
return YES;
146152
}
147153
}
148154
return NO;
@@ -187,11 +193,43 @@ + (BOOL)errorIsInvalidAuthCredentials:(NSError *)error
187193
}
188194

189195
/**
190-
* Evaluates an NSError object to see if it represents a network failure during
191-
* an attempted connection.
196+
* Evaluates an NSError object to see if it represents a host-connection failure —
197+
* an unreachable or non-existent login host — rather than a transient network
198+
* failure on an otherwise reachable host.
192199
* @param error The NSError to evaluate.
193-
* @return YES if the error represents a network failure, NO otherwise.
200+
* @return YES if the error should trigger the host-connection recovery path, NO otherwise.
194201
*/
202+
+ (BOOL)errorIsHostConnectionFailure:(NSError *)error
203+
{
204+
if (error == nil || error.domain == nil) {
205+
return NO;
206+
}
207+
// Legacy iOS <= 18 shape: CFNetwork stream stack attached _kCFStreamError* keys to userInfo.
208+
if (error.userInfo[@"_kCFStreamErrorCodeKey"] && error.userInfo[@"_kCFStreamErrorDomainKey"]) {
209+
return YES;
210+
}
211+
// OAuth invalid-URL is a bad-host signal.
212+
if ([error.domain isEqualToString:kSFOAuthErrorDomain] && error.code == kSFOAuthErrorInvalidURL) {
213+
return YES;
214+
}
215+
// iOS 26+ shape: DNS resolution moved to Network.framework, so the CFStream keys are absent.
216+
// NSURLErrorDomain surfaces these codes with a bare userInfo instead.
217+
if ([error.domain isEqualToString:NSURLErrorDomain]) {
218+
switch (error.code) {
219+
case NSURLErrorCannotFindHost: // -1003
220+
case NSURLErrorDNSLookupFailed: // -1006
221+
case NSURLErrorCannotConnectToHost: // -1004
222+
case NSURLErrorTimedOut: // -1001
223+
case NSURLErrorNotConnectedToInternet: // -1009
224+
case NSURLErrorNetworkConnectionLost: // -1005
225+
return YES;
226+
default:
227+
break;
228+
}
229+
}
230+
return NO;
231+
}
232+
195233
+ (BOOL)errorIsNetworkFailure:(NSError *)error
196234
{
197235
BOOL isNetworkFailure = NO;

0 commit comments

Comments
 (0)