Skip to content

Commit ec73fc5

Browse files
committed
Fix interrupted auth session completion
1 parent 02616ac commit ec73fc5

2 files changed

Lines changed: 170 additions & 1 deletion

File tree

GoogleSignIn/Sources/GIDSignIn.m

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@
128128
// The delay before the new sign-in flow can be presented after the existing one is cancelled.
129129
static const NSTimeInterval kPresentationDelayAfterCancel = 1.0;
130130

131+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
132+
// The delay before checking whether a backgrounded auth flow was dismissed without a callback.
133+
static const NSTimeInterval kInterruptedAuthFlowCancellationDelay = 0.5;
134+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
135+
131136
// Parameters for the auth and token exchange endpoints.
132137
static NSString *const kAudienceParameter = @"audience";
133138
// See b/11669751 .
@@ -189,6 +194,8 @@ @implementation GIDSignIn {
189194
GIDTimedLoader *_timedLoader;
190195
// Flag indicating developer's intent to use App Check.
191196
BOOL _configureAppCheckCalled;
197+
// Whether the current auth flow entered the background while still pending.
198+
BOOL _authorizationFlowEnteredBackground;
192199
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
193200
}
194201

@@ -202,6 +209,9 @@ - (BOOL)handleURL:(NSURL *)url {
202209
// Check if the callback path matches the expected one for a URL from Safari/Chrome/SafariVC.
203210
if ([url.path isEqual:kBrowserCallbackPath]) {
204211
if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) {
212+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
213+
_authorizationFlowEnteredBackground = NO;
214+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
205215
_currentAuthorizationFlow = nil;
206216
return YES;
207217
}
@@ -601,6 +611,12 @@ - (void)disconnectWithCompletion:(nullable GIDDisconnectCompletion)completion {
601611

602612
#pragma mark - Custom getters and setters
603613

614+
- (void)dealloc {
615+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
616+
[[NSNotificationCenter defaultCenter] removeObserver:self];
617+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
618+
}
619+
604620
+ (GIDSignIn *)sharedInstance {
605621
static dispatch_once_t once;
606622
static GIDSignIn *sharedInstance;
@@ -688,6 +704,18 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
688704
[authStateMigrationService migrateIfNeededWithTokenURL:_appAuthConfiguration.tokenEndpoint
689705
callbackPath:kBrowserCallbackPath
690706
isFreshInstall:isFreshInstall];
707+
708+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
709+
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
710+
[notificationCenter addObserver:self
711+
selector:@selector(applicationDidEnterBackground:)
712+
name:UIApplicationDidEnterBackgroundNotification
713+
object:nil];
714+
[notificationCenter addObserver:self
715+
selector:@selector(applicationDidBecomeActive:)
716+
name:UIApplicationDidBecomeActiveNotification
717+
object:nil];
718+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
691719
}
692720
return self;
693721
}
@@ -713,6 +741,9 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
713741
// derive suitable options for the continuation!
714742
if (!options.continuation) {
715743
_currentOptions = options;
744+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
745+
_authorizationFlowEnteredBackground = NO;
746+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
716747
}
717748

718749
if (options.interactive) {
@@ -781,6 +812,39 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
781812
}
782813
}
783814

815+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
816+
- (void)applicationDidEnterBackground:(NSNotification *)notification {
817+
if (_currentAuthorizationFlow && _currentOptions.interactive) {
818+
_authorizationFlowEnteredBackground = YES;
819+
}
820+
}
821+
822+
- (void)applicationDidBecomeActive:(NSNotification *)notification {
823+
if (!_authorizationFlowEnteredBackground) {
824+
return;
825+
}
826+
827+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
828+
(int64_t)(kInterruptedAuthFlowCancellationDelay * NSEC_PER_SEC)),
829+
dispatch_get_main_queue(), ^{
830+
[self cancelInterruptedAuthorizationFlowIfNeeded];
831+
});
832+
}
833+
834+
- (void)cancelInterruptedAuthorizationFlowIfNeeded {
835+
if (!_authorizationFlowEnteredBackground ||
836+
!_currentAuthorizationFlow ||
837+
!_currentOptions.interactive ||
838+
_currentOptions.presentingViewController.presentedViewController) {
839+
return;
840+
}
841+
842+
_authorizationFlowEnteredBackground = NO;
843+
[_currentAuthorizationFlow cancel];
844+
_currentAuthorizationFlow = nil;
845+
}
846+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
847+
784848
#pragma mark - Authentication flow
785849

