Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions GoogleSignIn/Sources/GIDSignIn.m
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@
// The delay before the new sign-in flow can be presented after the existing one is cancelled.
static const NSTimeInterval kPresentationDelayAfterCancel = 1.0;

#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
// The delay before checking whether a backgrounded auth flow was dismissed without a callback.
static const NSTimeInterval kInterruptedAuthFlowCancellationDelay = 0.5;
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

// Parameters for the auth and token exchange endpoints.
static NSString *const kAudienceParameter = @"audience";
// See b/11669751 .
Expand Down Expand Up @@ -189,6 +194,8 @@ @implementation GIDSignIn {
GIDTimedLoader *_timedLoader;
// Flag indicating developer's intent to use App Check.
BOOL _configureAppCheckCalled;
// Whether the current auth flow entered the background while still pending.
BOOL _authorizationFlowEnteredBackground;
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
}

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

#pragma mark - Custom getters and setters

- (void)dealloc {
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
[[NSNotificationCenter defaultCenter] removeObserver:self];
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
}

+ (GIDSignIn *)sharedInstance {
static dispatch_once_t once;
static GIDSignIn *sharedInstance;
Expand Down Expand Up @@ -688,6 +704,18 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
[authStateMigrationService migrateIfNeededWithTokenURL:_appAuthConfiguration.tokenEndpoint
callbackPath:kBrowserCallbackPath
isFreshInstall:isFreshInstall];

#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[notificationCenter addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
}
return self;
}
Expand All @@ -713,6 +741,9 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
// derive suitable options for the continuation!
if (!options.continuation) {
_currentOptions = options;
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
_authorizationFlowEnteredBackground = NO;
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
}

if (options.interactive) {
Expand Down Expand Up @@ -781,6 +812,39 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
}
}

#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
- (void)applicationDidEnterBackground:(NSNotification *)notification {
if (_currentAuthorizationFlow && _currentOptions.interactive) {
_authorizationFlowEnteredBackground = YES;
}
}

- (void)applicationDidBecomeActive:(NSNotification *)notification {
if (!_authorizationFlowEnteredBackground) {
return;
}

dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(kInterruptedAuthFlowCancellationDelay * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self cancelInterruptedAuthorizationFlowIfNeeded];
});
}

- (void)cancelInterruptedAuthorizationFlowIfNeeded {
if (!_authorizationFlowEnteredBackground ||
!_currentAuthorizationFlow ||
!_currentOptions.interactive ||
_currentOptions.presentingViewController.presentedViewController) {
return;
}

_authorizationFlowEnteredBackground = NO;
[_currentAuthorizationFlow cancel];
_currentAuthorizationFlow = nil;
}
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

#pragma mark - Authentication flow

