Skip to content

Commit c7160fd

Browse files
authored
Fix nil-sceneId crash on advanced-auth browser callback for pre-scene logins (#4098) (#4122)
Fix nil-sceneId crash on advanced-auth browser callback for pre-scene logins
1 parent fc724d1 commit c7160fd

4 files changed

Lines changed: 96 additions & 2 deletions

File tree

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator+Internal.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ NS_ASSUME_NONNULL_BEGIN
8888
*/
8989
- (void)migrateRefreshToken:(SFUserAccount *)user;
9090

91+
/**
92+
Builds the options dictionary handed to the URL handler on the advanced-auth browser callback.
93+
Guards against a nil sceneId so nil is never inserted into the dictionary.
94+
@param sceneId The auth session's scene id, or nil if no scene was connected / the session deallocated.
95+
@return A dictionary keyed by kSFIDPSceneIdKey when sceneId is non-nil, or an empty dictionary otherwise.
96+
*/
97+
- (NSDictionary *)browserCallbackOptionsForSceneId:(nullable NSString *)sceneId;
98+
9199
@end
92100

93101
NS_ASSUME_NONNULL_END

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ - (void)continueNativeBrowserFlowWithSharedBrowserSessionEnabled:(BOOL)shareBrow
433433
_asWebAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:nativeBrowserUrl callbackURLScheme:[NSURL URLWithString:self.credentials.redirectUri].scheme completionHandler:^(NSURL *callbackURL, NSError *error) {
434434
__strong typeof(weakSelf) strongSelf = weakSelf;
435435
if (!error && [[SFSDKURLHandlerManager sharedInstance] canHandleRequest:callbackURL options:nil]) {
436-
NSDictionary *options = @{kSFIDPSceneIdKey : self.authSession.sceneId};
436+
NSDictionary *options = [self browserCallbackOptionsForSceneId:self.authSession.sceneId];
437437
[[SFSDKURLHandlerManager sharedInstance] processRequest:callbackURL options:options completion:nil failure:nil];
438438
} else {
439439
[strongSelf.delegate oauthCoordinatorDidCancelBrowserAuthentication:strongSelf];
@@ -443,6 +443,13 @@ - (void)continueNativeBrowserFlowWithSharedBrowserSessionEnabled:(BOOL)shareBrow
443443
[self.delegate oauthCoordinator:self didBeginAuthenticationWithSession:_asWebAuthenticationSession];
444444
}
445445

446+
- (NSDictionary *)browserCallbackOptionsForSceneId:(nullable NSString *)sceneId {
447+
// Guard against a nil sceneId so we never insert nil into the options dictionary; omit the
448+
// key and let the URL handler fall back to the default scene. sceneId can be nil if the weak
449+
// authSession has deallocated by the time the browser callback fires.
450+
return sceneId ? @{kSFIDPSceneIdKey : sceneId} : @{};
451+
}
452+
446453
- (void)beginWebViewFlow {
447454
if (![NSThread isMainThread]) {
448455
dispatch_async(dispatch_get_main_queue(), ^{

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
#import "SFOAuthCoordinator+Internal.h"
3030
#import "SFIdentityCoordinator.h"
3131

32+
// Prefix for the synthesized scene id used when a login starts before any UIScene has connected.
33+
static NSString * const kSFSDKAuthSessionUnscopedSceneIdPrefix = @"com.salesforce.mobilesdk.unscopedAuthSession-";
34+
3235
@interface SFSDKAuthSession()
3336
@end
3437

@@ -47,7 +50,9 @@ -(instancetype)initWith:(SFSDKAuthRequest *)request credentials:(SFOAuthCredenti
4750
_credentials = (creds == nil) ? [self newClientCredentials] : creds;
4851
_credentials.jwt = request.jwtToken;
4952
_spAppCredentials = spAppCredentials;
50-
_sceneId = request.scene.session.persistentIdentifier; // Pass through for convenience
53+
// When no scene is connected yet, persistentIdentifier is nil; synthesize a unique per-session id
54+
// so this session gets its own authSessions[] key and the browser callback can key back to it.
55+
_sceneId = request.scene.session.persistentIdentifier ?: [kSFSDKAuthSessionUnscopedSceneIdPrefix stringByAppendingString:[[NSUUID UUID] UUIDString]];
5156
[self initCoordinator];
5257
}
5358
return self;

libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorTests.m

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,79 @@ - (void)testMigrateRefreshTokenSetup {
100100
XCTAssertEqual(capturedAuthInfo.authType, SFOAuthTypeRefreshTokenMigration, @"AuthInfo type should be refresh token migration");
101101
}
102102

103+
// Must match kSFSDKAuthSessionUnscopedSceneIdPrefix in SFSDKAuthSession.m.
104+
static NSString * const kExpectedUnscopedSceneIdPrefix = @"com.salesforce.mobilesdk.unscopedAuthSession-";
105+
106+
// A session created before any UIScene connects must still expose a non-nil sceneId, otherwise the
107+
// advanced-auth browser callback crashes and the session is dropped from the authSessions store.
108+
- (void)test_givenNoConnectedScene_whenAuthSessionCreated_thenSceneIdIsNonNilWithUnscopedPrefix {
109+
SFSDKAuthRequest *authRequest = [[SFSDKAuthRequest alloc] init];
110+
authRequest.oauthClientId = @"testClientId";
111+
authRequest.oauthCompletionUrl = @"testapp://callback";
112+
authRequest.loginHost = @"login.salesforce.com";
113+
XCTAssertNil(authRequest.scene, @"Precondition: no scene connected yet");
114+
115+
SFSDKAuthSession *authSession = [[SFSDKAuthSession alloc] initWith:authRequest credentials:nil];
116+
117+
XCTAssertNotNil(authSession.sceneId, @"sceneId must be non-nil so the advanced-auth callback options dictionary is safe to build and the session is stored under a valid key");
118+
XCTAssertTrue([authSession.sceneId hasPrefix:kExpectedUnscopedSceneIdPrefix], @"A scene-less session should get the synthesized unscoped scene id, got: %@", authSession.sceneId);
119+
}
120+
121+
// Two scene-less sessions must get distinct sceneIds so they cannot collide on a single authSessions[]
122+
// key, and each sceneId must be stable for the session's lifetime.
123+
- (void)test_givenTwoNoSceneAuthSessions_whenCreated_thenSceneIdsAreDistinctAndStable {
124+
SFSDKAuthRequest *request1 = [[SFSDKAuthRequest alloc] init];
125+
request1.oauthClientId = @"testClientId";
126+
request1.oauthCompletionUrl = @"testapp://callback";
127+
request1.loginHost = @"login.salesforce.com";
128+
129+
SFSDKAuthRequest *request2 = [[SFSDKAuthRequest alloc] init];
130+
request2.oauthClientId = @"testClientId";
131+
request2.oauthCompletionUrl = @"testapp://callback";
132+
request2.loginHost = @"login.salesforce.com";
133+
134+
SFSDKAuthSession *session1 = [[SFSDKAuthSession alloc] initWith:request1 credentials:nil];
135+
SFSDKAuthSession *session2 = [[SFSDKAuthSession alloc] initWith:request2 credentials:nil];
136+
137+
XCTAssertNotNil(session1.sceneId);
138+
XCTAssertNotNil(session2.sceneId);
139+
XCTAssertNotEqualObjects(session1.sceneId, session2.sceneId, @"Two scene-less sessions must get distinct scene ids so they cannot collide on a single authSessions[] key");
140+
// Frozen for the session's lifetime: reading again yields the same value.
141+
XCTAssertEqualObjects(session1.sceneId, session1.sceneId, @"sceneId must be stable for the session's lifetime");
142+
}
143+
144+
// Helper to build a coordinator whose browser-callback options we can inspect.
145+
- (SFOAuthCoordinator *)browserFlowCoordinator {
146+
SFSDKAuthRequest *authRequest = [[SFSDKAuthRequest alloc] init];
147+
authRequest.oauthClientId = @"testClientId";
148+
authRequest.oauthCompletionUrl = @"testapp://callback";
149+
authRequest.loginHost = @"login.salesforce.com";
150+
SFSDKAuthSession *authSession = [[SFSDKAuthSession alloc] initWith:authRequest credentials:nil];
151+
return [[SFOAuthCoordinator alloc] initWithAuthSession:authSession];
152+
}
153+
154+
// When a scene is connected, the advanced-auth browser callback must key its options dictionary by
155+
// the scene id so the URL handler routes the response to the originating scene.
156+
- (void)test_givenSceneId_whenBuildingBrowserCallbackOptions_thenOptionsAreKeyedBySceneId {
157+
SFOAuthCoordinator *coordinator = [self browserFlowCoordinator];
158+
159+
NSDictionary *options = [coordinator browserCallbackOptionsForSceneId:@"scene-42"];
160+
161+
XCTAssertEqualObjects(options[kSFIDPSceneIdKey], @"scene-42", @"A non-nil sceneId must be carried under kSFIDPSceneIdKey so the callback routes to the originating scene");
162+
XCTAssertEqual(options.count, (NSUInteger)1, @"Only the scene id key should be present");
163+
}
164+
165+
// When no scene id is available (e.g. login started before a UIScene connected, or the weak
166+
// authSession deallocated before the callback), the options must be an empty dictionary rather than
167+
// crashing on a nil insert; the URL handler then falls back to the default scene.
168+
- (void)test_givenNilSceneId_whenBuildingBrowserCallbackOptions_thenOptionsAreEmptyAndDoNotCrash {
169+
SFOAuthCoordinator *coordinator = [self browserFlowCoordinator];
170+
171+
NSDictionary *options = [coordinator browserCallbackOptionsForSceneId:nil];
172+
173+
XCTAssertNotNil(options, @"Options must never be nil");
174+
XCTAssertEqual(options.count, (NSUInteger)0, @"A nil sceneId must yield an empty options dictionary so nil is never inserted and the handler falls back to the default scene");
175+
}
176+
103177
@end
104178

0 commit comments

Comments
 (0)