786850
- (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options {
@@ -804,6 +868,10 @@ - (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options
804868
callback:
805869
^(OIDAuthorizationResponse *_Nullable authorizationResponse,
806870
NSError *_Nullable error) {
871+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
872+
self->_authorizationFlowEnteredBackground = NO;
873+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
874+
self->_currentAuthorizationFlow = nil;
807875
[self processAuthorizationResponse:authorizationResponse
808876
error:error
809877
emmSupport:emmSupport];

GoogleSignIn/Tests/Unit/GIDSignInTest.m

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ @interface GIDSignInTest : XCTestCase {
228228
// Mock for |OIDAuthorizationService|
229229
id _oidAuthorizationService;
230230

231+
// Mock for |OIDExternalUserAgentSession|.
232+
id _authorizationFlow;
233+
231234
// Parameter saved from delegate call.
232235
NSError *_authError;
233236

@@ -332,14 +335,16 @@ - (void)setUp {
332335
});
333336
_user = OCMStrictClassMock([GIDGoogleUser class]);
334337
_oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]);
338+
_authorizationFlow = OCMProtocolMock(@protocol(OIDExternalUserAgentSession));
335339
OCMStub([_oidAuthorizationService
336340
presentAuthorizationRequest:SAVE_TO_ARG_BLOCK(self->_savedAuthorizationRequest)
337341
#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
338342
presentingViewController:SAVE_TO_ARG_BLOCK(self->_savedPresentingViewController)
339343
#elif TARGET_OS_OSX
340344
presentingWindow:SAVE_TO_ARG_BLOCK(self->_savedPresentingWindow)
341345
#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
342-
callback:COPY_TO_ARG_BLOCK(self->_savedAuthorizationCallback)]);
346+
callback:COPY_TO_ARG_BLOCK(self->_savedAuthorizationCallback)])
347+
.andReturn(_authorizationFlow);
343348
OCMStub([self->_oidAuthorizationService
344349
performTokenRequest:SAVE_TO_ARG_BLOCK(self->_savedTokenRequest)
345350
originalAuthorizationResponse:[OCMArg any]
@@ -379,6 +384,7 @@ - (void)tearDown {
379384
OCMVerifyAll(_authorization);
380385
OCMVerifyAll(_user);
381386
OCMVerifyAll(_oidAuthorizationService);
387+
OCMVerifyAll(_authorizationFlow);
382388

383389
#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
384390
OCMVerifyAll(_presentingViewController);
@@ -1198,6 +1204,74 @@ - (void)testOAuthLogin_ModalCanceled {
11981204
XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled);
11991205
}
12001206

1207+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
1208+
1209+
- (void)testOAuthLogin_BackgroundedAuthFlowCanceledWhenAuthUIClears {
1210+
OCMStub([_presentingViewController presentedViewController]).andReturn(nil);
1211+
1212+
XCTestExpectation *completionExpectation =
1213+
[self expectationWithDescription:@"Completion should be called"];
1214+
GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult,
1215+
NSError *_Nullable error) {
1216+
[completionExpectation fulfill];
1217+
self->_completion(signInResult, error);
1218+
};
1219+
[self beginInteractiveSignInWithCompletion:completion];
1220+
1221+
[[[_authorizationFlow expect] andDo:^(NSInvocation *invocation) {
1222+
self->_savedAuthorizationCallback(nil, [self authorizationFlowCancellationError]);
1223+
}] cancel];
1224+
1225+
[[NSNotificationCenter defaultCenter]
1226+
postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil];
1227+
[[NSNotificationCenter defaultCenter]
1228+
postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
1229+
1230+
[self waitForExpectationsWithTimeout:1 handler:nil];
1231+
XCTAssertTrue(_completionCalled, @"should call delegate");
1232+
XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled);
1233+
}
1234+
1235+
- (void)testOAuthLogin_BackgroundedAuthFlowIgnoresCompletedFlow {
1236+
OCMStub([_presentingViewController presentedViewController]).andReturn(nil);
1237+
[[_authorizationFlow reject] cancel];
1238+
1239+
XCTestExpectation *completionExpectation =
1240+
[self expectationWithDescription:@"Completion should be called"];
1241+
GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult,
1242+
NSError *_Nullable error) {
1243+
[completionExpectation fulfill];
1244+
self->_completion(signInResult, error);
1245+
};
1246+
[self beginInteractiveSignInWithCompletion:completion];
1247+
1248+
[[NSNotificationCenter defaultCenter]
1249+
postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil];
1250+
[[NSNotificationCenter defaultCenter]
1251+
postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
1252+
_savedAuthorizationCallback(nil, [self authorizationFlowCancellationError]);
1253+
1254+
[self waitForExpectationsWithTimeout:1 handler:nil];
1255+
[self waitForInterruptedAuthFlowDelay];
1256+
XCTAssertTrue(_completionCalled, @"should call delegate");
1257+
XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled);
1258+
}
1259+
1260+
- (void)testOAuthLogin_DidBecomeActiveWithoutBackgroundDoesNotCancel {
1261+
OCMStub([_presentingViewController presentedViewController]).andReturn(nil);
1262+
[[_authorizationFlow reject] cancel];
1263+
1264+
[self beginInteractiveSignInWithCompletion:_completion];
1265+
1266+
[[NSNotificationCenter defaultCenter]
1267+
postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
1268+
1269+
[self waitForInterruptedAuthFlowDelay];
1270+
XCTAssertFalse(_completionCalled, @"should not call delegate");
1271+
}
1272+
1273+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
1274+
12011275
- (void)testOAuthLogin_KeychainError {
12021276
// This error is going be overidden by `-[GIDSignIn errorWithString:code:]`
12031277
// We just need to fill in the error so that happens.
@@ -2050,6 +2124,33 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
20502124

20512125
#pragma mark - Private Helpers
20522126

2127+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
2128+
2129+
- (void)beginInteractiveSignInWithCompletion:(nullable GIDSignInCompletion)completion {
2130+
[_signIn signInWithPresentingViewController:_presentingViewController completion:completion];
2131+
XCTAssertNotNil(_savedAuthorizationRequest);
2132+
XCTAssertNotNil(_savedAuthorizationCallback);
2133+
XCTAssertEqual(_savedPresentingViewController, _presentingViewController);
2134+
}
2135+
2136+
- (NSError *)authorizationFlowCancellationError {
2137+
return [NSError errorWithDomain:OIDGeneralErrorDomain
2138+
code:OIDErrorCodeUserCanceledAuthorizationFlow
2139+
userInfo:nil];
2140+
}
2141+
2142+
- (void)waitForInterruptedAuthFlowDelay {
2143+
XCTestExpectation *expectation =
2144+
[self expectationWithDescription:@"Interrupted auth flow delay"];
2145+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
2146+
dispatch_get_main_queue(), ^{
2147+
[expectation fulfill];
2148+
});
2149+
[self waitForExpectationsWithTimeout:2 handler:nil];
2150+
}
2151+
2152+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
2153+
20532154
- (NSDictionary<NSString *, NSString *> *)
20542155
additionalParametersWithEMMPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired
20552156
claimsAsJSONRequired:(BOOL)claimsAsJSONRequired {

0 commit comments

Comments
 (0)