- (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options {
Expand All @@ -804,6 +868,10 @@ - (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options
callback:
^(OIDAuthorizationResponse *_Nullable authorizationResponse,
NSError *_Nullable error) {
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
self->_authorizationFlowEnteredBackground = NO;
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
self->_currentAuthorizationFlow = nil;
[self processAuthorizationResponse:authorizationResponse
error:error
emmSupport:emmSupport];
Expand Down
103 changes: 102 additions & 1 deletion GoogleSignIn/Tests/Unit/GIDSignInTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ @interface GIDSignInTest : XCTestCase {
// Mock for |OIDAuthorizationService|
id _oidAuthorizationService;

// Mock for |OIDExternalUserAgentSession|.
id _authorizationFlow;

// Parameter saved from delegate call.
NSError *_authError;

Expand Down Expand Up @@ -332,14 +335,16 @@ - (void)setUp {
});
_user = OCMStrictClassMock([GIDGoogleUser class]);
_oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]);
_authorizationFlow = OCMProtocolMock(@protocol(OIDExternalUserAgentSession));
OCMStub([_oidAuthorizationService
presentAuthorizationRequest:SAVE_TO_ARG_BLOCK(self->_savedAuthorizationRequest)
#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
presentingViewController:SAVE_TO_ARG_BLOCK(self->_savedPresentingViewController)
#elif TARGET_OS_OSX
presentingWindow:SAVE_TO_ARG_BLOCK(self->_savedPresentingWindow)
#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
callback:COPY_TO_ARG_BLOCK(self->_savedAuthorizationCallback)]);
callback:COPY_TO_ARG_BLOCK(self->_savedAuthorizationCallback)])
.andReturn(_authorizationFlow);
OCMStub([self->_oidAuthorizationService
performTokenRequest:SAVE_TO_ARG_BLOCK(self->_savedTokenRequest)
originalAuthorizationResponse:[OCMArg any]
Expand Down Expand Up @@ -379,6 +384,7 @@ - (void)tearDown {
OCMVerifyAll(_authorization);
OCMVerifyAll(_user);
OCMVerifyAll(_oidAuthorizationService);
OCMVerifyAll(_authorizationFlow);

#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
OCMVerifyAll(_presentingViewController);
Expand Down Expand Up @@ -1198,6 +1204,74 @@ - (void)testOAuthLogin_ModalCanceled {
XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled);
}

#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST

- (void)testOAuthLogin_BackgroundedAuthFlowCanceledWhenAuthUIClears {
OCMStub([_presentingViewController presentedViewController]).andReturn(nil);

XCTestExpectation *completionExpectation =
[self expectationWithDescription:@"Completion should be called"];
GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult,
NSError *_Nullable error) {
[completionExpectation fulfill];
self->_completion(signInResult, error);
};
[self beginInteractiveSignInWithCompletion:completion];

[[[_authorizationFlow expect] andDo:^(NSInvocation *invocation) {
self->_savedAuthorizationCallback(nil, [self authorizationFlowCancellationError]);
}] cancel];

[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];

[self waitForExpectationsWithTimeout:1 handler:nil];
XCTAssertTrue(_completionCalled, @"should call delegate");
XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled);
}

- (void)testOAuthLogin_BackgroundedAuthFlowIgnoresCompletedFlow {
OCMStub([_presentingViewController presentedViewController]).andReturn(nil);
[[_authorizationFlow reject] cancel];

XCTestExpectation *completionExpectation =
[self expectationWithDescription:@"Completion should be called"];
GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult,
NSError *_Nullable error) {
[completionExpectation fulfill];
self->_completion(signInResult, error);
};
[self beginInteractiveSignInWithCompletion:completion];

[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
_savedAuthorizationCallback(nil, [self authorizationFlowCancellationError]);

[self waitForExpectationsWithTimeout:1 handler:nil];
[self waitForInterruptedAuthFlowDelay];
XCTAssertTrue(_completionCalled, @"should call delegate");
XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled);
}

- (void)testOAuthLogin_DidBecomeActiveWithoutBackgroundDoesNotCancel {
OCMStub([_presentingViewController presentedViewController]).andReturn(nil);
[[_authorizationFlow reject] cancel];

[self beginInteractiveSignInWithCompletion:_completion];

[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];

[self waitForInterruptedAuthFlowDelay];
XCTAssertFalse(_completionCalled, @"should not call delegate");
}

#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

- (void)testOAuthLogin_KeychainError {
// This error is going be overidden by `-[GIDSignIn errorWithString:code:]`
// We just need to fill in the error so that happens.
Expand Down Expand Up @@ -2050,6 +2124,33 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow

#pragma mark - Private Helpers

#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST

- (void)beginInteractiveSignInWithCompletion:(nullable GIDSignInCompletion)completion {
[_signIn signInWithPresentingViewController:_presentingViewController completion:completion];
XCTAssertNotNil(_savedAuthorizationRequest);
XCTAssertNotNil(_savedAuthorizationCallback);
XCTAssertEqual(_savedPresentingViewController, _presentingViewController);
}

- (NSError *)authorizationFlowCancellationError {
return [NSError errorWithDomain:OIDGeneralErrorDomain
code:OIDErrorCodeUserCanceledAuthorizationFlow
userInfo:nil];
}

- (void)waitForInterruptedAuthFlowDelay {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Interrupted auth flow delay"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:nil];
}

#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

- (NSDictionary<NSString *, NSString *> *)
additionalParametersWithEMMPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired
claimsAsJSONRequired:(BOOL)claimsAsJSONRequired {
Expand Down