From 87eddac9e41687a0e79afb1a0370ab9013cb3038 Mon Sep 17 00:00:00 2001 From: pinlu Date: Wed, 9 Nov 2022 16:21:15 -0800 Subject: [PATCH 01/10] Refactor GIDGoogleUser public API (#249) --- ...ppAuthFetcherAuthorizationWithEMMSupport.h | 36 + ...ppAuthFetcherAuthorizationWithEMMSupport.m | 129 ++++ .../GIDAuthentication.h} | 19 +- GoogleSignIn/Sources/GIDAuthentication.m | 377 +--------- ...thentication_Private.h => GIDEMMSupport.h} | 42 +- GoogleSignIn/Sources/GIDEMMSupport.m | 101 +++ GoogleSignIn/Sources/GIDGoogleUser.m | 305 ++++++-- GoogleSignIn/Sources/GIDGoogleUser_Private.h | 40 +- GoogleSignIn/Sources/GIDSignIn.m | 117 ++- .../Sources/GIDSignInInternalOptions.h | 21 +- .../Sources/GIDSignInInternalOptions.m | 16 +- GoogleSignIn/Sources/GIDSignIn_Private.h | 67 +- GoogleSignIn/Sources/GIDToken.m | 96 +++ GoogleSignIn/Sources/GIDToken_Private.h | 32 + GoogleSignIn/Sources/GIDUserAuth.m | 35 + GoogleSignIn/Sources/GIDUserAuth_Private.h | 33 + .../Public/GoogleSignIn/GIDAuthentication.h | 78 -- .../Public/GoogleSignIn/GIDGoogleUser.h | 95 ++- .../Sources/Public/GoogleSignIn/GIDSignIn.h | 64 +- .../Sources/Public/GoogleSignIn/GIDToken.h | 45 ++ .../Sources/Public/GoogleSignIn/GIDUserAuth.h | 40 + .../Public/GoogleSignIn/GoogleSignIn.h | 3 +- .../Tests/Unit/GIDAuthentication+Testing.m | 46 -- .../Tests/Unit/GIDAuthenticationTest.m | 684 ------------------ .../Tests/Unit/GIDConfiguration+Testing.m | 6 + .../Tests/Unit/GIDEMMErrorHandlerTest.m | 1 + GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m | 229 ++++++ .../Tests/Unit/GIDGoogleUser+Testing.h | 9 + .../Tests/Unit/GIDGoogleUser+Testing.m | 49 +- GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m | 508 ++++++++++++- .../Tests/Unit/GIDSignInInternalOptionsTest.m | 11 +- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 114 +-- GoogleSignIn/Tests/Unit/GIDTokenTest.m | 94 +++ .../Tests/Unit/OIDAuthState+Testing.h | 11 + .../Tests/Unit/OIDAuthState+Testing.m | 13 + .../Tests/Unit/OIDTokenResponse+Testing.h | 5 + .../Tests/Unit/OIDTokenResponse+Testing.m | 6 +- .../Source/AuthInspectorViewController.m | 10 +- .../Source/SignInViewController.m | 14 +- .../Shared/Services/BirthdayLoader.swift | 9 +- .../Services/GoogleSignInAuthenticator.swift | 32 +- 41 files changed, 2143 insertions(+), 1499 deletions(-) create mode 100644 GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h create mode 100644 GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.m rename GoogleSignIn/{Tests/Unit/GIDAuthentication+Testing.h => Sources/GIDAuthentication.h} (61%) rename GoogleSignIn/Sources/{GIDAuthentication_Private.h => GIDEMMSupport.h} (63%) create mode 100644 GoogleSignIn/Sources/GIDEMMSupport.m create mode 100644 GoogleSignIn/Sources/GIDToken.m create mode 100644 GoogleSignIn/Sources/GIDToken_Private.h create mode 100644 GoogleSignIn/Sources/GIDUserAuth.m create mode 100644 GoogleSignIn/Sources/GIDUserAuth_Private.h delete mode 100644 GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h create mode 100644 GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h create mode 100644 GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h delete mode 100644 GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.m delete mode 100644 GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m create mode 100644 GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m create mode 100644 GoogleSignIn/Tests/Unit/GIDTokenTest.m diff --git a/GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h b/GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h new file mode 100644 index 00000000..baffe3c1 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#ifdef SWIFT_PACKAGE +@import GTMAppAuth; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +// A specialized GTMAppAuthFetcherAuthorization subclass with EMM support. +@interface GIDAppAuthFetcherAuthorizationWithEMMSupport : GTMAppAuthFetcherAuthorization + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.m b/GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.m new file mode 100644 index 00000000..814a73fc --- /dev/null +++ b/GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.m @@ -0,0 +1,129 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import "GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h" + +#import "GoogleSignIn/Sources/GIDEMMSupport.h" + +#ifdef SWIFT_PACKAGE +@import AppAuth; +@import GTMAppAuth; +#else +#import +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +// The specialized GTMAppAuthFetcherAuthorization delegate that handles potential EMM error +// responses. +@interface GIDAppAuthFetcherAuthorizationEMMChainedDelegate : NSObject + +// Initializes with chained delegate and selector. +- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector; + +// The callback method for GTMAppAuthFetcherAuthorization to invoke. +- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth + request:(NSMutableURLRequest *)request + finishedWithError:(nullable NSError *)error; + +@end + +@implementation GIDAppAuthFetcherAuthorizationEMMChainedDelegate { + // We use a weak reference here to match GTMAppAuthFetcherAuthorization. + __weak id _delegate; + SEL _selector; + // We need to maintain a reference to the chained delegate because GTMAppAuthFetcherAuthorization + // only keeps a weak reference. + GIDAppAuthFetcherAuthorizationEMMChainedDelegate *_retained_self; +} + +- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector { + self = [super init]; + if (self) { + _delegate = delegate; + _selector = selector; + _retained_self = self; + } + return self; +} + +- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth + request:(NSMutableURLRequest *)request + finishedWithError:(nullable NSError *)error { + [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) { + if (!self->_delegate || !self->_selector) { + return; + } + NSMethodSignature *signature = [self->_delegate methodSignatureForSelector:self->_selector]; + if (!signature) { + return; + } + id argument1 = auth; + id argument2 = request; + id argument3 = error; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setTarget:self->_delegate]; // index 0 + [invocation setSelector:self->_selector]; // index 1 + [invocation setArgument:&argument1 atIndex:2]; + [invocation setArgument:&argument2 atIndex:3]; + [invocation setArgument:&argument3 atIndex:4]; + [invocation invoke]; + }]; + // Prepare to deallocate the chained delegate instance because the above block will retain the + // iVar references it uses. + _retained_self = nil; +} + +@end + +@implementation GIDAppAuthFetcherAuthorizationWithEMMSupport + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)authorizeRequest:(nullable NSMutableURLRequest *)request + delegate:(id)delegate + didFinishSelector:(SEL)sel { +#pragma clang diagnostic pop + GIDAppAuthFetcherAuthorizationEMMChainedDelegate *chainedDelegate = + [[GIDAppAuthFetcherAuthorizationEMMChainedDelegate alloc] initWithDelegate:delegate + selector:sel]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [super authorizeRequest:request + delegate:chainedDelegate + didFinishSelector:@selector(authentication:request:finishedWithError:)]; +#pragma clang diagnostic pop +} + +- (void)authorizeRequest:(nullable NSMutableURLRequest *)request + completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)handler { + [super authorizeRequest:request completionHandler:^(NSError *_Nullable error) { + [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) { + handler(error); + }]; + }]; +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h b/GoogleSignIn/Sources/GIDAuthentication.h similarity index 61% rename from GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h rename to GoogleSignIn/Sources/GIDAuthentication.h index 30672902..e38ee151 100644 --- a/GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h +++ b/GoogleSignIn/Sources/GIDAuthentication.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,19 @@ * limitations under the License. */ -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h" +#import -@interface GIDAuthentication (Testing) +@class OIDAuthState; -- (BOOL)isEqual:(id)object; -- (BOOL)isEqualToAuthentication:(GIDAuthentication *)other; -- (NSUInteger)hash; +NS_ASSUME_NONNULL_BEGIN + +// Internal class for GIDGoogleUser NSCoding backward compatibility. +@interface GIDAuthentication : NSObject + +@property(nonatomic) OIDAuthState* authState; + +- (instancetype)initWithAuthState:(OIDAuthState *)authState; @end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthentication.m b/GoogleSignIn/Sources/GIDAuthentication.m index 7965db1e..d8a400f2 100644 --- a/GoogleSignIn/Sources/GIDAuthentication.m +++ b/GoogleSignIn/Sources/GIDAuthentication.m @@ -1,377 +1,41 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h" - -#import "GoogleSignIn/Sources/GIDAuthentication_Private.h" - -#import "GoogleSignIn/Sources/GIDSignInPreferences.h" - -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" -#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h" -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GoogleSignIn/Sources/GIDAuthentication.h" #ifdef SWIFT_PACKAGE @import AppAuth; #else -#import -#import -#import -#import -#import -#import -#import -#import +#import #endif NS_ASSUME_NONNULL_BEGIN -// Minimal time interval before expiration for the access token or it needs to be refreshed. -NSTimeInterval kMinimalTimeToExpire = 60.0; - -// Key constants used for encode and decode. static NSString *const kAuthStateKey = @"authState"; -// Additional parameter names for EMM. -static NSString *const kEMMSupportParameterName = @"emm_support"; -static NSString *const kEMMOSVersionParameterName = @"device_os"; -static NSString *const kEMMPasscodeInfoParameterName = @"emm_passcode_info"; - -// Old UIDevice system name for iOS. -static NSString *const kOldIOSSystemName = @"iPhone OS"; - -// New UIDevice system name for iOS. -static NSString *const kNewIOSSystemName = @"iOS"; - -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - -// The specialized GTMAppAuthFetcherAuthorization delegate that handles potential EMM error -// responses. -@interface GTMAppAuthFetcherAuthorizationEMMChainedDelegate : NSObject - -// Initializes with chained delegate and selector. -- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector; - -// The callback method for GTMAppAuthFetcherAuthorization to invoke. -- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth - request:(NSMutableURLRequest *)request - finishedWithError:(nullable NSError *)error; - -@end - -@implementation GTMAppAuthFetcherAuthorizationEMMChainedDelegate { - // We use a weak reference here to match GTMAppAuthFetcherAuthorization. - __weak id _delegate; - SEL _selector; - // We need to maintain a reference to the chained delegate because GTMAppAuthFetcherAuthorization - // only keeps a weak reference. - GTMAppAuthFetcherAuthorizationEMMChainedDelegate *_retained_self; -} - -- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector { - self = [super init]; - if (self) { - _delegate = delegate; - _selector = selector; - _retained_self = self; - } - return self; -} - -- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth - request:(NSMutableURLRequest *)request - finishedWithError:(nullable NSError *)error { - [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) { - if (!self->_delegate || !self->_selector) { - return; - } - NSMethodSignature *signature = [self->_delegate methodSignatureForSelector:self->_selector]; - if (!signature) { - return; - } - id argument1 = auth; - id argument2 = request; - id argument3 = error; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; - [invocation setTarget:self->_delegate]; // index 0 - [invocation setSelector:self->_selector]; // index 1 - [invocation setArgument:&argument1 atIndex:2]; - [invocation setArgument:&argument2 atIndex:3]; - [invocation setArgument:&argument3 atIndex:4]; - [invocation invoke]; - }]; - // Prepare to deallocate the chained delegate instance because the above block will retain the - // iVar references it uses. - _retained_self = nil; -} - -@end - -// A specialized GTMAppAuthFetcherAuthorization subclass with EMM support. -@interface GTMAppAuthFetcherAuthorizationWithEMMSupport : GTMAppAuthFetcherAuthorization -@end - -@implementation GTMAppAuthFetcherAuthorizationWithEMMSupport - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (void)authorizeRequest:(nullable NSMutableURLRequest *)request - delegate:(id)delegate - didFinishSelector:(SEL)sel { -#pragma clang diagnostic pop - GTMAppAuthFetcherAuthorizationEMMChainedDelegate *chainedDelegate = - [[GTMAppAuthFetcherAuthorizationEMMChainedDelegate alloc] initWithDelegate:delegate - selector:sel]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [super authorizeRequest:request - delegate:chainedDelegate - didFinishSelector:@selector(authentication:request:finishedWithError:)]; -#pragma clang diagnostic pop -} - -- (void)authorizeRequest:(nullable NSMutableURLRequest *)request - completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)handler { - [super authorizeRequest:request completionHandler:^(NSError *_Nullable error) { - [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) { - handler(error); - }]; - }]; -} - -@end - -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - -@implementation GIDAuthentication { - // A queue for pending authentication handlers so we don't fire multiple requests in parallel. - // Access to this ivar should be synchronized. - NSMutableArray *_authenticationHandlerQueue; -} +@implementation GIDAuthentication - (instancetype)initWithAuthState:(OIDAuthState *)authState { - if (!authState) { - return nil; - } self = [super init]; if (self) { - _authenticationHandlerQueue = [[NSMutableArray alloc] init]; _authState = authState; } return self; } -#pragma mark - Public property accessors - -- (NSString *)clientID { - return _authState.lastAuthorizationResponse.request.clientID; -} - -- (NSString *)accessToken { - return _authState.lastTokenResponse.accessToken; -} - -- (NSDate *)accessTokenExpirationDate { - return _authState.lastTokenResponse.accessTokenExpirationDate; -} - -- (NSString *)refreshToken { - return _authState.refreshToken; -} - -- (nullable NSString *)idToken { - return _authState.lastTokenResponse.idToken; -} - -- (nullable NSDate *)idTokenExpirationDate { - return [[[OIDIDToken alloc] initWithIDTokenString:self.idToken] expiresAt]; -} - -#pragma mark - Private property accessors - -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -- (NSString *)emmSupport { - return - _authState.lastAuthorizationResponse.request.additionalParameters[kEMMSupportParameterName]; -} -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - -#pragma mark - Public methods - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -- (id)fetcherAuthorizer { -#pragma clang diagnostic pop -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - GTMAppAuthFetcherAuthorization *authorization = self.emmSupport ? - [[GTMAppAuthFetcherAuthorizationWithEMMSupport alloc] initWithAuthState:_authState] : - [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:_authState]; -#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST - GTMAppAuthFetcherAuthorization *authorization = - [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:_authState]; -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - authorization.tokenRefreshDelegate = self; - return authorization; -} - -- (void)doWithFreshTokens:(GIDAuthenticationCompletion)completion { - if (!([self.accessTokenExpirationDate timeIntervalSinceNow] < kMinimalTimeToExpire || - (self.idToken && [self.idTokenExpirationDate timeIntervalSinceNow] < kMinimalTimeToExpire))) { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(self, nil); - }); - return; - } - @synchronized (_authenticationHandlerQueue) { - // Push the handler into the callback queue. - [_authenticationHandlerQueue addObject:[completion copy]]; - if (_authenticationHandlerQueue.count > 1) { - // This is not the first handler in the queue, no fetch is needed. - return; - } - } - // This is the first handler in the queue, a fetch is needed. - NSMutableDictionary *additionalParameters = [@{} mutableCopy]; -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - [additionalParameters addEntriesFromDictionary: - [GIDAuthentication updatedEMMParametersWithParameters: - _authState.lastTokenResponse.request.additionalParameters]]; -#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST - [additionalParameters addEntriesFromDictionary: - _authState.lastTokenResponse.request.additionalParameters]; -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - additionalParameters[kSDKVersionLoggingParameter] = GIDVersion(); - additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment(); - - OIDTokenRequest *tokenRefreshRequest = - [_authState tokenRefreshRequestWithAdditionalParameters:additionalParameters]; - [OIDAuthorizationService performTokenRequest:tokenRefreshRequest - originalAuthorizationResponse:_authState.lastAuthorizationResponse - callback:^(OIDTokenResponse *_Nullable tokenResponse, - NSError *_Nullable error) { - if (tokenResponse) { - [self willChangeValueForKey:NSStringFromSelector(@selector(accessToken))]; - [self willChangeValueForKey:NSStringFromSelector(@selector(accessTokenExpirationDate))]; - [self willChangeValueForKey:NSStringFromSelector(@selector(idToken))]; - [self willChangeValueForKey:NSStringFromSelector(@selector(idTokenExpirationDate))]; - [self->_authState updateWithTokenResponse:tokenResponse error:nil]; - [self didChangeValueForKey:NSStringFromSelector(@selector(accessToken))]; - [self didChangeValueForKey:NSStringFromSelector(@selector(accessTokenExpirationDate))]; - [self didChangeValueForKey:NSStringFromSelector(@selector(idToken))]; - [self didChangeValueForKey:NSStringFromSelector(@selector(idTokenExpirationDate))]; - } else { - if (error.domain == OIDOAuthTokenErrorDomain) { - [self->_authState updateWithAuthorizationError:error]; - } - } -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) { - // Process the handler queue to call back. - NSArray *authenticationHandlerQueue; - @synchronized(self->_authenticationHandlerQueue) { - authenticationHandlerQueue = [self->_authenticationHandlerQueue copy]; - [self->_authenticationHandlerQueue removeAllObjects]; - } - for (GIDAuthenticationCompletion completion in authenticationHandlerQueue) { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(error ? nil : self, error); - }); - } - }]; -#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST - NSArray *authenticationHandlerQueue; - @synchronized(self->_authenticationHandlerQueue) { - authenticationHandlerQueue = [self->_authenticationHandlerQueue copy]; - [self->_authenticationHandlerQueue removeAllObjects]; - } - for (GIDAuthenticationCompletion completion in authenticationHandlerQueue) { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(error ? nil : self, error); - }); - } -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - }]; -} - -#pragma mark - Private methods - -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - -+ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters - emmSupport:(nullable NSString *)emmSupport - isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired { - if (!emmSupport) { - return parameters; - } - NSMutableDictionary *allParameters = [(parameters ?: @{}) mutableCopy]; - allParameters[kEMMSupportParameterName] = emmSupport; - UIDevice *device = [UIDevice currentDevice]; - NSString *systemName = device.systemName; - if ([systemName isEqualToString:kOldIOSSystemName]) { - systemName = kNewIOSSystemName; - } - allParameters[kEMMOSVersionParameterName] = - [NSString stringWithFormat:@"%@ %@", systemName, device.systemVersion]; - if (isPasscodeInfoRequired) { - allParameters[kEMMPasscodeInfoParameterName] = [GIDMDMPasscodeState passcodeState].info; - } - return allParameters; -} - -+ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters { - return [self parametersWithParameters:parameters - emmSupport:parameters[kEMMSupportParameterName] - isPasscodeInfoRequired:parameters[kEMMPasscodeInfoParameterName] != nil]; -} - -+ (void)handleTokenFetchEMMError:(nullable NSError *)error - completion:(void (^)(NSError *_Nullable))completion { - NSDictionary *errorJSON = error.userInfo[OIDOAuthErrorResponseErrorKey]; - if (errorJSON) { - __block BOOL handled = NO; - handled = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:errorJSON - completion:^() { - if (handled) { - completion([NSError errorWithDomain:kGIDSignInErrorDomain - code:kGIDSignInErrorCodeEMM - userInfo:error.userInfo]); - } else { - completion(error); - } - }]; - } else { - completion(error); - } -} - -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - -#pragma mark - GTMAppAuthFetcherAuthorizationTokenRefreshDelegate - -- (nullable NSDictionary *)additionalRefreshParameters: - (GTMAppAuthFetcherAuthorization *)authorization { -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - return [GIDAuthentication updatedEMMParametersWithParameters: - authorization.authState.lastTokenResponse.request.additionalParameters]; -#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST - return authorization.authState.lastTokenResponse.request.additionalParameters; -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST -} - #pragma mark - NSSecureCoding + (BOOL)supportsSecureCoding { @@ -381,14 +45,13 @@ + (BOOL)supportsSecureCoding { - (nullable instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; if (self) { - _authenticationHandlerQueue = [[NSMutableArray alloc] init]; _authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthStateKey]; } return self; } - (void)encodeWithCoder:(NSCoder *)encoder { - [encoder encodeObject:_authState forKey:kAuthStateKey]; + [encoder encodeObject:self.authState forKey:kAuthStateKey]; } @end diff --git a/GoogleSignIn/Sources/GIDAuthentication_Private.h b/GoogleSignIn/Sources/GIDEMMSupport.h similarity index 63% rename from GoogleSignIn/Sources/GIDAuthentication_Private.h rename to GoogleSignIn/Sources/GIDEMMSupport.h index c967a0da..d6f4e92e 100644 --- a/GoogleSignIn/Sources/GIDAuthentication_Private.h +++ b/GoogleSignIn/Sources/GIDEMMSupport.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,45 +14,31 @@ * limitations under the License. */ -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h" +#import -#ifdef SWIFT_PACKAGE -@import AppAuth; -@import GTMAppAuth; -#else -#import -#import -#endif +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -NS_ASSUME_NONNULL_BEGIN +#import -// Internal methods for the class that are not part of the public API. -@interface GIDAuthentication () +NS_ASSUME_NONNULL_BEGIN -// A representation of the state of the OAuth session for this instance. -@property(nonatomic, readonly) OIDAuthState *authState; +// A class to support EMM (Enterprise Mobility Management). +@interface GIDEMMSupport : NSObject -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -// A string indicating support for Enterprise Mobility Management. -@property(nonatomic, readonly) NSString *emmSupport; -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST +// Handles potential EMM error from token fetch response. ++ (void)handleTokenFetchEMMError:(nullable NSError *)error + completion:(void (^)(NSError *_Nullable))completion; -- (instancetype)initWithAuthState:(OIDAuthState *)authState; +// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed. ++ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters; -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST // Gets a new set of URL parameters that also contains EMM-related URL parameters if needed. + (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters emmSupport:(nullable NSString *)emmSupport isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired; -// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed. -+ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters; - -// Handles potential EMM error from token fetch response. -+ (void)handleTokenFetchEMMError:(nullable NSError *)error - completion:(void (^)(NSError *_Nullable))completion; -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - @end NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDEMMSupport.m b/GoogleSignIn/Sources/GIDEMMSupport.m new file mode 100644 index 00000000..a796f5fb --- /dev/null +++ b/GoogleSignIn/Sources/GIDEMMSupport.m @@ -0,0 +1,101 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import "GoogleSignIn/Sources/GIDEMMSupport.h" + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" + +#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" +#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h" + +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +// Additional parameter names for EMM. +static NSString *const kEMMSupportParameterName = @"emm_support"; +static NSString *const kEMMOSVersionParameterName = @"device_os"; +static NSString *const kEMMPasscodeInfoParameterName = @"emm_passcode_info"; + +// Old UIDevice system name for iOS. +static NSString *const kOldIOSSystemName = @"iPhone OS"; + +// New UIDevice system name for iOS. +static NSString *const kNewIOSSystemName = @"iOS"; + +@implementation GIDEMMSupport + ++ (void)handleTokenFetchEMMError:(nullable NSError *)error + completion:(void (^)(NSError *_Nullable))completion { + NSDictionary *errorJSON = error.userInfo[OIDOAuthErrorResponseErrorKey]; + if (errorJSON) { + __block BOOL handled = NO; + handled = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:errorJSON + completion:^() { + if (handled) { + completion([NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeEMM + userInfo:error.userInfo]); + } else { + completion(error); + } + }]; + } else { + completion(error); + } +} + ++ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters { + return [self parametersWithParameters:parameters + emmSupport:parameters[kEMMSupportParameterName] + isPasscodeInfoRequired:parameters[kEMMPasscodeInfoParameterName] != nil]; +} + + ++ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters + emmSupport:(nullable NSString *)emmSupport + isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired { + if (!emmSupport) { + return parameters; + } + NSMutableDictionary *allParameters = [(parameters ?: @{}) mutableCopy]; + allParameters[kEMMSupportParameterName] = emmSupport; + UIDevice *device = [UIDevice currentDevice]; + NSString *systemName = device.systemName; + if ([systemName isEqualToString:kOldIOSSystemName]) { + systemName = kNewIOSSystemName; + } + allParameters[kEMMOSVersionParameterName] = + [NSString stringWithFormat:@"%@ %@", systemName, device.systemVersion]; + if (isPasscodeInfoRequired) { + allParameters[kEMMPasscodeInfoParameterName] = [GIDMDMPasscodeState passcodeState].info; + } + return allParameters; +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDGoogleUser.m b/GoogleSignIn/Sources/GIDGoogleUser.m index 34e636d6..4291ba38 100644 --- a/GoogleSignIn/Sources/GIDGoogleUser.m +++ b/GoogleSignIn/Sources/GIDGoogleUser.m @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,10 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h" + #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" -#import "GoogleSignIn/Sources/GIDAuthentication_Private.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" + +#import "GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h" +#import "GoogleSignIn/Sources/GIDAuthentication.h" +#import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDProfileData_Private.h" +#import "GoogleSignIn/Sources/GIDSignIn_Private.h" +#import "GoogleSignIn/Sources/GIDSignInPreferences.h" +#import "GoogleSignIn/Sources/GIDToken_Private.h" #ifdef SWIFT_PACKAGE @import AppAuth; @@ -29,57 +39,41 @@ static NSString *const kHostedDomainIDTokenClaimKey = @"hd"; // Key constants used for encode and decode. -static NSString *const kAuthenticationKey = @"authentication"; static NSString *const kProfileDataKey = @"profileData"; -static NSString *const kAuthState = @"authState"; +static NSString *const kAuthStateKey = @"authState"; // Parameters for the token exchange endpoint. static NSString *const kAudienceParameter = @"audience"; static NSString *const kOpenIDRealmParameter = @"openid.realm"; +// Additional parameter names for EMM. +static NSString *const kEMMSupportParameterName = @"emm_support"; + +// Minimal time interval before expiration for the access token or it needs to be refreshed. +static NSTimeInterval const kMinimalTimeToExpire = 60.0; + @implementation GIDGoogleUser { - OIDAuthState *_authState; + GIDConfiguration *_cachedConfiguration; + + // A queue for pending token refresh handlers so we don't fire multiple requests in parallel. + // Access to this ivar should be synchronized. + NSMutableArray *_tokenRefreshHandlerQueue; } - (nullable NSString *)userID { - NSString *idToken = [self idToken]; - if (idToken) { - OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken]; + NSString *idTokenString = self.idToken.tokenString; + if (idTokenString) { + OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString]; if (idTokenDecoded && idTokenDecoded.subject) { return [idTokenDecoded.subject copy]; } } - return nil; } -- (nullable NSString *)hostedDomain { - NSString *idToken = [self idToken]; - if (idToken) { - OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken]; - if (idTokenDecoded && idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) { - return [idTokenDecoded.claims[kHostedDomainIDTokenClaimKey] copy]; - } - } - - return nil; -} - -- (nullable NSString *)serverAuthCode { - return [_authState.lastTokenResponse.additionalParameters[@"server_code"] copy]; -} - -- (nullable NSString *)serverClientID { - return [_authState.lastTokenResponse.request.additionalParameters[kAudienceParameter] copy]; -} - -- (nullable NSString *)openIDRealm { - return [_authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter] copy]; -} - - (nullable NSArray *)grantedScopes { NSArray *grantedScopes; - NSString *grantedScopeString = _authState.lastTokenResponse.scope; + NSString *grantedScopeString = self.authState.lastTokenResponse.scope; if (grantedScopeString) { // If we have a 'scope' parameter from the backend, this is authoritative. // Remove leading and trailing whitespace. @@ -95,28 +89,237 @@ - (nullable NSString *)openIDRealm { return grantedScopes; } +- (GIDConfiguration *)configuration { + @synchronized(self) { + // Caches the configuration since it would not change for one GIDGoogleUser instance. + if (!_cachedConfiguration) { + NSString *clientID = self.authState.lastAuthorizationResponse.request.clientID; + NSString *serverClientID = + self.authState.lastTokenResponse.request.additionalParameters[kAudienceParameter]; + NSString *openIDRealm = + self.authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter]; + + _cachedConfiguration = [[GIDConfiguration alloc] initWithClientID:clientID + serverClientID:serverClientID + hostedDomain:[self hostedDomain] + openIDRealm:openIDRealm]; + }; + } + return _cachedConfiguration; +} + +- (void)refreshTokensIfNeededWithCompletion:(GIDGoogleUserCompletion)completion { + if (!([self.accessToken.expirationDate timeIntervalSinceNow] < kMinimalTimeToExpire || + (self.idToken && [self.idToken.expirationDate timeIntervalSinceNow] < kMinimalTimeToExpire))) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(self, nil); + }); + return; + } + @synchronized (_tokenRefreshHandlerQueue) { + // Push the handler into the callback queue. + [_tokenRefreshHandlerQueue addObject:[completion copy]]; + if (_tokenRefreshHandlerQueue.count > 1) { + // This is not the first handler in the queue, no fetch is needed. + return; + } + } + // This is the first handler in the queue, a fetch is needed. + NSMutableDictionary *additionalParameters = [@{} mutableCopy]; +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + [additionalParameters addEntriesFromDictionary: + [GIDEMMSupport updatedEMMParametersWithParameters: + self.authState.lastTokenResponse.request.additionalParameters]]; +#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST + [additionalParameters addEntriesFromDictionary: + self.authState.lastTokenResponse.request.additionalParameters]; +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + additionalParameters[kSDKVersionLoggingParameter] = GIDVersion(); + additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment(); + + OIDTokenRequest *tokenRefreshRequest = + [self.authState tokenRefreshRequestWithAdditionalParameters:additionalParameters]; + [OIDAuthorizationService performTokenRequest:tokenRefreshRequest + originalAuthorizationResponse:self.authState.lastAuthorizationResponse + callback:^(OIDTokenResponse *_Nullable tokenResponse, + NSError *_Nullable error) { + if (tokenResponse) { + [self.authState updateWithTokenResponse:tokenResponse error:nil]; + } else { + if (error.domain == OIDOAuthTokenErrorDomain) { + [self.authState updateWithAuthorizationError:error]; + } + } +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) { + // Process the handler queue to call back. + NSArray *refreshTokensHandlerQueue; + @synchronized(self->_tokenRefreshHandlerQueue) { + refreshTokensHandlerQueue = [self->_tokenRefreshHandlerQueue copy]; + [self->_tokenRefreshHandlerQueue removeAllObjects]; + } + for (GIDGoogleUserCompletion completion in refreshTokensHandlerQueue) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(error ? nil : self, error); + }); + } + }]; +#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST + NSArray *refreshTokensHandlerQueue; + @synchronized(self->_tokenRefreshHandlerQueue) { + refreshTokensHandlerQueue = [self->_tokenRefreshHandlerQueue copy]; + [self->_tokenRefreshHandlerQueue removeAllObjects]; + } + for (GIDGoogleUserCompletion completion in refreshTokensHandlerQueue) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(error ? nil : self, error); + }); + } +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + }]; +} + +- (OIDAuthState *) authState{ + return ((GTMAppAuthFetcherAuthorization *)self.fetcherAuthorizer).authState; +} + +- (void)addScopes:(NSArray *)scopes +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + presentingViewController:(UIViewController *)presentingViewController +#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST + presentingWindow:(NSWindow *)presentingWindow +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion { + if (self != GIDSignIn.sharedInstance.currentUser) { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeMismatchWithCurrentUser + userInfo:nil]; + if (completion) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(nil, error); + }); + } + return; + } + + [GIDSignIn.sharedInstance addScopes:scopes +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + presentingViewController:presentingViewController +#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST + presentingWindow:presentingWindow +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + completion:completion]; +} + #pragma mark - Private Methods +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (nullable NSString *)emmSupport { + return self.authState.lastAuthorizationResponse + .request.additionalParameters[kEMMSupportParameterName]; +} +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + - (instancetype)initWithAuthState:(OIDAuthState *)authState profileData:(nullable GIDProfileData *)profileData { self = [super init]; if (self) { - [self updateAuthState:authState profileData:profileData]; + _tokenRefreshHandlerQueue = [[NSMutableArray alloc] init]; + _profile = profileData; + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + GTMAppAuthFetcherAuthorization *authorization = self.emmSupport ? + [[GIDAppAuthFetcherAuthorizationWithEMMSupport alloc] initWithAuthState:authState] : + [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState]; +#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST + GTMAppAuthFetcherAuthorization *authorization = + [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState]; +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + authorization.tokenRefreshDelegate = self; + authorization.authState.stateChangeDelegate = self; + self.fetcherAuthorizer = authorization; + + [self updateTokensWithAuthState:authState]; } return self; } -- (void)updateAuthState:(OIDAuthState *)authState - profileData:(nullable GIDProfileData *)profileData { - _authState = authState; - _authentication = [[GIDAuthentication alloc] initWithAuthState:authState]; - _profile = profileData; +- (void)updateWithTokenResponse:(OIDTokenResponse *)tokenResponse + authorizationResponse:(OIDAuthorizationResponse *)authorizationResponse + profileData:(nullable GIDProfileData *)profileData { + @synchronized(self) { + _profile = profileData; + + // We don't want to trigger the delegate before we update authState completely. So we unset the + // delegate before the first update. Also the order of updates is important because + // `updateWithAuthorizationResponse` would clear the last token reponse and refresh token. + // TODO: Rewrite authState update logic when the issue is addressed.(openid/AppAuth-iOS#728) + self.authState.stateChangeDelegate = nil; + [self.authState updateWithAuthorizationResponse:authorizationResponse error:nil]; + self.authState.stateChangeDelegate = self; + [self.authState updateWithTokenResponse:tokenResponse error:nil]; + } +} + +- (void)updateTokensWithAuthState:(OIDAuthState *)authState { + GIDToken *accessToken = + [[GIDToken alloc] initWithTokenString:authState.lastTokenResponse.accessToken + expirationDate:authState.lastTokenResponse.accessTokenExpirationDate]; + if (![self.accessToken isEqualToToken:accessToken]) { + self.accessToken = accessToken; + } + + GIDToken *refreshToken = [[GIDToken alloc] initWithTokenString:authState.refreshToken + expirationDate:nil]; + if (![self.refreshToken isEqualToToken:refreshToken]) { + self.refreshToken = refreshToken; + } + + GIDToken *idToken; + NSString *idTokenString = authState.lastTokenResponse.idToken; + if (idTokenString) { + NSDate *idTokenExpirationDate = + [[[OIDIDToken alloc] initWithIDTokenString:idTokenString] expiresAt]; + idToken = [[GIDToken alloc] initWithTokenString:idTokenString + expirationDate:idTokenExpirationDate]; + } else { + idToken = nil; + } + if ((self.idToken || idToken) && ![self.idToken isEqualToToken:idToken]) { + self.idToken = idToken; + } } #pragma mark - Helpers -- (NSString *)idToken { - return _authState ? _authState.lastTokenResponse.idToken : nil; +- (nullable NSString *)hostedDomain { + NSString *idTokenString = self.idToken.tokenString; + if (idTokenString) { + OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString]; + if (idTokenDecoded && idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) { + return idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]; + } + } + return nil; +} + +#pragma mark - GTMAppAuthFetcherAuthorizationTokenRefreshDelegate + +- (nullable NSDictionary *)additionalRefreshParameters: + (GTMAppAuthFetcherAuthorization *)authorization { +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + return [GIDEMMSupport updatedEMMParametersWithParameters: + authorization.authState.lastTokenResponse.request.additionalParameters]; +#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST + return authorization.authState.lastTokenResponse.request.additionalParameters; +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST +} + +#pragma mark - OIDAuthStateChangeDelegate + +- (void)didChangeState:(OIDAuthState *)state { + [self updateTokensWithAuthState:state]; } #pragma mark - NSSecureCoding @@ -128,22 +331,26 @@ + (BOOL)supportsSecureCoding { - (nullable instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; if (self) { - _profile = [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey]; - if ([decoder containsValueForKey:kAuthState]) { // Current encoding - _authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState]; + GIDProfileData *profile = + [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey]; + + OIDAuthState *authState; + if ([decoder containsValueForKey:kAuthStateKey]) { // Current encoding + authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthStateKey]; } else { // Old encoding GIDAuthentication *authentication = [decoder decodeObjectOfClass:[GIDAuthentication class] - forKey:kAuthenticationKey]; - _authState = authentication.authState; + forKey:@"authentication"]; + authState = authentication.authState; } - _authentication = [[GIDAuthentication alloc] initWithAuthState:_authState]; + + self = [self initWithAuthState:authState profileData:profile]; } return self; } - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:_profile forKey:kProfileDataKey]; - [encoder encodeObject:_authState forKey:kAuthState]; + [encoder encodeObject:self.authState forKey:kAuthStateKey]; } @end diff --git a/GoogleSignIn/Sources/GIDGoogleUser_Private.h b/GoogleSignIn/Sources/GIDGoogleUser_Private.h index c6a1fc84..94a019ae 100644 --- a/GoogleSignIn/Sources/GIDGoogleUser_Private.h +++ b/GoogleSignIn/Sources/GIDGoogleUser_Private.h @@ -16,20 +16,52 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h" -NS_ASSUME_NONNULL_BEGIN +#ifdef SWIFT_PACKAGE +@import AppAuth; +@import GTMAppAuth; +#else +#import +#import +#endif @class OIDAuthState; +NS_ASSUME_NONNULL_BEGIN + +/// A completion block that takes a `GIDGoogleUser` or an error if the attempt to refresh tokens was unsuccessful. +typedef void (^GIDGoogleUserCompletion)(GIDGoogleUser *_Nullable user, NSError *_Nullable error); + // Internal methods for the class that are not part of the public API. -@interface GIDGoogleUser () +@interface GIDGoogleUser () + +@property(nonatomic, readwrite) GIDToken *accessToken; + +@property(nonatomic, readwrite) GIDToken *refreshToken; + +@property(nonatomic, readwrite, nullable) GIDToken *idToken; + +// A representation of the state of the OAuth session for this instance. +@property(nonatomic, readonly) OIDAuthState *authState; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +@property(nonatomic, readwrite) id fetcherAuthorizer; +#pragma clang diagnostic pop + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +// A string indicating support for Enterprise Mobility Management. +@property(nonatomic, readonly, nullable) NSString *emmSupport; +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST // Create a object with an auth state, scopes, and profile data. - (instancetype)initWithAuthState:(OIDAuthState *)authState profileData:(nullable GIDProfileData *)profileData; // Update the auth state and profile data. -- (void)updateAuthState:(OIDAuthState *)authState - profileData:(nullable GIDProfileData *)profileData; +- (void)updateWithTokenResponse:(OIDTokenResponse *)tokenResponse + authorizationResponse:(OIDAuthorizationResponse *)authorizationResponse + profileData:(nullable GIDProfileData *)profileData; @end diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index f7e6d04c..b388bf56 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -16,11 +16,12 @@ #import "GoogleSignIn/Sources/GIDSignIn_Private.h" -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h" +#import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" #import "GoogleSignIn/Sources/GIDCallbackQueue.h" @@ -31,9 +32,9 @@ #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST -#import "GoogleSignIn/Sources/GIDAuthentication_Private.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" #import "GoogleSignIn/Sources/GIDProfileData_Private.h" +#import "GoogleSignIn/Sources/GIDUserAuth_Private.h" #ifdef SWIFT_PACKAGE @import AppAuth; @@ -186,15 +187,23 @@ - (BOOL)handleURL:(NSURL *)url { } - (BOOL)hasPreviousSignIn { - if ([_currentUser.authentication.authState isAuthorized]) { + if ([_currentUser.authState isAuthorized]) { return YES; } OIDAuthState *authState = [self loadAuthState]; return [authState isAuthorized]; } -- (void)restorePreviousSignInWithCompletion:(nullable GIDSignInCompletion)completion { - [self signInWithOptions:[GIDSignInInternalOptions silentOptionsWithCompletion:completion]]; +- (void)restorePreviousSignInWithCompletion:(nullable void (^)(GIDGoogleUser *_Nullable user, + NSError *_Nullable error))completion { + [self signInWithOptions:[GIDSignInInternalOptions silentOptionsWithCompletion: + ^(GIDUserAuth *userAuth, NSError *error) { + if (userAuth) { + completion(userAuth.user, nil); + } else { + completion(nil, error); + } + }]]; } - (BOOL)restorePreviousSignInNoRefresh { @@ -222,7 +231,7 @@ - (BOOL)restorePreviousSignInNoRefresh { - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable GIDUserAuthCompletion)completion { GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration presentingViewController:presentingViewController @@ -235,7 +244,7 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable GIDUserAuthCompletion)completion { GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration presentingViewController:presentingViewController @@ -247,7 +256,7 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon } - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable GIDUserAuthCompletion)completion { [self signInWithPresentingViewController:presentingViewController hint:nil completion:completion]; @@ -255,26 +264,8 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon - (void)addScopes:(NSArray *)scopes presentingViewController:(UIViewController *)presentingViewController - completion:(nullable GIDSignInCompletion)completion { - // A currentUser must be available in order to complete this flow. - if (!self.currentUser) { - // No currentUser is set, notify callback of failure. - NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain - code:kGIDSignInErrorCodeNoCurrentUser - userInfo:nil]; - if (completion) { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(nil, error); - }); - } - return; - } - - GIDConfiguration *configuration = - [[GIDConfiguration alloc] initWithClientID:self.currentUser.authentication.clientID - serverClientID:self.currentUser.serverClientID - hostedDomain:self.currentUser.hostedDomain - openIDRealm:self.currentUser.openIDRealm]; + completion:(nullable GIDUserAuthCompletion)completion { + GIDConfiguration *configuration = self.currentUser.configuration; GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration presentingViewController:presentingViewController @@ -311,7 +302,7 @@ - (void)addScopes:(NSArray *)scopes - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable GIDUserAuthCompletion)completion { GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration presentingWindow:presentingWindow @@ -322,7 +313,7 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow } - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable GIDUserAuthCompletion)completion { [self signInWithPresentingWindow:presentingWindow hint:nil completion:completion]; @@ -331,7 +322,7 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable GIDUserAuthCompletion)completion { GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration presentingWindow:presentingWindow @@ -344,26 +335,8 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow - (void)addScopes:(NSArray *)scopes presentingWindow:(NSWindow *)presentingWindow - completion:(nullable GIDSignInCompletion)completion { - // A currentUser must be available in order to complete this flow. - if (!self.currentUser) { - // No currentUser is set, notify callback of failure. - NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain - code:kGIDSignInErrorCodeNoCurrentUser - userInfo:nil]; - if (completion) { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(nil, error); - }); - } - return; - } - - GIDConfiguration *configuration = - [[GIDConfiguration alloc] initWithClientID:self.currentUser.authentication.clientID - serverClientID:self.currentUser.serverClientID - hostedDomain:self.currentUser.hostedDomain - openIDRealm:self.currentUser.openIDRealm]; + completion:(nullable GIDUserAuthCompletion)completion { + GIDConfiguration *configuration = self.currentUser.configuration; GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration presentingWindow:presentingWindow @@ -408,8 +381,7 @@ - (void)signOut { } - (void)disconnectWithCompletion:(nullable GIDDisconnectCompletion)completion { - GIDGoogleUser *user = _currentUser; - OIDAuthState *authState = user.authentication.authState; + OIDAuthState *authState = _currentUser.authState; if (!authState) { // Even the user is not signed in right now, we still need to remove any token saved in the // keychain. @@ -545,15 +517,16 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { } // If this is a non-interactive flow, use cached authentication if possible. - if (!options.interactive && _currentUser.authentication) { - [_currentUser.authentication doWithFreshTokens:^(GIDAuthentication *unused, NSError *error) { + if (!options.interactive && _currentUser) { + [_currentUser refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *unused, NSError *error) { if (error) { [self authenticateWithOptions:options]; } else { if (options.completion) { self->_currentOptions = nil; dispatch_async(dispatch_get_main_queue(), ^{ - options.completion(self->_currentUser, nil); + GIDUserAuth *userAuth = [[GIDUserAuth alloc] initWithGoogleUser:self->_currentUser serverAuthCode:nil]; + options.completion(userAuth, nil); }); } } @@ -592,9 +565,9 @@ - (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST [additionalParameters addEntriesFromDictionary: - [GIDAuthentication parametersWithParameters:options.extraParams - emmSupport:emmSupport - isPasscodeInfoRequired:NO]]; + [GIDEMMSupport parametersWithParameters:options.extraParams + emmSupport:emmSupport + isPasscodeInfoRequired:NO]]; #elif TARGET_OS_OSX || TARGET_OS_MACCATALYST [additionalParameters addEntriesFromDictionary:options.extraParams]; #endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST @@ -741,9 +714,9 @@ - (void)maybeFetchToken:(GIDAuthFlow *)authFlow { authState.lastAuthorizationResponse.additionalParameters; NSString *passcodeInfoRequired = (NSString *)params[kEMMPasscodeInfoRequiredKeyName]; [additionalParameters addEntriesFromDictionary: - [GIDAuthentication parametersWithParameters:@{} - emmSupport:authFlow.emmSupport - isPasscodeInfoRequired:passcodeInfoRequired.length > 0]]; + [GIDEMMSupport parametersWithParameters:@{} + emmSupport:authFlow.emmSupport + isPasscodeInfoRequired:passcodeInfoRequired.length > 0]]; #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST additionalParameters[kSDKVersionLoggingParameter] = GIDVersion(); additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment(); @@ -769,7 +742,7 @@ - (void)maybeFetchToken:(GIDAuthFlow *)authFlow { #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST if (authFlow.emmSupport) { - [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *error) { + [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *error) { authFlow.error = error; [authFlow next]; }]; @@ -796,8 +769,9 @@ - (void)addSaveAuthCallback:(GIDAuthFlow *)authFlow { } if (self->_currentOptions.addScopesFlow) { - [self->_currentUser updateAuthState:authState - profileData:handlerAuthFlow.profileData]; + [self->_currentUser updateWithTokenResponse:authState.lastTokenResponse + authorizationResponse:authState.lastAuthorizationResponse + profileData:handlerAuthFlow.profileData]; } else { GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState profileData:handlerAuthFlow.profileData]; @@ -865,10 +839,19 @@ - (void)addCompletionCallback:(GIDAuthFlow *)authFlow { [authFlow addCallback:^() { GIDAuthFlow *handlerAuthFlow = weakAuthFlow; if (self->_currentOptions.completion) { - GIDSignInCompletion completion = self->_currentOptions.completion; + GIDUserAuthCompletion completion = self->_currentOptions.completion; self->_currentOptions = nil; dispatch_async(dispatch_get_main_queue(), ^{ - completion(self->_currentUser, handlerAuthFlow.error); + if (handlerAuthFlow.error) { + completion(nil, handlerAuthFlow.error); + } else { + OIDAuthState *authState = handlerAuthFlow.authState; + NSString *_Nullable serverAuthCode = + [authState.lastTokenResponse.additionalParameters[@"server_code"] copy]; + GIDUserAuth *userAuth = [[GIDUserAuth alloc] initWithGoogleUser:self->_currentUser + serverAuthCode:serverAuthCode]; + completion(userAuth, nil); + } }); } }]; diff --git a/GoogleSignIn/Sources/GIDSignInInternalOptions.h b/GoogleSignIn/Sources/GIDSignInInternalOptions.h index d104246e..a51bfd95 100644 --- a/GoogleSignIn/Sources/GIDSignInInternalOptions.h +++ b/GoogleSignIn/Sources/GIDSignInInternalOptions.h @@ -22,9 +22,8 @@ #import #endif -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" - @class GIDConfiguration; +@class GIDUserAuth; NS_ASSUME_NONNULL_BEGIN @@ -55,7 +54,8 @@ NS_ASSUME_NONNULL_BEGIN #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST /// The completion block to be called at the completion of the flow. -@property(nonatomic, readonly, nullable) GIDSignInCompletion completion; +@property(nonatomic, readonly, nullable) void (^completion)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error); /// The scopes to be used during the flow. @property(nonatomic, copy, nullable) NSArray *scopes; @@ -69,32 +69,37 @@ NS_ASSUME_NONNULL_BEGIN presentingViewController:(nullable UIViewController *)presentingViewController loginHint:(nullable NSString *)loginHint addScopesFlow:(BOOL)addScopesFlow - completion:(nullable GIDSignInCompletion)completion; + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration presentingViewController:(nullable UIViewController *)presentingViewController loginHint:(nullable NSString *)loginHint addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes - completion:(nullable GIDSignInCompletion)completion; + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; #elif TARGET_OS_OSX + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration presentingWindow:(nullable NSWindow *)presentingWindow loginHint:(nullable NSString *)loginHint addScopesFlow:(BOOL)addScopesFlow - completion:(nullable GIDSignInCompletion)completion; + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration presentingWindow:(nullable NSWindow *)presentingWindow loginHint:(nullable NSString *)loginHint addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes - completion:(nullable GIDSignInCompletion)completion; + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST /// Creates the options to sign in silently. -+ (instancetype)silentOptionsWithCompletion:(GIDSignInCompletion)completion; ++ (instancetype)silentOptionsWithCompletion:(void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; /// Creates options with the same values as the receiver, except for the "extra parameters", and /// continuation flag, which are replaced by the arguments passed to this method. diff --git a/GoogleSignIn/Sources/GIDSignInInternalOptions.m b/GoogleSignIn/Sources/GIDSignInInternalOptions.m index 06db24ab..dfa39aa1 100644 --- a/GoogleSignIn/Sources/GIDSignInInternalOptions.m +++ b/GoogleSignIn/Sources/GIDSignInInternalOptions.m @@ -31,16 +31,17 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con loginHint:(nullable NSString *)loginHint addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion { #elif TARGET_OS_OSX + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration presentingWindow:(nullable NSWindow *)presentingWindow loginHint:(nullable NSString *)loginHint addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion { #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST - GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init]; if (options) { options->_interactive = YES; @@ -64,13 +65,15 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con presentingViewController:(nullable UIViewController *)presentingViewController loginHint:(nullable NSString *)loginHint addScopesFlow:(BOOL)addScopesFlow - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion { #elif TARGET_OS_OSX + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration presentingWindow:(nullable NSWindow *)presentingWindow loginHint:(nullable NSString *)loginHint addScopesFlow:(BOOL)addScopesFlow - completion:(nullable GIDSignInCompletion)completion { + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion { #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST GIDSignInInternalOptions *options = [self defaultOptionsWithConfiguration:configuration #if TARGET_OS_IOS || TARGET_OS_MACCATALYST @@ -85,7 +88,8 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con return options; } -+ (instancetype)silentOptionsWithCompletion:(GIDSignInCompletion)completion { ++ (instancetype)silentOptionsWithCompletion:(void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion { GIDSignInInternalOptions *options = [self defaultOptionsWithConfiguration:nil #if TARGET_OS_IOS || TARGET_OS_MACCATALYST presentingViewController:nil diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 74b4b636..39df848f 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -14,32 +14,83 @@ * limitations under the License. */ +#import + #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#if __has_include() +#import +#elif __has_include() +#import +#endif + NS_ASSUME_NONNULL_BEGIN @class GIDGoogleUser; @class GIDSignInInternalOptions; +/// Represents a completion block that takes a `GIDUserAuth` on success or an error if the operation +/// was unsuccessful. +typedef void (^GIDUserAuthCompletion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error); + // Private |GIDSignIn| methods that are used internally in this SDK and other Google SDKs. @interface GIDSignIn () -// Redeclare |currentUser| as readwrite for internal use. +/// Redeclare |currentUser| as readwrite for internal use. @property(nonatomic, readwrite, nullable) GIDGoogleUser *currentUser; -// Private initializer for |GIDSignIn|. +/// Private initializer for |GIDSignIn|. - (instancetype)initPrivate; -// Authenticates with extra options. +/// Authenticates with extra options. - (void)signInWithOptions:(GIDSignInInternalOptions *)options; -// Restores a previously authenticated user from the keychain synchronously without refreshing -// the access token or making a userinfo request. The currentUser.profile will be nil unless -// the profile data can be extracted from the ID token. -// -// @return NO if there is no user restored from the keychain. +/// Restores a previously authenticated user from the keychain synchronously without refreshing +/// the access token or making a userinfo request. +/// +/// The currentUser.profile will be nil unless the profile data can be extracted from the ID token. +/// +/// @return NO if there is no user restored from the keychain. - (BOOL)restorePreviousSignInNoRefresh; +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + +/// Starts an interactive consent flow on iOS to add scopes to the current user's grants. +/// +/// The completion will be called at the end of this process. If successful, a `GIDUserAuth` +/// instance will be returned reflecting the new scopes and saved sign-in state will be updated. +/// +/// @param scopes The scopes to ask the user to consent to. +/// @param presentingViewController The view controller used to present `SFSafariViewContoller` on +/// iOS 9 and 10 and to supply `presentationContextProvider` for `ASWebAuthenticationSession` on +/// iOS 13+. +/// @param completion The block that is called on completion. This block will be called asynchronously +/// on the main queue. +- (void)addScopes:(NSArray *)scopes + presentingViewController:(UIViewController *)presentingViewController + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion + NS_EXTENSION_UNAVAILABLE("The add scopes flow is not supported in App Extensions."); + +#elif TARGET_OS_OSX + +/// Starts an interactive consent flow on macOS to add scopes to the current user's grants. +/// +/// The completion will be called at the end of this process. If successful, a `GIDUserAuth` +/// instance will be returned reflecting the new scopes and saved sign-in state will be updated. +/// +/// @param scopes An array of scopes to ask the user to consent to. +/// @param presentingWindow The window used to supply `presentationContextProvider` for +/// `ASWebAuthenticationSession`. +/// @param completion The block that is called on completion. This block will be called asynchronously +/// on the main queue. +- (void)addScopes:(NSArray *)scopes + presentingWindow:(NSWindow *)presentingWindow + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; + +#endif + @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDToken.m b/GoogleSignIn/Sources/GIDToken.m new file mode 100644 index 00000000..2702a952 --- /dev/null +++ b/GoogleSignIn/Sources/GIDToken.m @@ -0,0 +1,96 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" + +#import "GoogleSignIn/Sources/GIDToken_Private.h" + +// Key constants used for encode and decode. +static NSString *const kTokenStringKey = @"tokenString"; +static NSString *const kExpirationDateKey = @"expirationDate"; + +NS_ASSUME_NONNULL_BEGIN + +@implementation GIDToken + +- (instancetype)initWithTokenString:(NSString *)tokenString + expirationDate:(nullable NSDate *)expirationDate { + self = [super init]; + if (self) { + _tokenString = [tokenString copy]; + _expirationDate = expirationDate; + } + + return self; +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)decoder { + self = [super init]; + if (self) { + _tokenString = [decoder decodeObjectOfClass:[NSString class] forKey:kTokenStringKey]; + _expirationDate = [decoder decodeObjectOfClass:[NSDate class] forKey:kExpirationDateKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:_tokenString forKey:kTokenStringKey]; + [encoder encodeObject:_expirationDate forKey:kExpirationDateKey]; +} + +#pragma mark - isEqual + +- (BOOL)isEqual:(nullable id)object { + if (object == nil) { + return NO; + } + if (self == object) { + return YES; + } + if (![object isKindOfClass:[GIDToken class]]) { + return NO; + } + return [self isEqualToToken:(GIDToken *)object]; +} + +- (BOOL)isEqualToToken:(GIDToken *)otherToken { + return [_tokenString isEqual:otherToken.tokenString] && + [self isTheSameDate:_expirationDate with:otherToken.expirationDate]; +} + +// The date is nullable in GIDToken. Two `nil` dates are considered equal so +// token equality check succeeds if token strings are equal and have no expiration. +- (BOOL)isTheSameDate:(nullable NSDate *)date1 + with:(nullable NSDate *)date2 { + if (!date1 && !date2) { + return YES; + } + return [date1 isEqualToDate:date2]; +} + +- (NSUInteger)hash { + return [self.tokenString hash] ^ [self.expirationDate hash]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDToken_Private.h b/GoogleSignIn/Sources/GIDToken_Private.h new file mode 100644 index 00000000..0ce94af4 --- /dev/null +++ b/GoogleSignIn/Sources/GIDToken_Private.h @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" + +NS_ASSUME_NONNULL_BEGIN + +// Private |GIDToken| methods that are used in this SDK. +@interface GIDToken () + +// Private initializer for |GIDToken|. +// @param token The token String. +// @param expirationDate The expiration date of the token. +- (instancetype)initWithTokenString:(NSString *)tokenString + expirationDate:(nullable NSDate *)expirationDate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDUserAuth.m b/GoogleSignIn/Sources/GIDUserAuth.m new file mode 100644 index 00000000..90dfa289 --- /dev/null +++ b/GoogleSignIn/Sources/GIDUserAuth.m @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h" + +#import "GoogleSignIn/Sources/GIDUserAuth_Private.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h" + +@implementation GIDUserAuth + +- (instancetype)initWithGoogleUser:(GIDGoogleUser *)user + serverAuthCode:(nullable NSString *)serverAuthCode { + self = [super init]; + if (self) { + _user = user; + _serverAuthCode = serverAuthCode; + } + + return self; +} + +@end diff --git a/GoogleSignIn/Sources/GIDUserAuth_Private.h b/GoogleSignIn/Sources/GIDUserAuth_Private.h new file mode 100644 index 00000000..c3f1f728 --- /dev/null +++ b/GoogleSignIn/Sources/GIDUserAuth_Private.h @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h" + +NS_ASSUME_NONNULL_BEGIN + +// Private |GIDUserAuth| methods that are used in this SDK. +@interface GIDUserAuth () + +// Private initializer for |GIDUserAuth|. +// @param user The current GIDGoogleUser. +// @param severAuthCode The one-time authorization code for backend to exchange +// access and refresh tokens. +- (instancetype)initWithGoogleUser:(GIDGoogleUser *)user + serverAuthCode:(nullable NSString *)serverAuthCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h deleted file mode 100644 index 13ae1920..00000000 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2021 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -// We have to import GTMAppAuth because forward declaring the protocol does -// not generate the `fetcherAuthorizer` method below for Swift. -#ifdef SWIFT_PACKAGE -@import GTMAppAuth; -#else -#import -#endif - -@class GIDAuthentication; - -NS_ASSUME_NONNULL_BEGIN - -/// A completion block that takes a `GIDAuthentication` or an error if the attempt to refresh tokens -/// was unsuccessful. -typedef void (^GIDAuthenticationCompletion)(GIDAuthentication *_Nullable authentication, - NSError *_Nullable error); - -/// This class represents the OAuth 2.0 entities needed for sign-in. -@interface GIDAuthentication : NSObject - -/// The client ID associated with the authentication. -@property(nonatomic, readonly) NSString *clientID; - -/// The OAuth2 access token to access Google services. -@property(nonatomic, readonly) NSString *accessToken; - -/// The estimated expiration date of the access token. -@property(nonatomic, readonly) NSDate *accessTokenExpirationDate; - -/// The OAuth2 refresh token to exchange for new access tokens. -@property(nonatomic, readonly) NSString *refreshToken; - -/// An OpenID Connect ID token that identifies the user. Send this token to your server to -/// authenticate the user there. For more information on this topic, see -/// https://developers.google.com/identity/sign-in/ios/backend-auth -@property(nonatomic, readonly, nullable) NSString *idToken; - -/// The estimated expiration date of the ID token. -@property(nonatomic, readonly, nullable) NSDate *idTokenExpirationDate; - -/// Gets a new authorizer for `GTLService`, `GTMSessionFetcher`, or `GTMHTTPFetcher`. -/// -/// @return A new authorizer -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -- (id)fetcherAuthorizer; -#pragma clang diagnostic pop - -/// Get a valid access token and a valid ID token, refreshing them first if they have expired or are -/// about to expire. -/// -/// @param completion A completion block that takes a `GIDAuthentication` or an error if the attempt -/// to refresh tokens was unsuccessful. The block will be called asynchronously on the main -/// queue. -- (void)doWithFreshTokens:(GIDAuthenticationCompletion)completion; - -@end - - -NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h index f5f130df..342c0ab4 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,29 @@ */ #import +#import -NS_ASSUME_NONNULL_BEGIN +#if __has_include() +#import +#elif __has_include() +#import +#endif + +// We have to import GTMAppAuth because forward declaring the protocol does +// not generate the `fetcherAuthorizer` property below for Swift. +#ifdef SWIFT_PACKAGE +@import GTMAppAuth; +#else +#import +#endif -@class GIDAuthentication; +@class GIDConfiguration; +@class GIDUserAuth; +@class GIDToken; @class GIDProfileData; +NS_ASSUME_NONNULL_BEGIN + /// This class represents a user account. @interface GIDGoogleUser : NSObject @@ -30,24 +47,74 @@ NS_ASSUME_NONNULL_BEGIN /// Representation of basic profile data for the user. @property(nonatomic, readonly, nullable) GIDProfileData *profile; -/// The authentication object for the user. -@property(nonatomic, readonly) GIDAuthentication *authentication; - /// The API scopes granted to the app in an array of `NSString`. @property(nonatomic, readonly, nullable) NSArray *grantedScopes; -/// For Google Apps hosted accounts, the domain of the user. -@property(nonatomic, readonly, nullable) NSString *hostedDomain; +/// The configuration that was used to sign in this user. +@property(nonatomic, readonly) GIDConfiguration *configuration; + +/// The OAuth2 access token to access Google services. +@property(nonatomic, readonly) GIDToken *accessToken; + +/// The OAuth2 refresh token to exchange for new access tokens. +@property(nonatomic, readonly) GIDToken *refreshToken; + +/// An OpenID Connect ID token that identifies the user. +/// +/// Send this token to your server to authenticate the user there. For more information on this topic, +/// see https://developers.google.com/identity/sign-in/ios/backend-auth. +@property(nonatomic, readonly, nullable) GIDToken *idToken; + +/// The authorizer for `GTLService`, `GTMSessionFetcher`, or `GTMHTTPFetcher`. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +@property(nonatomic, readonly) id fetcherAuthorizer; +#pragma clang diagnostic pop + +/// Get a valid access token and a valid ID token, refreshing them first if they have expired or +/// are about to expire. +/// +/// @param completion A completion block that takes a `GIDGoogleUser` or an error if the attempt to +/// refresh tokens was unsuccessful. The block will be called asynchronously on the main queue. +- (void)refreshTokensIfNeededWithCompletion:(void (^)(GIDGoogleUser *_Nullable user, + NSError *_Nullable error))completion; + +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST -/// The client ID of the home server. -@property(nonatomic, readonly, nullable) NSString *serverClientID; +/// Starts an interactive consent flow on iOS to add scopes to the current user's grants. +/// +/// The completion will be called at the end of this process. If successful, a `GIDUserAuth` +/// instance will be returned reflecting the new scopes and saved sign-in state will be updated. +/// +/// @param scopes The scopes to ask the user to consent to. +/// @param presentingViewController The view controller used to present `SFSafariViewContoller` on +/// iOS 9 and 10 and to supply `presentationContextProvider` for `ASWebAuthenticationSession` on +/// iOS 13+. +/// @param completion The block that is called on completion. This block will be called asynchronously +/// on the main queue. +- (void)addScopes:(NSArray *)scopes + presentingViewController:(UIViewController *)presentingViewController + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion + NS_EXTENSION_UNAVAILABLE("The add scopes flow is not supported in App Extensions."); -/// An OAuth2 authorization code for the home server. -@property(nonatomic, readonly, nullable) NSString *serverAuthCode; +#elif TARGET_OS_OSX -/// The OpenID2 realm of the home server. -@property(nonatomic, readonly, nullable) NSString *openIDRealm; +/// Starts an interactive consent flow on macOS to add scopes to the current user's grants. +/// +/// The completion will be called at the end of this process. If successful, a `GIDUserAuth` +/// instance will be returned reflecting the new scopes and saved sign-in state will be updated. +/// +/// @param scopes An array of scopes to ask the user to consent to. +/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`. +/// @param completion The block that is called on completion. This block will be called asynchronously +/// on the main queue. +- (void)addScopes:(NSArray *)scopes + presentingWindow:(NSWindow *)presentingWindow + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; +#endif @end diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index 41a059d7..5abf4f6f 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -25,6 +25,7 @@ @class GIDConfiguration; @class GIDGoogleUser; +@class GIDUserAuth; NS_ASSUME_NONNULL_BEGIN @@ -44,16 +45,12 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { kGIDSignInErrorCodeCanceled = -5, /// Indicates an Enterprise Mobility Management related error has occurred. kGIDSignInErrorCodeEMM = -6, - /// Indicates there is no `currentUser`. - kGIDSignInErrorCodeNoCurrentUser = -7, /// Indicates the requested scopes have already been granted to the `currentUser`. kGIDSignInErrorCodeScopesAlreadyGranted = -8, + /// Indicates there is an operation on a previous user. + kGIDSignInErrorCodeMismatchWithCurrentUser = -9, }; -/// Represents a completion block that takes a `GIDGoogleUser` on success or an error if the operation -/// was unsuccessful. -typedef void (^GIDSignInCompletion)(GIDGoogleUser *_Nullable user, NSError *_Nullable error); - /// Represents a completion block that takes an error if the operation was unsuccessful. typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); @@ -94,9 +91,10 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); /// Attempts to restore a previously authenticated user without interaction. /// -/// @param completion The `GIDSignInCompletion` block that is called on completion. This block will -/// be called asynchronously on the main queue. -- (void)restorePreviousSignInWithCompletion:(nullable GIDSignInCompletion)completion; +/// @param completion The block that is called on completion. This block will be called asynchronously +/// on the main queue. +- (void)restorePreviousSignInWithCompletion:(nullable void (^)(GIDGoogleUser *_Nullable user, + NSError *_Nullable error))completion; /// Marks current user as being in the signed out state. - (void)signOut; @@ -122,7 +120,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); /// @param completion The `GIDSignInCompletion` block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController - completion:(nullable GIDSignInCompletion)completion + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); /// Starts an interactive sign-in flow on iOS using the provided hint. @@ -141,7 +140,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); /// be called asynchronously on the main queue. - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint - completion:(nullable GIDSignInCompletion)completion + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); /// Starts an interactive sign-in flow on iOS using the provided hint and additional scopes. @@ -162,23 +162,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - completion:(nullable GIDSignInCompletion)completion; - -/// Starts an interactive consent flow on iOS to add scopes to the current user's grants. -/// -/// The completion will be called at the end of this process. If successful, a new `GIDGoogleUser` -/// instance will be returned reflecting the new scopes and saved sign-in state will be updated. -/// -/// @param scopes The scopes to ask the user to consent to. -/// @param presentingViewController The view controller used to present `SFSafariViewContoller` on -/// iOS 9 and 10 and to supply `presentationContextProvider` for `ASWebAuthenticationSession` on -/// iOS 13+. -/// @param completion The `GIDSignInCompletion` block that is called on completion. This block will -/// be called asynchronously on the main queue. -- (void)addScopes:(NSArray *)scopes - presentingViewController:(UIViewController *)presentingViewController - completion:(nullable GIDSignInCompletion)completion - NS_EXTENSION_UNAVAILABLE("The add scopes flow is not supported in App Extensions."); + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; #elif TARGET_OS_OSX /// Starts an interactive sign-in flow on macOS. @@ -192,7 +177,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); /// @param completion The `GIDSignInCompletion` block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow - completion:(nullable GIDSignInCompletion)completion; + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; /// Starts an interactive sign-in flow on macOS using the provided hint. /// @@ -208,7 +194,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); /// be called asynchronously on the main queue. - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint - completion:(nullable GIDSignInCompletion)completion; + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; /// Starts an interactive sign-in flow on macOS using the provided hint. /// @@ -223,24 +210,11 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); /// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. /// @param completion The `GIDSignInCompletion` block that is called on completion. This block will /// be called asynchronously on the main queue. - - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - completion:(nullable GIDSignInCompletion)completion; - -/// Starts an interactive consent flow on macOS to add scopes to the current user's grants. -/// -/// The completion will be called at the end of this process. If successful, a new `GIDGoogleUser` -/// instance will be returned reflecting the new scopes and saved sign-in state will be updated. -/// -/// @param scopes An array of scopes to ask the user to consent to. -/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`. -/// @param completion The `GIDSignInCompletion` block that is called on completion. This block will -/// be called asynchronously on the main queue. -- (void)addScopes:(NSArray *)scopes - presentingWindow:(NSWindow *)presentingWindow - completion:(nullable GIDSignInCompletion)completion; + completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error))completion; #endif diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h new file mode 100644 index 00000000..29d86a9e --- /dev/null +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h @@ -0,0 +1,45 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// This class represents the basic information of a token. +@interface GIDToken : NSObject + +/// The token string. +@property(nonatomic, copy, readonly) NSString *tokenString; + +/// The estimated expiration date of the token. +@property(nonatomic, readonly, nullable) NSDate *expirationDate; + +/// Check if current token is equal to another one. +/// +/// @param otherToken Another token to compare. +- (BOOL)isEqualToToken:(GIDToken *)otherToken; + +/// Unavailable. +/// :nodoc: ++ (instancetype)new NS_UNAVAILABLE; + +/// Unavailable. +/// :nodoc: +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h new file mode 100644 index 00000000..6baeea09 --- /dev/null +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h @@ -0,0 +1,40 @@ +/* +* Copyright 2022 Google LLC +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import + +@class GIDGoogleUser; + +NS_ASSUME_NONNULL_BEGIN + +/// A helper object that contains the outcome of a successful signIn or addScopes flow. +@interface GIDUserAuth : NSObject + +/// The updated `GIDGoogleUser` instance for the user who just completed the flow. +@property(nonatomic, readonly) GIDGoogleUser *user; + +/// An OAuth2 authorization code for the home server. +@property(nonatomic, readonly, nullable) NSString *serverAuthCode; + +/// Unsupported. ++ (instancetype)new NS_UNAVAILABLE; + +/// Unsupported. +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h index 091f2c1a..1b9d1042 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h @@ -15,11 +15,12 @@ */ #import -#import "GIDAuthentication.h" #import "GIDConfiguration.h" #import "GIDGoogleUser.h" #import "GIDProfileData.h" #import "GIDSignIn.h" +#import "GIDToken.h" +#import "GIDUserAuth.h" #if TARGET_OS_IOS || TARGET_OS_MACCATALYST #import "GIDSignInButton.h" #endif diff --git a/GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.m b/GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.m deleted file mode 100644 index feae4cc1..00000000 --- a/GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.m +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#import "GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h" - -@implementation GIDAuthentication (Testing) - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[GIDAuthentication class]]) { - return NO; - } - return [self isEqualToAuthentication:(GIDAuthentication *)object]; -} - -- (BOOL)isEqualToAuthentication:(GIDAuthentication *)other { - return [self.clientID isEqual:other.clientID] && - [self.accessToken isEqual:other.accessToken] && - [self.accessTokenExpirationDate isEqual:other.accessTokenExpirationDate] && - [self.refreshToken isEqual:other.refreshToken] && - (self.idToken == other.idToken || [self.idToken isEqual:other.idToken]) && - (self.idTokenExpirationDate == other.idTokenExpirationDate || - [self.idTokenExpirationDate isEqual:other.idTokenExpirationDate]); -} - -// Not the hash implemention you want to use on prod, but just to match |isEqual:| here. -- (NSUInteger)hash { - return [self.clientID hash] ^ [self.accessToken hash] ^ [self.accessTokenExpirationDate hash] ^ - [self.refreshToken hash] ^ [self.idToken hash] ^ [self.idTokenExpirationDate hash]; -} - -@end - diff --git a/GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m b/GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m deleted file mode 100644 index ad57bead..00000000 --- a/GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m +++ /dev/null @@ -1,684 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#import - -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h" -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" - -#import "GoogleSignIn/Sources/GIDAuthentication_Private.h" -#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" -#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h" -#import "GoogleSignIn/Sources/GIDSignInPreferences.h" -#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" -#import "GoogleSignIn/Tests/Unit/OIDTokenRequest+Testing.h" -#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h" - -#ifdef SWIFT_PACKAGE -@import AppAuth; -@import GoogleUtilities_MethodSwizzler; -@import GoogleUtilities_SwizzlerTestHelpers; -@import GTMAppAuth; -@import GTMSessionFetcherCore; -@import OCMock; -#else -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#endif - -static NSString *const kClientID = @"87654321.googleusercontent.com"; -static NSString *const kNewAccessToken = @"new_access_token"; -static NSString *const kUserEmail = @"foo@gmail.com"; -static NSTimeInterval const kExpireTime = 442886117; -static NSTimeInterval const kNewExpireTime = 442886123; -static NSTimeInterval const kNewExpireTime2 = 442886124; - -static NSTimeInterval const kTimeAccuracy = 10; - -// The system name in old iOS versions. -static NSString *const kOldIOSName = @"iPhone OS"; - -// The system name in new iOS versions. -static NSString *const kNewIOSName = @"iOS"; - -// List of observed properties of the class being tested. -static NSString *const kObservedProperties[] = { - @"accessToken", - @"accessTokenExpirationDate", - @"idToken", - @"idTokenExpirationDate" -}; -static const NSUInteger kNumberOfObservedProperties = - sizeof(kObservedProperties) / sizeof(*kObservedProperties); - -// Bit position for notification change type bitmask flags. -// Must match the list of observed properties above. -typedef NS_ENUM(NSUInteger, ChangeType) { - kChangeTypeAccessTokenPrior, - kChangeTypeAccessToken, - kChangeTypeAccessTokenExpirationDatePrior, - kChangeTypeAccessTokenExpirationDate, - kChangeTypeIDTokenPrior, - kChangeTypeIDToken, - kChangeTypeIDTokenExpirationDatePrior, - kChangeTypeIDTokenExpirationDate, - kChangeTypeEnd // not a real change type but an end mark for calculating |kChangeAll| -}; - -static const NSUInteger kChangeNone = 0u; -static const NSUInteger kChangeAll = (1u << kChangeTypeEnd) - 1u; - -#if __has_feature(c_static_assert) || __has_extension(c_static_assert) -_Static_assert(kChangeTypeEnd == (sizeof(kObservedProperties) / sizeof(*kObservedProperties)) * 2, - "List of observed properties must match list of change notification enums"); -#endif - -@interface GIDAuthenticationTest : XCTestCase -@end - -@implementation GIDAuthenticationTest { - // Whether the auth object has ID token or not. - BOOL _hasIDToken; - - // Fake data used to generate the expiration date of the access token. - NSTimeInterval _accessTokenExpireTime; - - // Fake data used to generate the expiration date of the ID token. - NSTimeInterval _idTokenExpireTime; - - // Fake data used to generate the additional token request parameters. - NSDictionary *_additionalTokenRequestParameters; - - // The saved token fetch handler. - OIDTokenCallback _tokenFetchHandler; - - // The saved token request. - OIDTokenRequest *_tokenRequest; - - // All GIDAuthentication objects that are observed. - NSMutableArray *_observedAuths; - - // Bitmask flags for observed changes, as specified in |ChangeType|. - NSUInteger _changesObserved; - - // The fake system name used for testing. - NSString *_fakeSystemName; -} - -- (void)setUp { - _hasIDToken = YES; - _accessTokenExpireTime = kAccessTokenExpiresIn; - _idTokenExpireTime = kExpireTime; - _additionalTokenRequestParameters = nil; - _tokenFetchHandler = nil; - _tokenRequest = nil; - [GULSwizzler swizzleClass:[OIDAuthorizationService class] - selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:) - isClassSelector:YES - withBlock:^(id sender, - OIDTokenRequest *request, - OIDAuthorizationResponse *authorizationResponse, - OIDTokenCallback callback) { - XCTAssertNotNil(authorizationResponse.request.clientID); - XCTAssertNotNil(authorizationResponse.request.configuration.tokenEndpoint); - XCTAssertNil(self->_tokenFetchHandler); // only one on-going fetch allowed - self->_tokenFetchHandler = [callback copy]; - self->_tokenRequest = [request copy]; - return nil; - }]; - _observedAuths = [[NSMutableArray alloc] init]; - _changesObserved = 0; - _fakeSystemName = kNewIOSName; -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - [GULSwizzler swizzleClass:[UIDevice class] - selector:@selector(systemName) - isClassSelector:NO - withBlock:^(id sender) { return self->_fakeSystemName; }]; -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST -} - -- (void)tearDown { - [GULSwizzler unswizzleClass:[OIDAuthorizationService class] - selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:) - isClassSelector:YES]; -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - [GULSwizzler unswizzleClass:[UIDevice class] - selector:@selector(systemName) - isClassSelector:NO]; -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - for (GIDAuthentication *auth in _observedAuths) { - for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) { - [auth removeObserver:self forKeyPath:kObservedProperties[i]]; - } - } - _observedAuths = nil; -} - -#pragma mark - Tests - -- (void)testInitWithAuthState { - OIDAuthState *authState = [OIDAuthState testInstance]; - GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState]; - - XCTAssertEqualObjects(auth.clientID, authState.lastAuthorizationResponse.request.clientID); - XCTAssertEqualObjects(auth.accessToken, authState.lastTokenResponse.accessToken); - XCTAssertEqualObjects(auth.accessTokenExpirationDate, - authState.lastTokenResponse.accessTokenExpirationDate); - XCTAssertEqualObjects(auth.refreshToken, authState.refreshToken); - XCTAssertEqualObjects(auth.idToken, authState.lastTokenResponse.idToken); - OIDIDToken *idToken = [[OIDIDToken alloc] - initWithIDTokenString:authState.lastTokenResponse.idToken]; - XCTAssertEqualObjects(auth.idTokenExpirationDate, [idToken expiresAt]); -} - -- (void)testInitWithAuthStateNoIDToken { - OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:nil]; - GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState]; - - XCTAssertEqualObjects(auth.clientID, authState.lastAuthorizationResponse.request.clientID); - XCTAssertEqualObjects(auth.accessToken, authState.lastTokenResponse.accessToken); - XCTAssertEqualObjects(auth.accessTokenExpirationDate, - authState.lastTokenResponse.accessTokenExpirationDate); - XCTAssertEqualObjects(auth.refreshToken, authState.refreshToken); - XCTAssertNil(auth.idToken); - XCTAssertNil(auth.idTokenExpirationDate); -} - -- (void)testAuthState { - OIDAuthState *authState = [OIDAuthState testInstance]; - GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState]; - OIDAuthState *authStateReturned = auth.authState; - - XCTAssertEqual(authState, authStateReturned); -} - -- (void)testCoding { - if (@available(iOS 11, macOS 10.13, *)) { - GIDAuthentication *auth = [self auth]; - NSData *data = [NSKeyedArchiver archivedDataWithRootObject:auth requiringSecureCoding:YES error:nil]; - GIDAuthentication *newAuth = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDAuthentication class] - fromData:data - error:nil]; - XCTAssertEqualObjects(auth, newAuth); - XCTAssertTrue([GIDAuthentication supportsSecureCoding]); - } else { - XCTSkip(@"Required API is not available for this test."); - } -} - -#if TARGET_OS_IOS || TARGET_OS_MACCATALYST -// Deprecated in iOS 13 and macOS 10.14 -- (void)testLegacyCoding { - GIDAuthentication *auth = [self auth]; - NSData *data = [NSKeyedArchiver archivedDataWithRootObject:auth]; - GIDAuthentication *newAuth = [NSKeyedUnarchiver unarchiveObjectWithData:data]; - XCTAssertEqualObjects(auth, newAuth); - XCTAssertTrue([GIDAuthentication supportsSecureCoding]); -} -#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST - -- (void)testFetcherAuthorizer { - // This is really hard to test without assuming how GTMAppAuthFetcherAuthorization works - // internally, so let's just take the shortcut here by asserting we get a - // GTMAppAuthFetcherAuthorization object. - GIDAuthentication *auth = [self auth]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - id fetcherAuthroizer = auth.fetcherAuthorizer; -#pragma clang diagnostic pop - XCTAssertTrue([fetcherAuthroizer isKindOfClass:[GTMAppAuthFetcherAuthorization class]]); - XCTAssertTrue([fetcherAuthroizer canAuthorize]); -} - -- (void)testDoWithFreshTokensWithBothExpired { - // Both tokens expired 10 seconds ago. - [self setExpireTimeForAccessToken:-10 IDToken:-10]; - [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)]; -} - -- (void)testDoWithFreshTokensWithAccessTokenExpired { - // Access token expired 10 seconds ago while ID token to expire in 10 minutes. - [self setExpireTimeForAccessToken:-10 IDToken:10 * 60]; - [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)]; -} - -- (void)testDoWithFreshTokensWithIDTokenToExpire { - // Access token to expire in 10 minutes while ID token to expire in 10 seconds. - [self setExpireTimeForAccessToken:10 * 60 IDToken:10]; - [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)]; -} - -- (void)testDoWithFreshTokensWithBothFresh { - // Both tokens to expire in 10 minutes. - [self setExpireTimeForAccessToken:10 * 60 IDToken:10 * 60]; - [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)]; -} - -- (void)testDoWithFreshTokensWithAccessTokenExpiredAndNoIDToken { - _hasIDToken = NO; - [self setExpireTimeForAccessToken:-10 IDToken:10 * 60]; // access token expired 10 seconds ago - [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)]; -} - -- (void)testDoWithFreshTokensWithAccessTokenFreshAndNoIDToken { - _hasIDToken = NO; - [self setExpireTimeForAccessToken:10 * 60 IDToken:-10]; // access token to expire in 10 minutes - [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)]; -} - -- (void)testDoWithFreshTokensError { - [self setTokensExpireTime:-10]; // expired 10 seconds ago - GIDAuthentication *auth = [self observedAuth]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; - [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) { - [expectation fulfill]; - XCTAssertNil(authentication); - XCTAssertNotNil(error); - }]; - _tokenFetchHandler(nil, [self fakeError]); - [self waitForExpectationsWithTimeout:1 handler:nil]; - [self assertOldTokensInAuth:auth]; -} - -- (void)testDoWithFreshTokensQueue { - GIDAuthentication *auth = [self observedAuth]; - XCTestExpectation *firstExpectation = - [self expectationWithDescription:@"First callback is called"]; - [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) { - [firstExpectation fulfill]; - [self assertNewTokensInAuth:authentication]; - XCTAssertNil(error); - }]; - XCTestExpectation *secondExpectation = - [self expectationWithDescription:@"Second callback is called"]; - [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) { - [secondExpectation fulfill]; - [self assertNewTokensInAuth:authentication]; - XCTAssertNil(error); - }]; - _tokenFetchHandler([self tokenResponseWithNewTokens], nil); - [self waitForExpectationsWithTimeout:1 handler:nil]; - [self assertNewTokensInAuth:auth]; -} - -#pragma mark - EMM Support - -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - -- (void)testEMMSupport { - _additionalTokenRequestParameters = @{ - @"emm_support" : @"xyz", - }; - GIDAuthentication *auth = [self auth]; - [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication, - NSError * _Nullable error) {}]; - _tokenFetchHandler([self tokenResponseWithNewTokens], nil); - NSDictionary *expectedParameters = @{ - @"emm_support" : @"xyz", - @"device_os" : [NSString stringWithFormat:@"%@ %@", - _fakeSystemName, [UIDevice currentDevice].systemVersion], - kSDKVersionLoggingParameter : GIDVersion(), - kEnvironmentLoggingParameter : GIDEnvironment(), - }; - XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters, - expectedParameters); -} - -- (void)testSystemNameNormalization { - _fakeSystemName = kOldIOSName; - _additionalTokenRequestParameters = @{ - @"emm_support" : @"xyz", - }; - GIDAuthentication *auth = [self auth]; - [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication, - NSError * _Nullable error) {}]; - _tokenFetchHandler([self tokenResponseWithNewTokens], nil); - NSDictionary *expectedParameters = @{ - @"emm_support" : @"xyz", - @"device_os" : [NSString stringWithFormat:@"%@ %@", - kNewIOSName, [UIDevice currentDevice].systemVersion], - kSDKVersionLoggingParameter : GIDVersion(), - kEnvironmentLoggingParameter : GIDEnvironment(), - }; - XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters, - expectedParameters); -} - -- (void)testEMMPasscodeInfo { - _additionalTokenRequestParameters = @{ - @"emm_support" : @"xyz", - @"device_os" : @"old one", - @"emm_passcode_info" : @"something", - }; - GIDAuthentication *auth = [self auth]; - [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication, - NSError * _Nullable error) {}]; - _tokenFetchHandler([self tokenResponseWithNewTokens], nil); - NSDictionary *expectedParameters = @{ - @"emm_support" : @"xyz", - @"device_os" : [NSString stringWithFormat:@"%@ %@", - _fakeSystemName, [UIDevice currentDevice].systemVersion], - @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info, - kSDKVersionLoggingParameter : GIDVersion(), - kEnvironmentLoggingParameter : GIDEnvironment(), - }; - XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters, - expectedParameters); -} - -- (void)testEMMError { - // Set expectations. - NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" }; - NSError *emmError = [NSError errorWithDomain:@"anydomain" - code:12345 - userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }]; - id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]); - [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance]; - __block void (^completion)(void); - [[[mockEMMErrorHandler expect] andReturnValue:@YES] - handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) { - completion = arg; - return YES; - }]]; - - // Start testing. - _additionalTokenRequestParameters = @{ - @"emm_support" : @"xyz", - }; - GIDAuthentication *auth = [self auth]; - XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"]; - notCalled.inverted = YES; - XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"]; - [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) { - [notCalled fulfill]; - [called fulfill]; - XCTAssertNil(authentication); - XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain); - XCTAssertEqual(error.code, kGIDSignInErrorCodeEMM); - }]; - _tokenFetchHandler(nil, emmError); - - // Verify and clean up. - [mockEMMErrorHandler verify]; - [mockEMMErrorHandler stopMocking]; - [self waitForExpectations:@[ notCalled ] timeout:1]; - completion(); - [self waitForExpectations:@[ called ] timeout:1]; - [self assertOldTokensInAuth:auth]; -} - -- (void)testNonEMMError { - // Set expectations. - NSDictionary *errorJSON = @{ @"error" : @"Not EMM Specific Error" }; - NSError *emmError = [NSError errorWithDomain:@"anydomain" - code:12345 - userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }]; - id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]); - [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance]; - __block void (^completion)(void); - [[[mockEMMErrorHandler expect] andReturnValue:@NO] - handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) { - completion = arg; - return YES; - }]]; - - // Start testing. - _additionalTokenRequestParameters = @{ - @"emm_support" : @"xyz", - }; - GIDAuthentication *auth = [self auth]; - XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"]; - notCalled.inverted = YES; - XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"]; - [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) { - [notCalled fulfill]; - [called fulfill]; - XCTAssertNil(authentication); - XCTAssertEqualObjects(error.domain, @"anydomain"); - XCTAssertEqual(error.code, 12345); - }]; - _tokenFetchHandler(nil, emmError); - - // Verify and clean up. - [mockEMMErrorHandler verify]; - [mockEMMErrorHandler stopMocking]; - [self waitForExpectations:@[ notCalled ] timeout:1]; - completion(); - [self waitForExpectations:@[ called ] timeout:1]; - [self assertOldTokensInAuth:auth]; -} - -- (void)testCodingPreserveEMMParameters { - _additionalTokenRequestParameters = @{ - @"emm_support" : @"xyz", - @"device_os" : @"old one", - @"emm_passcode_info" : @"something", - }; - NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[self auth]]; - GIDAuthentication *auth = [NSKeyedUnarchiver unarchiveObjectWithData:data]; - [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication, - NSError * _Nullable error) {}]; - _tokenFetchHandler([self tokenResponseWithNewTokens], nil); - NSDictionary *expectedParameters = @{ - @"emm_support" : @"xyz", - @"device_os" : [NSString stringWithFormat:@"%@ %@", - [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion], - @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info, - kSDKVersionLoggingParameter : GIDVersion(), - kEnvironmentLoggingParameter : GIDEnvironment(), - }; - XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters, - expectedParameters); -} - -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - -#pragma mark - NSKeyValueObserving - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context { - GIDAuthentication *auth = (GIDAuthentication *)object; - ChangeType changeType; - if ([keyPath isEqualToString:@"accessToken"]) { - if (change[NSKeyValueChangeNotificationIsPriorKey]) { - XCTAssertEqualObjects(auth.accessToken, kAccessToken); - changeType = kChangeTypeAccessTokenPrior; - } else { - XCTAssertEqualObjects(auth.accessToken, kNewAccessToken); - changeType = kChangeTypeAccessToken; - } - } else if ([keyPath isEqualToString:@"accessTokenExpirationDate"]) { - if (change[NSKeyValueChangeNotificationIsPriorKey]) { - [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime]; - changeType = kChangeTypeAccessTokenExpirationDatePrior; - } else { - [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime]; - changeType = kChangeTypeAccessTokenExpirationDate; - } - } else if ([keyPath isEqualToString:@"idToken"]) { - if (change[NSKeyValueChangeNotificationIsPriorKey]) { - XCTAssertEqualObjects(auth.idToken, [self idToken]); - changeType = kChangeTypeIDTokenPrior; - } else { - XCTAssertEqualObjects(auth.idToken, [self idTokenNew]); - changeType = kChangeTypeIDToken; - } - } else if ([keyPath isEqualToString:@"idTokenExpirationDate"]) { - if (change[NSKeyValueChangeNotificationIsPriorKey]) { - if (_hasIDToken) { - [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime]; - } - changeType = kChangeTypeIDTokenExpirationDatePrior; - } else { - if (_hasIDToken) { - [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2]; - } - changeType = kChangeTypeIDTokenExpirationDate; - } - } else { - XCTFail(@"unexpected keyPath"); - return; // so compiler knows |changeType| is always assigned - } - NSUInteger changeMask = 1u << changeType; - XCTAssertFalse(_changesObserved & changeMask); // each change type should only fire once - _changesObserved |= changeMask; -} - -#pragma mark - Helpers - -- (GIDAuthentication *)auth { - NSString *idToken = [self idToken]; - NSNumber *accessTokenExpiresIn = - @(_accessTokenExpireTime - [[NSDate date] timeIntervalSinceReferenceDate]); - OIDTokenRequest *tokenRequest = - [OIDTokenRequest testInstanceWithAdditionalParameters:_additionalTokenRequestParameters]; - OIDTokenResponse *tokenResponse = - [OIDTokenResponse testInstanceWithIDToken:idToken - accessToken:kAccessToken - expiresIn:accessTokenExpiresIn - tokenRequest:tokenRequest]; - return [[GIDAuthentication alloc] - initWithAuthState:[OIDAuthState testInstanceWithTokenResponse:tokenResponse]]; -} - -- (NSString *)idTokenWithExpireTime:(NSTimeInterval)expireTime { - if (!_hasIDToken) { - return nil; - } - return [OIDTokenResponse idTokenWithSub:kUserID exp:@(expireTime + NSTimeIntervalSince1970)]; -} - -- (NSString *)idToken { - return [self idTokenWithExpireTime:_idTokenExpireTime]; -} - -- (NSString *)idTokenNew { - return [self idTokenWithExpireTime:kNewExpireTime2]; -} - -// Return the auth object that has certain property changes observed. -- (GIDAuthentication *)observedAuth { - GIDAuthentication *auth = [self auth]; - for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) { - [auth addObserver:self - forKeyPath:kObservedProperties[i] - options:NSKeyValueObservingOptionPrior - context:NULL]; - } - [_observedAuths addObject:auth]; - return auth; -} - -- (OIDTokenResponse *)tokenResponseWithNewTokens { - NSNumber *expiresIn = @(kNewExpireTime - [NSDate timeIntervalSinceReferenceDate]); - return [OIDTokenResponse testInstanceWithIDToken:(_hasIDToken ? [self idTokenNew] : nil) - accessToken:kNewAccessToken - expiresIn:expiresIn - tokenRequest:_tokenRequest ?: nil]; -} - -- (NSError *)fakeError { - return [NSError errorWithDomain:@"fake.domain" code:-1 userInfo:nil]; -} - -- (void)assertDate:(NSDate *)date equalTime:(NSTimeInterval)time { - XCTAssertEqualWithAccuracy([date timeIntervalSinceReferenceDate], time, kTimeAccuracy); -} - -- (void)assertOldAccessTokenInAuth:(GIDAuthentication *)auth { - XCTAssertEqualObjects(auth.accessToken, kAccessToken); - [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime]; - XCTAssertEqual(_changesObserved, kChangeNone); -} - -- (void)assertNewAccessTokenInAuth:(GIDAuthentication *)auth { - XCTAssertEqualObjects(auth.accessToken, kNewAccessToken); - [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime]; - XCTAssertEqual(_changesObserved, kChangeAll); -} - -- (void)assertOldTokensInAuth:(GIDAuthentication *)auth { - [self assertOldAccessTokenInAuth:auth]; - XCTAssertEqualObjects(auth.idToken, [self idToken]); - if (_hasIDToken) { - [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime]; - } -} - -- (void)assertNewTokensInAuth:(GIDAuthentication *)auth { - [self assertNewAccessTokenInAuth:auth]; - XCTAssertEqualObjects(auth.idToken, [self idTokenNew]); - if (_hasIDToken) { - [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2]; - } -} - -- (void)setTokensExpireTime:(NSTimeInterval)fromNow { - [self setExpireTimeForAccessToken:fromNow IDToken:fromNow]; -} - -- (void)setExpireTimeForAccessToken:(NSTimeInterval)accessExpire IDToken:(NSTimeInterval)idExpire { - _accessTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + accessExpire; - _idTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + idExpire; -} - -- (void)verifyTokensRefreshedWithMethod:(SEL)sel { - GIDAuthentication *auth = [self observedAuth]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - // We know the method doesn't return anything, so there is no risk of leaking. - [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) { -#pragma clang diagnostic pop - [expectation fulfill]; - [self assertNewTokensInAuth:authentication]; - XCTAssertNil(error); - }]; - _tokenFetchHandler([self tokenResponseWithNewTokens], nil); - [self waitForExpectationsWithTimeout:1 handler:nil]; - [self assertNewTokensInAuth:auth]; -} - -- (void)verifyTokensNotRefreshedWithMethod:(SEL)sel { - GIDAuthentication *auth = [self observedAuth]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - // We know the method doesn't return anything, so there is no risk of leaking. - [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) { -#pragma clang diagnostic pop - [expectation fulfill]; - [self assertOldTokensInAuth:authentication]; - XCTAssertNil(error); - }]; - XCTAssertNil(_tokenFetchHandler); - [self waitForExpectationsWithTimeout:1 handler:nil]; - [self assertOldTokensInAuth:auth]; -} - -@end diff --git a/GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.m b/GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.m index 8fd52fc1..72bc9106 100644 --- a/GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.m +++ b/GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.m @@ -46,6 +46,12 @@ - (BOOL)isEqualToConfiguration:(GIDConfiguration *)other { self.openIDRealm == other.openIDRealm); } +// Not the hash implemention you want to use on prod, but just to match |isEqual:| here. +- (NSUInteger)hash { + return [self.clientID hash] ^ [self.serverClientID hash] ^ [self.hostedDomain hash] ^ + [self.openIDRealm hash]; +} + + (instancetype)testInstance { return [[GIDConfiguration alloc] initWithClientID:OIDAuthorizationRequestTestingClientID serverClientID:kServerClientID diff --git a/GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m b/GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m index 57b81702..5f714f0a 100644 --- a/GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m +++ b/GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + #import #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m b/GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m new file mode 100644 index 00000000..aaf5c71e --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m @@ -0,0 +1,229 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import + +#import "GoogleSignIn/Sources/GIDEMMSupport.h" + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" + +#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" +#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h" + +#ifdef SWIFT_PACKAGE +@import AppAuth; +@import GoogleUtilities_MethodSwizzler; +@import GoogleUtilities_SwizzlerTestHelpers; +@import OCMock; +#else +#import +#import +#import +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +// The system name in old iOS versions. +static NSString *const kOldIOSName = @"iPhone OS"; + +// The system name in new iOS versions. +static NSString *const kNewIOSName = @"iOS"; + +// They keys in EMM dictionary. +static NSString *const kEMMKey = @"emm_support"; +static NSString *const kDeviceOSKey = @"device_os"; +static NSString *const kEMMPasscodeInfoKey = @"emm_passcode_info"; + +@interface GIDEMMSupportTest : XCTestCase +@end + +@implementation GIDEMMSupportTest + +- (void)testUpdatedEMMParametersWithParameters_NoEMMKey { + NSDictionary *originalParameters = @{ + @"not_emm_support_key" : @"xyz", + }; + + NSDictionary *updatedEMMParameters = + [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters]; + + XCTAssertEqualObjects(updatedEMMParameters, originalParameters); +} + +- (void)testUpdateEMMParametersWithParameters_systemName { + [GULSwizzler swizzleClass:[UIDevice class] + selector:@selector(systemName) + isClassSelector:NO + withBlock:^(id sender) { return kNewIOSName; }]; + + NSDictionary *originalParameters = @{ + kEMMKey : @"xyz", + }; + + NSDictionary *updatedEMMParameters = + [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters]; + + NSDictionary *expectedParameters = @{ + kEMMKey : @"xyz", + kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]] + }; + + XCTAssertEqualObjects(updatedEMMParameters, expectedParameters); + + [self addTeardownBlock:^{ + [GULSwizzler unswizzleClass:[UIDevice class] + selector:@selector(systemName) + isClassSelector:NO]; + }]; +} + +// When the systemName is @"iPhone OS" we still get "iOS". +- (void)testUpdateEMMParametersWithParameters_systemNameNormalization { + [GULSwizzler swizzleClass:[UIDevice class] + selector:@selector(systemName) + isClassSelector:NO + withBlock:^(id sender) { return kOldIOSName; }]; + + NSDictionary *originalParameters = @{ + kEMMKey : @"xyz", + }; + + NSDictionary *updatedEMMParameters = + [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters]; + + NSDictionary *expectedParameters = @{ + kEMMKey : @"xyz", + kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]] + }; + + XCTAssertEqualObjects(updatedEMMParameters, expectedParameters); + + [self addTeardownBlock:^{ + [GULSwizzler unswizzleClass:[UIDevice class] + selector:@selector(systemName) + isClassSelector:NO]; + }]; +} + +- (void)testUpdateEMMParametersWithParameters_passcodInfo { + [GULSwizzler swizzleClass:[UIDevice class] + selector:@selector(systemName) + isClassSelector:NO + withBlock:^(id sender) { return kOldIOSName; }]; + + NSDictionary *originalParameters = @{ + kEMMKey : @"xyz", + kDeviceOSKey : @"old one", + kEMMPasscodeInfoKey : @"something", + }; + + NSDictionary *updatedEMMParameters = + [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters]; + + NSDictionary *expectedParameters = @{ + kEMMKey : @"xyz", + kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]], + kEMMPasscodeInfoKey : [GIDMDMPasscodeState passcodeState].info, + }; + + XCTAssertEqualObjects(updatedEMMParameters, expectedParameters); + + [self addTeardownBlock:^{ + [GULSwizzler unswizzleClass:[UIDevice class] + selector:@selector(systemName) + isClassSelector:NO]; + }]; + +} + +- (void)testHandleTokenFetchEMMError_errorIsEMM { + // Set expectations. + NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" }; + NSError *emmError = [NSError errorWithDomain:@"anydomain" + code:12345 + userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }]; + id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]); + [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance]; + __block void (^savedCompletion)(void); + [[[mockEMMErrorHandler stub] andReturnValue:@YES] + handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) { + savedCompletion = arg; + return YES; + }]]; + + XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"]; + notCalled.inverted = YES; + XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"]; + + [GIDEMMSupport handleTokenFetchEMMError:emmError completion:^(NSError *error) { + [notCalled fulfill]; + [called fulfill]; + XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain); + XCTAssertEqual(error.code, kGIDSignInErrorCodeEMM); + }]; + + [self waitForExpectations:@[ notCalled ] timeout:1]; + savedCompletion(); + [self waitForExpectations:@[ called ] timeout:1]; +} + +- (void)testHandleTokenFetchEMMError_errorIsNotEMM { + // Set expectations. + NSDictionary *errorJSON = @{ @"error" : @"Not EMM Specific Error" }; + NSError *emmError = [NSError errorWithDomain:@"anydomain" + code:12345 + userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }]; + id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]); + [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance]; + __block void (^savedCompletion)(void); + [[[mockEMMErrorHandler stub] andReturnValue:@NO] + handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) { + savedCompletion = arg; + return YES; + }]]; + + XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"]; + notCalled.inverted = YES; + XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"]; + + [GIDEMMSupport handleTokenFetchEMMError:emmError completion:^(NSError *error) { + [notCalled fulfill]; + [called fulfill]; + XCTAssertEqualObjects(error.domain, @"anydomain"); + XCTAssertEqual(error.code, 12345); + }]; + + [self waitForExpectations:@[ notCalled ] timeout:1]; + savedCompletion(); + [self waitForExpectations:@[ called ] timeout:1]; +} + +# pragma mark - Helpers + +- (NSString *)systemVersion { + return [UIDevice currentDevice].systemVersion; +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h b/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h index 8aa6faa3..3f833030 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h @@ -23,3 +23,12 @@ - (NSUInteger)hash; @end + +// The old format GIDGoogleUser contains a GIDAuthentication. +// Note: remove this class when GIDGoogleUser no longer support old encoding. +@interface GIDGoogleUserOldFormat : GIDGoogleUser + +- (instancetype)initWithAuthState:(OIDAuthState *)authState + profileData:(GIDProfileData *)profileData; + +@end diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m b/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m index 64e00fed..3428c896 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m @@ -14,9 +14,19 @@ #import "GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h" -#import "GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h" +#import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" + +#import "GoogleSignIn/Sources/GIDAuthentication.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" + +#import "GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.h" #import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h" +// Key constants used for encode and decode. +static NSString *const kProfileDataKey = @"profileData"; +static NSString *const kAuthentication = @"authentication"; + @implementation GIDGoogleUser (Testing) - (BOOL)isEqual:(id)object { @@ -30,17 +40,42 @@ - (BOOL)isEqual:(id)object { } - (BOOL)isEqualToGoogleUser:(GIDGoogleUser *)other { - return [self.authentication isEqual:other.authentication] && - [self.userID isEqual:other.userID] && - [self.serverAuthCode isEqual:other.serverAuthCode] && + return [self.userID isEqual:other.userID] && [self.profile isEqual:other.profile] && - [self.hostedDomain isEqual:other.hostedDomain]; + [self.configuration isEqual:other.configuration] && + [self.idToken isEqual:other.idToken] && + [self.refreshToken isEqual:other.refreshToken] && + [self.accessToken isEqual:other.accessToken]; } // Not the hash implemention you want to use on prod, but just to match |isEqual:| here. - (NSUInteger)hash { - return [self.authentication hash] ^ [self.userID hash] ^ [self.serverAuthCode hash] ^ - [self.profile hash] ^ [self.hostedDomain hash]; + return [self.userID hash] ^ [self.configuration hash] ^ [self.profile hash] ^ + [self.idToken hash] ^ [self.refreshToken hash] ^ [self.accessToken hash]; +} + +@end + +@implementation GIDGoogleUserOldFormat { + GIDAuthentication *_authentication; + GIDProfileData *_profile; +} + +- (instancetype)initWithAuthState:(OIDAuthState *)authState + profileData:(GIDProfileData *)profileData { + self = [super initWithAuthState:authState profileData:profileData]; + if (self) { + _authentication = [[GIDAuthentication alloc] initWithAuthState:authState]; + _profile = profileData; + } + return self; +} + +#pragma mark - NSSecureCoding + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:_profile forKey:kProfileDataKey]; + [encoder encodeObject:_authentication forKey:kAuthentication]; } @end diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m index 6dee9267..b79d419b 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m @@ -15,27 +15,80 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h" #import +#import -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" -#import "GoogleSignIn/Sources/GIDAuthentication_Private.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" +#import "GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h" #import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h" #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" #import "GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h" +#import "GoogleSignIn/Tests/Unit/OIDTokenRequest+Testing.h" #import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h" #ifdef SWIFT_PACKAGE @import AppAuth; +@import GoogleUtilities_MethodSwizzler; +@import GoogleUtilities_SwizzlerTestHelpers; +@import GTMAppAuth; +@import OCMock; #else #import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import #endif +static NSString *const kNewAccessToken = @"new_access_token"; +static NSString *const kNewRefreshToken = @"new_refresh_token"; + +static NSTimeInterval const kTimeAccuracy = 10; +static NSTimeInterval const kIDTokenExpiresIn = 100; +static NSTimeInterval const kNewIDTokenExpiresIn = 200; + +static NSString *const kNewScope = @"newScope"; + @interface GIDGoogleUserTest : XCTestCase @end -@implementation GIDGoogleUserTest +@implementation GIDGoogleUserTest { + // The saved token fetch handler. + OIDTokenCallback _tokenFetchHandler; +} + +- (void)setUp { + _tokenFetchHandler = nil; + + // We need to use swizzle here because OCMock can not stub class method with arguments. + [GULSwizzler swizzleClass:[OIDAuthorizationService class] + selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:) + isClassSelector:YES + withBlock:^(id sender, + OIDTokenRequest *request, + OIDAuthorizationResponse *authorizationResponse, + OIDTokenCallback callback) { + // Save the OIDTokenCallback. + self->_tokenFetchHandler = [callback copy]; + }]; +} + +- (void)tearDown { + [GULSwizzler unswizzleClass:[OIDAuthorizationService class] + selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:) + isClassSelector:YES]; +} #pragma mark - Tests @@ -43,15 +96,18 @@ - (void)testInitWithAuthState { OIDAuthState *authState = [OIDAuthState testInstance]; GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState profileData:[GIDProfileData testInstance]]; - GIDAuthentication *authentication = - [[GIDAuthentication alloc] initWithAuthState:authState]; - - XCTAssertEqualObjects(user.authentication, authentication); + XCTAssertEqualObjects(user.grantedScopes, @[ OIDAuthorizationRequestTestingScope2 ]); XCTAssertEqualObjects(user.userID, kUserID); - XCTAssertEqualObjects(user.hostedDomain, kHostedDomain); - XCTAssertEqualObjects(user.serverAuthCode, kServerAuthCode); + XCTAssertEqualObjects(user.configuration.hostedDomain, kHostedDomain); + XCTAssertEqualObjects(user.configuration.clientID, OIDAuthorizationRequestTestingClientID); XCTAssertEqualObjects(user.profile, [GIDProfileData testInstance]); + XCTAssertEqualObjects(user.accessToken.tokenString, kAccessToken); + XCTAssertEqualObjects(user.refreshToken.tokenString, kRefreshToken); + + OIDIDToken *idToken = [[OIDIDToken alloc] + initWithIDTokenString:authState.lastTokenResponse.idToken]; + XCTAssertEqualObjects(user.idToken.expirationDate, [idToken expiresAt]); } - (void)testCoding { @@ -83,4 +139,438 @@ - (void)testLegacyCoding { } #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST +// Test the old encoding format for backword compatability. +- (void)testOldFormatCoding { + if (@available(iOS 11, macOS 10.13, *)) { + OIDAuthState *authState = [OIDAuthState testInstance]; + GIDProfileData *profileDate = [GIDProfileData testInstance]; + GIDGoogleUserOldFormat *user = [[GIDGoogleUserOldFormat alloc] initWithAuthState:authState + profileData:profileDate]; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:user + requiringSecureCoding:YES + error:nil]; + GIDGoogleUser *newUser = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDGoogleUser class] + fromData:data + error:nil]; + XCTAssertEqualObjects(user, newUser); + } +} + +- (void)testUpdateAuthState { + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn + idTokenExpiresIn:kIDTokenExpiresIn]; + + NSString *updatedIDToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn]; + OIDAuthState *updatedAuthState = [OIDAuthState testInstanceWithIDToken:updatedIDToken + accessToken:kNewAccessToken + accessTokenExpiresIn:kAccessTokenExpiresIn + refreshToken:kNewRefreshToken]; + GIDProfileData *updatedProfileData = [GIDProfileData testInstance]; + + [user updateWithTokenResponse:updatedAuthState.lastTokenResponse + authorizationResponse:updatedAuthState.lastAuthorizationResponse + profileData:updatedProfileData]; + + XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken); + [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn]; + + XCTAssertEqualObjects(user.idToken.tokenString, updatedIDToken); + [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn]; + + XCTAssertEqualObjects(user.refreshToken.tokenString, kNewRefreshToken); + + XCTAssertEqual(user.profile, updatedProfileData); +} + +// When updating with a new OIDAuthState in which token information is not changed, the token objects +// should remain the same. +- (void)testUpdateAuthState_tokensAreNotChanged { + NSString *idToken = [self idTokenWithExpiresIn:kIDTokenExpiresIn]; + OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:idToken + accessToken:kAccessToken + accessTokenExpiresIn:kAccessTokenExpiresIn + refreshToken:kRefreshToken]; + + GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState profileData:nil]; + + GIDToken *accessTokenBeforeUpdate = user.accessToken; + GIDToken *refreshTokenBeforeUpdate = user.refreshToken; + GIDToken *idTokenBeforeUpdate = user.idToken; + + [user updateWithTokenResponse:authState.lastTokenResponse + authorizationResponse:authState.lastAuthorizationResponse + profileData:nil]; + + XCTAssertIdentical(user.accessToken, accessTokenBeforeUpdate); + XCTAssertIdentical(user.idToken, idTokenBeforeUpdate); + XCTAssertIdentical(user.refreshToken, refreshTokenBeforeUpdate); +} + +- (void)testFetcherAuthorizer { + // This is really hard to test without assuming how GTMAppAuthFetcherAuthorization works + // internally, so let's just take the shortcut here by asserting we get a + // GTMAppAuthFetcherAuthorization object. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn + idTokenExpiresIn:kIDTokenExpiresIn]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + id fetcherAuthorizer = user.fetcherAuthorizer; +#pragma clang diagnostic pop + XCTAssertTrue([fetcherAuthorizer isKindOfClass:[GTMAppAuthFetcherAuthorization class]]); + XCTAssertTrue([fetcherAuthorizer canAuthorize]); +} + +- (void)testFetcherAuthorizer_returnTheSameInstance { + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn + idTokenExpiresIn:kIDTokenExpiresIn]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + id fetcherAuthorizer = user.fetcherAuthorizer; + id fetcherAuthorizer2 = user.fetcherAuthorizer; +#pragma clang diagnostic pop + + XCTAssertIdentical(fetcherAuthorizer, fetcherAuthorizer2); +} + +#pragma mark - Test `refreshTokensIfNeededWithCompletion:` + +- (void)testRefreshTokensIfNeededWithCompletion_refresh_givenBothTokensExpired { + // Both tokens expired 10 seconds ago. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; + NSString *newIdToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + XCTAssertNil(error); + XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken); + [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn]; + XCTAssertEqualObjects(user.idToken.tokenString, newIdToken); + [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn]; + }]; + + // Creates a fake response. + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:newIdToken + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:nil]; + + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRefreshTokens_refresh_givenBothTokensExpired_NoNewIDToken { + // Both tokens expired 10 seconds ago. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; + // Creates a fake response without ID token. + + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + XCTAssertNil(error); + XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken); + [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn]; + XCTAssertNil(user.idToken); + }]; + + + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRefreshTokensIfNeededWithCompletion_refresh_givenAccessTokenExpired { + // Access token expired 10 seconds ago. ID token will expire in 10 minutes. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:10 * 60]; + // Creates a fake response. + NSString *newIdToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn]; + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:newIdToken + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + XCTAssertNil(error); + XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken); + [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn]; + XCTAssertEqualObjects(user.idToken.tokenString, newIdToken); + [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn]; + }]; + + + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRefreshTokensIfNeededWithCompletion_refresh_givenIDTokenExpired { + // ID token expired 10 seconds ago. Access token will expire in 10 minutes. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:10 * 60 idTokenExpiresIn:-10]; + + // Creates a fake response. + NSString *newIdToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn]; + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:newIdToken + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + XCTAssertNil(error); + XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken); + [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn]; + + XCTAssertEqualObjects(user.idToken.tokenString, newIdToken); + [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn]; + }]; + + + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenBothTokensNotExpired { + // Both tokens will expire in 10 min. + NSTimeInterval expiresIn = 10 * 60; + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:expiresIn + idTokenExpiresIn:expiresIn]; + + NSString *accessTokenStringBeforeRefresh = user.accessToken.tokenString; + NSString *idTokenStringBeforeRefresh = user.idToken.tokenString; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + XCTAssertNil(error); + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + + XCTAssertEqualObjects(user.accessToken.tokenString, accessTokenStringBeforeRefresh); + [self verifyUser:user accessTokenExpiresIn:expiresIn]; + XCTAssertEqualObjects(user.idToken.tokenString, idTokenStringBeforeRefresh); + [self verifyUser:user idTokenExpiresIn:expiresIn]; +} + +- (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenRefreshErrors { + // Both tokens expired 10 second ago. + NSTimeInterval expiresIn = -10; + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:expiresIn + idTokenExpiresIn:expiresIn]; + + NSString *accessTokenStringBeforeRefresh = user.accessToken.tokenString; + NSString *idTokenStringBeforeRefresh = user.idToken.tokenString; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + XCTAssertNotNil(error); + XCTAssertNil(user); + }]; + + _tokenFetchHandler(nil, [self fakeError]); + [self waitForExpectationsWithTimeout:1 handler:nil]; + + XCTAssertEqualObjects(user.accessToken.tokenString, accessTokenStringBeforeRefresh); + [self verifyUser:user accessTokenExpiresIn:expiresIn]; + XCTAssertEqualObjects(user.idToken.tokenString, idTokenStringBeforeRefresh); + [self verifyUser:user idTokenExpiresIn:expiresIn]; +} + +- (void)testRefreshTokensIfNeededWithCompletion_handleConcurrentRefresh { + // Both tokens expired 10 second ago. + NSTimeInterval expiresIn = -10; + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:expiresIn + idTokenExpiresIn:expiresIn]; + // Creates a fake response. + NSString *newIdToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn]; + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:newIdToken + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:nil]; + + XCTestExpectation *firstExpectation = + [self expectationWithDescription:@"First callback is called"]; + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *user, NSError *error) { + [firstExpectation fulfill]; + XCTAssertNil(error); + + XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken); + [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn]; + + XCTAssertEqualObjects(user.idToken.tokenString, newIdToken); + [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn]; + }]; + XCTestExpectation *secondExpectation = + [self expectationWithDescription:@"Second callback is called"]; + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *user, NSError *error) { + [secondExpectation fulfill]; + XCTAssertNil(error); + + XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken); + [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn]; + + XCTAssertEqualObjects(user.idToken.tokenString, newIdToken); + [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn]; + }]; + + + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +# pragma mark - Test `addScopes:` + +- (void)testAddScopes_success { + id signIn = OCMClassMock([GIDSignIn class]); + OCMStub([signIn sharedInstance]).andReturn(signIn); + [[signIn expect] addScopes:OCMOCK_ANY +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + presentingViewController:OCMOCK_ANY +#elif TARGET_OS_OSX + presentingWindow:OCMOCK_ANY +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST + completion:OCMOCK_ANY]; + + GIDGoogleUser *currentUser = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn + idTokenExpiresIn:kIDTokenExpiresIn]; + + OCMStub([signIn currentUser]).andReturn(currentUser); + + [currentUser addScopes:@[kNewScope] +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + presentingViewController:[[UIViewController alloc] init] +#elif TARGET_OS_OSX + presentingWindow:[[NSWindow alloc] init] +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST + completion:nil]; + + [signIn verify]; +} + +- (void)testAddScopes_failure_addScopesToPreviousUser { + id signIn = OCMClassMock([GIDSignIn class]); + OCMStub([signIn sharedInstance]).andReturn(signIn); + + GIDGoogleUser *currentUser = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn + idTokenExpiresIn:kIDTokenExpiresIn]; + + OCMStub([signIn currentUser]).andReturn(currentUser); + + GIDGoogleUser *previousUser = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn + idTokenExpiresIn:kNewIDTokenExpiresIn]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion is called."]; + + [previousUser addScopes:@[kNewScope] +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + presentingViewController:[[UIViewController alloc] init] +#elif TARGET_OS_OSX + presentingWindow:[[NSWindow alloc] init] +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST + completion:^(GIDUserAuth *userAuth, NSError *error) { + [expectation fulfill]; + XCTAssertNil(userAuth); + XCTAssertEqual(error.code, kGIDSignInErrorCodeMismatchWithCurrentUser); + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testAddScopes_failure_addScopesToPreviousUser_currentUserIsNull { + id signIn = OCMClassMock([GIDSignIn class]); + OCMStub([signIn sharedInstance]).andReturn(signIn); + + GIDGoogleUser *currentUser = nil; + OCMStub([signIn currentUser]).andReturn(currentUser); + + GIDGoogleUser *previousUser = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn + idTokenExpiresIn:kNewIDTokenExpiresIn]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion is called."]; + + [previousUser addScopes:@[kNewScope] +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + presentingViewController:[[UIViewController alloc] init] +#elif TARGET_OS_OSX + presentingWindow:[[NSWindow alloc] init] +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST + completion:^(GIDUserAuth *userAuth, NSError *error) { + [expectation fulfill]; + XCTAssertNil(userAuth); + XCTAssertEqual(error.code, kGIDSignInErrorCodeMismatchWithCurrentUser); + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +#pragma mark - Helpers + +// Returns a GIDGoogleUser with different tokens expiresIn time. The token strings are constants. +- (GIDGoogleUser *)googleUserWithAccessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn + idTokenExpiresIn:(NSTimeInterval)idTokenExpiresIn { + NSString *idToken = [self idTokenWithExpiresIn:idTokenExpiresIn]; + OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:idToken + accessToken:kAccessToken + accessTokenExpiresIn:accessTokenExpiresIn + refreshToken:kRefreshToken]; + + return [[GIDGoogleUser alloc] initWithAuthState:authState profileData:nil]; +} + +- (NSString *)idTokenWithExpiresIn:(NSTimeInterval)expiresIn { + // The expireTime should be based on 1970. + NSTimeInterval expireTime = [[NSDate date] timeIntervalSince1970] + expiresIn; + return [OIDTokenResponse idTokenWithSub:kUserID exp:@(expireTime)]; +} + +- (void)verifyUser:(GIDGoogleUser *)user accessTokenExpiresIn:(NSTimeInterval)expiresIn { + NSDate *expectedAccessTokenExpirationDate = [[NSDate date] dateByAddingTimeInterval:expiresIn]; + XCTAssertEqualWithAccuracy([user.accessToken.expirationDate timeIntervalSince1970], + [expectedAccessTokenExpirationDate timeIntervalSince1970], + kTimeAccuracy); +} + +- (void)verifyUser:(GIDGoogleUser *)user idTokenExpiresIn:(NSTimeInterval)expiresIn { + NSDate *expectedIDTokenExpirationDate = [[NSDate date] dateByAddingTimeInterval:expiresIn]; + XCTAssertEqualWithAccuracy([user.idToken.expirationDate timeIntervalSince1970], + [expectedIDTokenExpirationDate timeIntervalSince1970], kTimeAccuracy); +} + +- (NSError *)fakeError { + return [NSError errorWithDomain:@"fake.domain" code:-1 userInfo:nil]; +} + @end diff --git a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m index ce98c93a..cc1d63d2 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m @@ -37,8 +37,9 @@ - (void)testDefaultOptions { id presentingWindow = OCMStrictClassMock([NSWindow class]); #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST NSString *loginHint = @"login_hint"; - GIDSignInCompletion completion = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {}; - + + void (^completion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error) = + ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {}; GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration #if TARGET_OS_IOS || TARGET_OS_MACCATALYST @@ -63,9 +64,9 @@ - (void)testDefaultOptions { } - (void)testSilentOptions { - GIDSignInCompletion completion = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {}; - GIDSignInInternalOptions *options = [GIDSignInInternalOptions - silentOptionsWithCompletion:completion]; + void (^completion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error) = + ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {}; + GIDSignInInternalOptions *options = [GIDSignInInternalOptions silentOptionsWithCompletion:completion]; XCTAssertFalse(options.interactive); XCTAssertFalse(options.continuation); XCTAssertNil(options.extraParams); diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 01bb447a..c42be2de 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -26,10 +26,10 @@ // Test module imports @import GoogleSignIn; +#import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" #import "GoogleSignIn/Sources/GIDSignIn_Private.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" -#import "GoogleSignIn/Sources/GIDAuthentication_Private.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" @@ -204,9 +204,6 @@ @interface GIDSignInTest : XCTestCase { // Mock for |GIDGoogleUser|. id _user; - // Mock for |GIDAuthentication|. - id _authentication; - // Mock for |OIDAuthorizationService| id _oidAuthorizationService; @@ -238,7 +235,7 @@ @interface GIDSignInTest : XCTestCase { NSString *_hint; // The completion to be used when testing |GIDSignIn|. - GIDSignInCompletion _completion; + GIDUserAuthCompletion _completion; // The saved authorization request. OIDAuthorizationRequest *_savedAuthorizationRequest; @@ -310,7 +307,6 @@ - (void)setUp { self->_keychainRemoved = YES; }); _user = OCMStrictClassMock([GIDGoogleUser class]); - _authentication = OCMStrictClassMock([GIDAuthentication class]); _oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]); OCMStub([_oidAuthorizationService presentAuthorizationRequest:SAVE_TO_ARG_BLOCK(self->_savedAuthorizationRequest) @@ -338,10 +334,10 @@ - (void)setUp { _hint = nil; __weak GIDSignInTest *weakSelf = self; - _completion = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) { + _completion = ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) { GIDSignInTest *strongSelf = weakSelf; - if (!user) { - XCTAssertNotNil(error, @"should have an error if user is nil"); + if (!userAuth) { + XCTAssertNotNil(error, @"should have an error if the userAuth is nil"); } XCTAssertFalse(strongSelf->_completionCalled, @"callback already called"); strongSelf->_completionCalled = YES; @@ -355,7 +351,6 @@ - (void)tearDown { OCMVerifyAll(_tokenRequest); OCMVerifyAll(_authorization); OCMVerifyAll(_user); - OCMVerifyAll(_authentication); OCMVerifyAll(_oidAuthorizationService); #if TARGET_OS_IOS || TARGET_OS_MACCATALYST @@ -418,24 +413,36 @@ - (void)testInitPrivate_invalidConfig { } - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser { - [[[_authorization expect] andReturn:_authState] authState]; + [[[_authorization stub] andReturn:_authState] authState]; + [[_authorization expect] setTokenRefreshDelegate:OCMOCK_ANY]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); - OCMStub([_tokenResponse scope]).andReturn(nil); - OCMStub([_tokenResponse additionalParameters]).andReturn(nil); - OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken); - OCMStub([_tokenResponse request]).andReturn(_tokenRequest); - OCMStub([_tokenRequest additionalParameters]).andReturn(nil); + OCMStub([_authState refreshToken]).andReturn(kRefreshToken); + [[_authState expect] setStateChangeDelegate:OCMOCK_ANY]; id idTokenDecoded = OCMClassMock([OIDIDToken class]); OCMStub([idTokenDecoded alloc]).andReturn(idTokenDecoded); OCMStub([idTokenDecoded initWithIDTokenString:OCMOCK_ANY]).andReturn(idTokenDecoded); OCMStub([idTokenDecoded subject]).andReturn(kFakeGaiaID); - + + // Mock generating a GIDConfiguration when initializing GIDGoogleUser. + OIDAuthorizationResponse *authResponse = + [OIDAuthorizationResponse testInstanceWithAdditionalParameters:nil + errorString:nil]; + + OCMStub([_authState lastAuthorizationResponse]).andReturn(authResponse); + OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken); + OCMStub([_tokenResponse request]).andReturn(_tokenRequest); + OCMStub([_tokenRequest additionalParameters]).andReturn(nil); + OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken); + OCMStub([_tokenResponse accessTokenExpirationDate]).andReturn(nil); + [_signIn restorePreviousSignInNoRefresh]; [_authorization verify]; [_authState verify]; [_tokenResponse verify]; + [_tokenRequest verify]; + [idTokenDecoded verify]; XCTAssertEqual(_signIn.currentUser.userID, kFakeGaiaID); [idTokenDecoded stopMocking]; @@ -479,7 +486,7 @@ - (void)testRestorePreviousSignInWhenSignedOut { XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called."]; - [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser * _Nullable user, + [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser *_Nullable user, NSError * _Nullable error) { [expectation fulfill]; XCTAssertNotNil(error, @"error should not have been nil"); @@ -590,13 +597,13 @@ - (void)testAddScopes { id profile = OCMStrictClassMock([GIDProfileData class]); OCMStub([profile email]).andReturn(kUserEmail); - - OCMStub([_user authentication]).andReturn(_authentication); - OCMStub([_authentication clientID]).andReturn(kClientId); - OCMStub([_user serverClientID]).andReturn(nil); - OCMStub([_user hostedDomain]).andReturn(nil); - - OCMStub([_user openIDRealm]).andReturn(kOpenIDRealm); + + // Mock for the method `addScopes`. + GIDConfiguration *configuration = [[GIDConfiguration alloc] initWithClientID:kClientId + serverClientID:nil + hostedDomain:nil + openIDRealm:kOpenIDRealm]; + OCMStub([_user configuration]).andReturn(configuration); OCMStub([_user profile]).andReturn(profile); OCMStub([_user grantedScopes]).andReturn(@[kGrantedScope]); @@ -626,6 +633,8 @@ - (void)testAddScopes { NSArray *expectedScopes = @[kNewScope, kGrantedScope]; XCTAssertEqualObjects(grantedScopes, expectedScopes); + [_user verify]; + [profile verify]; [profile stopMocking]; } @@ -1075,9 +1084,9 @@ - (void)testTokenEndpointEMMError { NSError *emmError = [NSError errorWithDomain:@"anydomain" code:12345 userInfo:@{ OIDOAuthErrorFieldError : errorJSON }]; - [[_authentication expect] handleTokenFetchEMMError:emmError - completion:SAVE_TO_ARG_BLOCK(completion)]; - + id emmSupport = OCMStrictClassMock([GIDEMMSupport class]); + [[emmSupport expect] handleTokenFetchEMMError:emmError + completion:SAVE_TO_ARG_BLOCK(completion)]; [self OAuthLoginWithAddScopesFlow:NO authError:nil @@ -1091,11 +1100,12 @@ - (void)testTokenEndpointEMMError { NSError *handledError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeEMM userInfo:emmError.userInfo]; + completion(handledError); [self waitForExpectationsWithTimeout:1 handler:nil]; - [_authentication verify]; + [emmSupport verify]; XCTAssertFalse(_keychainSaved, @"should not save to keychain"); XCTAssertTrue(_completionCalled, @"should call delegate"); XCTAssertNotNil(_authError, @"should have error"); @@ -1208,6 +1218,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] accessToken:restoredSignIn ? kAccessToken : nil expiresIn:oldAccessToken ? @(300) : nil + refreshToken:kRefreshToken tokenRequest:nil]; OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc] @@ -1238,10 +1249,13 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow } } else { XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called"]; - GIDSignInCompletion completion = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) { + GIDUserAuthCompletion completion = + ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) { [expectation fulfill]; - if (!user) { - XCTAssertNotNil(error, @"should have an error if user is nil"); + if (userAuth) { + XCTAssertEqualObjects(userAuth.serverAuthCode, kServerAuthCode); + } else { + XCTAssertNotNil(error, @"Should have an error if the userAuth is nil"); } XCTAssertFalse(self->_completionCalled, @"callback already called"); self->_completionCalled = YES; @@ -1350,20 +1364,30 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow // SaveAuthCallback __block OIDAuthState *authState; + __block OIDTokenResponse *updatedTokenResponse; + __block OIDAuthorizationResponse *updatedAuthorizationResponse; __block GIDProfileData *profileData; if (keychainError) { _saveAuthorizationReturnValue = NO; } else { if (addScopesFlow) { - [[_user expect] updateAuthState:SAVE_TO_ARG_BLOCK(authState) - profileData:SAVE_TO_ARG_BLOCK(profileData)]; + [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse]; + [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; + [[_user expect] updateWithTokenResponse:SAVE_TO_ARG_BLOCK(updatedTokenResponse) + authorizationResponse:SAVE_TO_ARG_BLOCK(updatedAuthorizationResponse) + profileData:SAVE_TO_ARG_BLOCK(profileData)]; } else { [[[_user stub] andReturn:_user] alloc]; (void)[[[_user expect] andReturn:_user] initWithAuthState:SAVE_TO_ARG_BLOCK(authState) profileData:SAVE_TO_ARG_BLOCK(profileData)]; } } + + // CompletionCallback - mock server auth code parsing + if (!keychainError) { + [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; + } if (restoredSignIn && !oldAccessToken) { XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"]; @@ -1377,13 +1401,20 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow _savedTokenCallback(tokenResponse, nil); } - [_authState verify]; if (keychainError) { return; } [self waitForExpectationsWithTimeout:1 handler:nil]; + + [_authState verify]; + XCTAssertTrue(_keychainSaved, @"should save to keychain"); - XCTAssertNotNil(authState); + if (addScopesFlow) { + XCTAssertNotNil(updatedTokenResponse); + XCTAssertNotNil(updatedAuthorizationResponse); + } else { + XCTAssertNotNil(authState); + } // Check fat ID token decoding XCTAssertEqualObjects(profileData.name, kFatName); XCTAssertEqualObjects(profileData.givenName, kFatGivenName); @@ -1396,13 +1427,8 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow _keychainSaved = NO; _authError = nil; - if (!addScopesFlow) { - [[[_user expect] andReturn:_authentication] authentication]; - [[[_user expect] andReturn:_authentication] authentication]; - } - - __block GIDAuthenticationCompletion completion; - [[_authentication expect] doWithFreshTokens:SAVE_TO_ARG_BLOCK(completion)]; + __block GIDGoogleUserCompletion completion; + [[_user expect] refreshTokensIfNeededWithCompletion:SAVE_TO_ARG_BLOCK(completion)]; XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"]; @@ -1412,7 +1438,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow XCTAssertNil(error, @"should have no error"); }]; - completion(_authentication, nil); + completion(_user, nil); [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertFalse(_keychainRemoved, @"should not remove keychain"); diff --git a/GoogleSignIn/Tests/Unit/GIDTokenTest.m b/GoogleSignIn/Tests/Unit/GIDTokenTest.m new file mode 100644 index 00000000..94a8ba06 --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDTokenTest.m @@ -0,0 +1,94 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" + +#import "GoogleSignIn/Sources/GIDToken_Private.h" + +static NSString * const tokenString = @"tokenString"; +static NSString * const tokenString2 = @"tokenString2"; + +@interface GIDTokenTest : XCTestCase { + NSDate *_date; +} +@end + +@implementation GIDTokenTest + +- (void)setUp { + [super setUp]; + _date = [[NSDate alloc]initWithTimeIntervalSince1970:1000]; +} + +- (void)testInitializer { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + XCTAssertEqualObjects(token.tokenString, tokenString); + XCTAssertEqualObjects(token.expirationDate, _date); +} + +- (void)testTokensWithSameTokenStringAndExpirationDateAreEqual { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + XCTAssertEqualObjects(token, token2); +} + +- (void)testEqualTokensHaveTheSameHash { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + XCTAssertEqualObjects(token, token2); + XCTAssertEqual(token.hash, token2.hash); +} + +- (void)testTokensWithDifferentTokenStringsAreNotEqual { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString2 expirationDate:_date]; + XCTAssertNotEqualObjects(token, token2); +} + +- (void)testTokensWithSameTokenStringAndNoExpirationDateAreEqual { + GIDToken *refreshToken = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil]; + GIDToken *refreshToken2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil]; + XCTAssertEqualObjects(refreshToken, refreshToken2); +} + +- (void)testTokensWithSameTokenStringAndDifferentExpirationDateAreNotEqual { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + NSDate *date2 = [[NSDate alloc]initWithTimeIntervalSince1970:2000]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:date2]; + XCTAssertNotEqualObjects(token, token2); +} + +- (void)testTokensWithSameTokenStringAndOneHasNoExpirationDateAreNotEqual { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil]; + XCTAssertNotEqualObjects(token, token2); +} + +- (void)testCoding { + if (@available(iOS 11, macOS 10.13, *)) { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:token requiringSecureCoding:YES error:nil]; + GIDToken *newToken = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDToken class] + fromData:data + error:nil]; + XCTAssertEqualObjects(token, newToken); + + XCTAssertTrue([GIDToken supportsSecureCoding]); + } else { + XCTSkip(@"Required API is not available for this test."); + } +} + +@end diff --git a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h index bc97e6d2..15feb758 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h @@ -28,4 +28,15 @@ + (instancetype)testInstanceWithTokenResponse:(OIDTokenResponse *)tokenResponse; +/** + * @idToken The ID token. + * @accessToken The access token string. + * @accessTokenExipresIn The life time of the access token starting from the moment when `OIDTokenResponse` is created. + * @refreshToken The refresh token string. + */ ++ (instancetype)testInstanceWithIDToken:(NSString *)idToken + accessToken:(NSString *)accessToken + accessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn + refreshToken:(NSString *)refreshToken; + @end diff --git a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m index a58994da..c6c694a4 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m @@ -33,4 +33,17 @@ + (instancetype)testInstanceWithTokenResponse:(OIDTokenResponse *)tokenResponse tokenResponse:tokenResponse]; } ++ (instancetype)testInstanceWithIDToken:(NSString *)idToken + accessToken:(NSString *)accessToken + accessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn + refreshToken:(NSString *)refreshToken { + OIDTokenResponse *newResponse = + [OIDTokenResponse testInstanceWithIDToken:idToken + accessToken:accessToken + expiresIn:@(accessTokenExpiresIn) + refreshToken:refreshToken + tokenRequest:nil]; + return [self testInstanceWithTokenResponse:newResponse]; +} + @end diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h index 990fda0f..b565a392 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h @@ -56,12 +56,17 @@ extern NSString * const kFatPictureURL; + (instancetype)testInstanceWithIDToken:(NSString *)idToken accessToken:(NSString *)accessToken expiresIn:(NSNumber *)expiresIn + refreshToken:(NSString *)refreshToken tokenRequest:(OIDTokenRequest *)tokenRequest; + (NSString *)idToken; + (NSString *)fatIDToken; +/** + * @sub The subject of the ID token. + * @exp The interval between 00:00:00 UTC on 1 January 1970 and the expiration date of the ID token. + */ + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp; + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat; diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m index b8a5974d..49823be7 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m @@ -61,19 +61,21 @@ + (instancetype)testInstanceWithIDToken:(NSString *)idToken { return [OIDTokenResponse testInstanceWithIDToken:idToken accessToken:nil expiresIn:nil + refreshToken:nil tokenRequest:nil]; } + (instancetype)testInstanceWithIDToken:(NSString *)idToken accessToken:(NSString *)accessToken expiresIn:(NSNumber *)expiresIn + refreshToken:(NSString *)refreshToken tokenRequest:(OIDTokenRequest *)tokenRequest { NSMutableDictionary *parameters; parameters = [[NSMutableDictionary alloc] initWithDictionary:@{ @"access_token" : accessToken ?: kAccessToken, @"expires_in" : expiresIn ?: @(kAccessTokenExpiresIn), @"token_type" : @"example_token_type", - @"refresh_token" : kRefreshToken, + @"refresh_token" : refreshToken ?: kRefreshToken, @"scope" : [OIDScopeUtilities scopesWithArray:@[ OIDAuthorizationRequestTestingScope2 ]], @"server_code" : kServerAuthCode, }]; @@ -112,7 +114,7 @@ + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat { NSMutableDictionary *payloadContents = [NSMutableDictionary dictionaryWithDictionary:@{ @"sub" : sub, - @"hd" : kHostedDomain, + @"hd" : kHostedDomain, @"iss" : kIssuer, @"aud" : kAudience, @"exp" : exp, diff --git a/Samples/ObjC/SignInSample/Source/AuthInspectorViewController.m b/Samples/ObjC/SignInSample/Source/AuthInspectorViewController.m index bfa10d5c..a0594514 100644 --- a/Samples/ObjC/SignInSample/Source/AuthInspectorViewController.m +++ b/Samples/ObjC/SignInSample/Source/AuthInspectorViewController.m @@ -36,13 +36,12 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { _keyPaths = @[ - @"authentication.accessToken", - @"authentication.accessTokenExpirationDate", - @"authentication.refreshToken", - @"authentication.idToken", + @"accessToken.tokenString", + @"accessToken.expirationDate", + @"refreshToken.tokenString", + @"idToken.tokenString", @"grantedScopes", @"userID", - @"serverAuthCode", @"profile.email", @"profile.name", ]; @@ -140,4 +139,3 @@ - (CGFloat)heightForTableView:(UITableView *)tableView content:(NSString *)conte } @end - diff --git a/Samples/ObjC/SignInSample/Source/SignInViewController.m b/Samples/ObjC/SignInSample/Source/SignInViewController.m index 5b3f1651..8524bb26 100644 --- a/Samples/ObjC/SignInSample/Source/SignInViewController.m +++ b/Samples/ObjC/SignInSample/Source/SignInViewController.m @@ -175,7 +175,7 @@ - (CGFloat)minimumButtonWidth { - (void)reportAuthStatus { GIDGoogleUser *googleUser = [GIDSignIn.sharedInstance currentUser]; - if (googleUser.authentication) { + if (googleUser) { _signInAuthStatus.text = @"Status: Authenticated"; } else { // To authenticate, use Google Sign-In button. @@ -188,7 +188,7 @@ - (void)reportAuthStatus { // Update the interface elements containing user data to reflect the // currently signed in user. - (void)refreshUserInfo { - if (GIDSignIn.sharedInstance.currentUser.authentication == nil) { + if (!GIDSignIn.sharedInstance.currentUser) { self.userName.text = kPlaceholderUserName; self.userEmailAddress.text = kPlaceholderEmailAddress; self.userAvatar.image = [UIImage imageNamed:kPlaceholderAvatarImageName]; @@ -248,7 +248,7 @@ - (void)updateButtons { - (IBAction)signIn:(id)sender { [GIDSignIn.sharedInstance signInWithPresentingViewController:self - completion:^(GIDGoogleUser *user, + completion:^(GIDUserAuth *userAuth, NSError *error) { if (error) { self->_signInAuthStatus.text = @@ -280,9 +280,11 @@ - (IBAction)disconnect:(id)sender { } - (IBAction)addScopes:(id)sender { - [GIDSignIn.sharedInstance addScopes:@[ @"https://www.googleapis.com/auth/user.birthday.read" ] - presentingViewController:self - completion:^(GIDGoogleUser *user, NSError *error) { + GIDGoogleUser *currentUser = GIDSignIn.sharedInstance.currentUser; + [currentUser addScopes:@[ @"https://www.googleapis.com/auth/user.birthday.read" ] + presentingViewController:self + completion:^(GIDUserAuth *_Nullable userAuth, + NSError *_Nullable error) { if (error) { self->_signInAuthStatus.text = [NSString stringWithFormat:@"Status: Failed to add scopes: %@", error]; diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift index 1dc4dea4..7692cb22 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift @@ -42,8 +42,8 @@ final class BirthdayLoader: ObservableObject { guard let accessToken = GIDSignIn .sharedInstance .currentUser? - .authentication - .accessToken else { return nil } + .accessToken + .tokenString else { return nil } let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = [ "Authorization": "Bearer \(accessToken)" @@ -52,9 +52,8 @@ final class BirthdayLoader: ObservableObject { }() private func sessionWithFreshToken(completion: @escaping (Result) -> Void) { - let authentication = GIDSignIn.sharedInstance.currentUser?.authentication - authentication?.do { auth, error in - guard let token = auth?.accessToken else { + GIDSignIn.sharedInstance.currentUser?.refreshTokensIfNeeded { user, error in + guard let token = user?.accessToken.tokenString else { completion(.failure(.couldNotCreateURLSession(error))) return } diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift index 00c144b7..93c29a6f 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift @@ -36,12 +36,12 @@ final class GoogleSignInAuthenticator: ObservableObject { return } - GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { user, error in - guard let user = user else { + GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { userAuth, error in + guard let userAuth = userAuth else { print("Error! \(String(describing: error))") return } - self.authViewModel.state = .signedIn(user) + self.authViewModel.state = .signedIn(userAuth.user) } #elseif os(macOS) @@ -50,12 +50,12 @@ final class GoogleSignInAuthenticator: ObservableObject { return } - GIDSignIn.sharedInstance.signIn(withPresenting: presentingWindow) { user, error in - guard let user = user else { + GIDSignIn.sharedInstance.signIn(withPresenting: presentingWindow) { userAuth, error in + guard let userAuth = userAuth else { print("Error! \(String(describing: error))") return } - self.authViewModel.state = .signedIn(user) + self.authViewModel.state = .signedIn(userAuth.user) } #endif } @@ -83,20 +83,24 @@ final class GoogleSignInAuthenticator: ObservableObject { /// - note: Successful requests will update the `authViewModel.state` with a new current user that /// has the granted scope. func addBirthdayReadScope(completion: @escaping () -> Void) { + guard let currentUser = GIDSignIn.sharedInstance.currentUser else { + fatalError("No user signed in!") + } + #if os(iOS) guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else { fatalError("No root view controller!") } - GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope], - presenting: rootViewController) { user, error in + currentUser.addScopes([BirthdayLoader.birthdayReadScope], + presenting: rootViewController) { userAuth, error in if let error = error { print("Found error while adding birthday read scope: \(error).") return } - guard let currentUser = user else { return } - self.authViewModel.state = .signedIn(currentUser) + guard let userAuth = userAuth else { return } + self.authViewModel.state = .signedIn(userAuth.user) completion() } @@ -105,15 +109,15 @@ final class GoogleSignInAuthenticator: ObservableObject { fatalError("No presenting window!") } - GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope], - presenting: presentingWindow) { user, error in + currentUser.addScopes([BirthdayLoader.birthdayReadScope], + presenting: presentingWindow) { userAuth, error in if let error = error { print("Found error while adding birthday read scope: \(error).") return } - guard let currentUser = user else { return } - self.authViewModel.state = .signedIn(currentUser) + guard let userAuth = userAuth else { return } + self.authViewModel.state = .signedIn(userAuth.user) completion() } From 46c6dc7de336f3e6369fa37438a955a023fe5f30 Mon Sep 17 00:00:00 2001 From: Peter Andrews Date: Thu, 10 Nov 2022 10:48:25 -0800 Subject: [PATCH 02/10] Add badge for integration tests (#254) --- .github/workflows/{tests.yml => unit_tests.yml} | 2 +- README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename .github/workflows/{tests.yml => unit_tests.yml} (99%) diff --git a/.github/workflows/tests.yml b/.github/workflows/unit_tests.yml similarity index 99% rename from .github/workflows/tests.yml rename to .github/workflows/unit_tests.yml index 0b3f4fbd..09b1d6f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/unit_tests.yml @@ -1,4 +1,4 @@ -name: tests +name: unit_tests on: push: diff --git a/README.md b/README.md index 09056612..acd9acae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ [![Version](https://img.shields.io/cocoapods/v/GoogleSignIn.svg?style=flat)](https://cocoapods.org/pods/GoogleSignIn) [![Platform](https://img.shields.io/cocoapods/p/GoogleSignIn.svg?style=flat)](https://cocoapods.org/pods/GoogleSignIn) [![License](https://img.shields.io/cocoapods/l/GoogleSignIn.svg?style=flat)](https://cocoapods.org/pods/GoogleSignIn) -[![tests](https://github.com/google/GoogleSignIn-iOS/actions/workflows/tests.yml/badge.svg?event=push)](https://github.com/google/GoogleSignIn-iOS/actions/workflows/tests.yml) +[![unit_tests](https://github.com/google/GoogleSignIn-iOS/actions/workflows/unit_tests.yml/badge.svg?branch=main)](https://github.com/google/GoogleSignIn-iOS/actions/workflows/unit_tests.yml) +[![integration_tests](https://github.com/google/GoogleSignIn-iOS/actions/workflows/integration_tests.yml/badge.svg?branch=main)](https://github.com/google/GoogleSignIn-iOS/actions/workflows/integration_tests.yml) # Google Sign-In for iOS and macOS From eade6df4163f2415523f6fdc1e1d64fbce3cf0c4 Mon Sep 17 00:00:00 2001 From: Peter Andrews Date: Fri, 18 Nov 2022 14:40:50 -0800 Subject: [PATCH 03/10] Allow GTMSessionFetcher 3.x (#257) --- GoogleSignIn.podspec | 6 ++--- GoogleSignIn/Sources/GIDEMMErrorHandler.m | 8 +----- .../Tests/Unit/GIDEMMErrorHandlerTest.m | 6 +---- Package.swift | 6 ++--- .../SignInSample.xcodeproj/project.pbxproj | 25 +++++-------------- .../project.pbxproj | 8 +++--- 6 files changed, 18 insertions(+), 41 deletions(-) diff --git a/GoogleSignIn.podspec b/GoogleSignIn.podspec index 2113f684..5bbcf94b 100644 --- a/GoogleSignIn.podspec +++ b/GoogleSignIn.podspec @@ -12,7 +12,7 @@ The Google Sign-In SDK allows users to sign in with their Google account from th :git => 'https://github.com/google/GoogleSignIn-iOS.git', :tag => s.version.to_s } - ios_deployment_target = '9.0' + ios_deployment_target = '10.0' osx_deployment_target = '10.15' s.ios.deployment_target = ios_deployment_target s.osx.deployment_target = osx_deployment_target @@ -33,8 +33,8 @@ The Google Sign-In SDK allows users to sign in with their Google account from th s.ios.framework = 'UIKit' s.osx.framework = 'AppKit' s.dependency 'AppAuth', '~> 1.5' - s.dependency 'GTMAppAuth', '~> 1.3' - s.dependency 'GTMSessionFetcher/Core', '>= 1.1', '< 3.0' + s.dependency 'GTMAppAuth', '>= 1.3', '< 3.0' + s.dependency 'GTMSessionFetcher/Core', '>= 1.1', '< 4.0' s.resource_bundle = { 'GoogleSignIn' => ['GoogleSignIn/Sources/{Resources,Strings}/*'] } diff --git a/GoogleSignIn/Sources/GIDEMMErrorHandler.m b/GoogleSignIn/Sources/GIDEMMErrorHandler.m index 4e74b1fe..1429a435 100644 --- a/GoogleSignIn/Sources/GIDEMMErrorHandler.m +++ b/GoogleSignIn/Sources/GIDEMMErrorHandler.m @@ -267,13 +267,7 @@ - (UIAlertController *)appVerificationRequiredAlertWithURL:(nullable NSURL *)url } - (void)openURL:(NSURL *)url { - if (@available(iOS 10, *)) { - [UIApplication.sharedApplication openURL:url options:@{} completionHandler:nil]; - } else { -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0 - [UIApplication.sharedApplication openURL:url]; -#endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0 - } + [UIApplication.sharedApplication openURL:url options:@{} completionHandler:nil]; } #pragma mark - Localization diff --git a/GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m b/GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m index 5f714f0a..0b3a5685 100644 --- a/GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m +++ b/GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m @@ -114,11 +114,7 @@ - (void)expectOpenURLString:(NSString *)urlString inAction:(void (^)(void))actio selector:@selector(sharedApplication) isClassSelector:YES withBlock:^() { return mockApplication; }]; - if (@available(iOS 10, *)) { - [[mockApplication expect] openURL:[NSURL URLWithString:urlString] options:@{} completionHandler:nil]; - } else { - [[mockApplication expect] openURL:[NSURL URLWithString:urlString]]; - } + [[mockApplication expect] openURL:[NSURL URLWithString:urlString] options:@{} completionHandler:nil]; action(); [mockApplication verify]; [GULSwizzler unswizzleClass:[UIApplication class] diff --git a/Package.swift b/Package.swift index db9a0d38..88b82d57 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let package = Package( defaultLocalization: "en", platforms: [ .macOS(.v10_15), - .iOS(.v9) + .iOS(.v10) ], products: [ .library( @@ -48,11 +48,11 @@ let package = Package( .package( name: "GTMAppAuth", url: "https://github.com/google/GTMAppAuth.git", - "1.3.0" ..< "2.0.0"), + "1.3.0" ..< "3.0.0"), .package( name: "GTMSessionFetcher", url: "https://github.com/google/gtm-session-fetcher.git", - "1.5.0" ..< "3.0.0"), + "1.5.0" ..< "4.0.0"), .package( name: "OCMock", url: "https://github.com/firebase/ocmock.git", diff --git a/Samples/ObjC/SignInSample/SignInSample.xcodeproj/project.pbxproj b/Samples/ObjC/SignInSample/SignInSample.xcodeproj/project.pbxproj index 6879f565..cff9da77 100644 --- a/Samples/ObjC/SignInSample/SignInSample.xcodeproj/project.pbxproj +++ b/Samples/ObjC/SignInSample/SignInSample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + C1D4BC8A2926EF61001BCB24 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = C1D4BC892926EF61001BCB24 /* GoogleSignIn */; }; C1E69232264315E7004CE2BC /* SignInViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C1E69226264315E7004CE2BC /* SignInViewController.m */; }; C1E69233264315E7004CE2BC /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1E69228264315E7004CE2BC /* LaunchScreen.xib */; }; C1E69234264315E7004CE2BC /* DataPickerState.m in Sources */ = {isa = PBXBuildFile; fileRef = C1E6922A264315E7004CE2BC /* DataPickerState.m */; }; @@ -15,7 +16,6 @@ C1E69237264315E7004CE2BC /* SignInViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1E6922D264315E7004CE2BC /* SignInViewController.xib */; }; C1E69238264315E7004CE2BC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = C1E6922F264315E7004CE2BC /* AppDelegate.m */; }; C1E69239264315E7004CE2BC /* AuthInspectorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C1E69231264315E7004CE2BC /* AuthInspectorViewController.m */; }; - C1E6924426431A6D004CE2BC /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = C1E6924326431A6D004CE2BC /* GoogleSignIn */; }; C1E6927C26431E13004CE2BC /* Dummy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1E6925326431E12004CE2BC /* Dummy.strings */; }; C1E6927D26431E13004CE2BC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C1E6926E26431E13004CE2BC /* Images.xcassets */; }; C1E6927E26431E13004CE2BC /* DataPickerDictionary.plist in Resources */ = {isa = PBXBuildFile; fileRef = C1E6927026431E13004CE2BC /* DataPickerDictionary.plist */; }; @@ -92,7 +92,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C1E6924426431A6D004CE2BC /* GoogleSignIn in Frameworks */, + C1D4BC8A2926EF61001BCB24 /* GoogleSignIn in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -188,7 +188,7 @@ ); name = SignInSample; packageProductDependencies = ( - C1E6924326431A6D004CE2BC /* GoogleSignIn */, + C1D4BC892926EF61001BCB24 /* GoogleSignIn */, ); productName = SignInSample; productReference = D99924CA1A92B3C7008CC226 /* SignInSample.app */; @@ -253,7 +253,6 @@ ); mainGroup = D99924C11A92B3C7008CC226; packageReferences = ( - C1E6924226431A6D004CE2BC /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, ); productRefGroup = D99924CB1A92B3C7008CC226 /* Products */; projectDirPath = ""; @@ -392,7 +391,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "-ObjC"; @@ -442,7 +441,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; OTHER_LDFLAGS = "-ObjC"; SDKROOT = iphoneos; @@ -510,21 +509,9 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - C1E6924226431A6D004CE2BC /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/google/GoogleSignIn-iOS.git"; - requirement = { - branch = main; - kind = branch; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ - C1E6924326431A6D004CE2BC /* GoogleSignIn */ = { + C1D4BC892926EF61001BCB24 /* GoogleSignIn */ = { isa = XCSwiftPackageProductDependency; - package = C1E6924226431A6D004CE2BC /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; productName = GoogleSignIn; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Samples/ObjC/SignInSample/SignInSampleForPod.xcodeproj/project.pbxproj b/Samples/ObjC/SignInSample/SignInSampleForPod.xcodeproj/project.pbxproj index e98bb017..154116db 100644 --- a/Samples/ObjC/SignInSample/SignInSampleForPod.xcodeproj/project.pbxproj +++ b/Samples/ObjC/SignInSample/SignInSampleForPod.xcodeproj/project.pbxproj @@ -433,7 +433,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -483,7 +483,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -497,7 +497,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/SignInSample-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.google.SignInSample; PRODUCT_NAME = SignInSample; @@ -510,7 +510,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/SignInSample-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.google.SignInSample; PRODUCT_NAME = SignInSample; From dbe49dc1bb9e1e17183a165842c8dc295413e0b3 Mon Sep 17 00:00:00 2001 From: pinlu Date: Mon, 28 Nov 2022 14:04:41 -0800 Subject: [PATCH 04/10] Fix macro in GIDGoogleUser.m (#259) --- GoogleSignIn/Sources/GIDGoogleUser.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GoogleSignIn/Sources/GIDGoogleUser.m b/GoogleSignIn/Sources/GIDGoogleUser.m index 4291ba38..c1112ea8 100644 --- a/GoogleSignIn/Sources/GIDGoogleUser.m +++ b/GoogleSignIn/Sources/GIDGoogleUser.m @@ -184,11 +184,11 @@ - (OIDAuthState *) authState{ } - (void)addScopes:(NSArray *)scopes -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST presentingViewController:(UIViewController *)presentingViewController -#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST +#elif TARGET_OS_OSX presentingWindow:(NSWindow *)presentingWindow -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error))completion { if (self != GIDSignIn.sharedInstance.currentUser) { From cc589b49f8d1ffb66d1c621c4f37d9a50b2585e7 Mon Sep 17 00:00:00 2001 From: pinlu Date: Mon, 28 Nov 2022 14:22:16 -0800 Subject: [PATCH 05/10] Improve documentation for method `handleURL:` in GIDSignIn public API (#246) --- GoogleSignIn/Sources/GIDSignIn.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index b388bf56..4690781e 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -170,6 +170,10 @@ @implementation GIDSignIn { #pragma mark - Public methods +// Handles the custom scheme URL opened by SFSafariViewController or the Device Policy App. +// +// For SFSafariViewController invoked via AppAuth, this method is used on iOS 10. +// For the Device Policy App (EMM flow) this method is used on all iOS versions. - (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]) { From e587132bbca5f56a4ac8ed1895f6ac8f191baa7d Mon Sep 17 00:00:00 2001 From: pinlu Date: Tue, 29 Nov 2022 12:21:43 -0800 Subject: [PATCH 06/10] Follow up: Fix macro in GIDGoogleUser.m (#263) Co-authored-by: Peter Andrews --- GoogleSignIn/Sources/GIDGoogleUser.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GoogleSignIn/Sources/GIDGoogleUser.m b/GoogleSignIn/Sources/GIDGoogleUser.m index c1112ea8..d6ab89b7 100644 --- a/GoogleSignIn/Sources/GIDGoogleUser.m +++ b/GoogleSignIn/Sources/GIDGoogleUser.m @@ -204,11 +204,11 @@ - (void)addScopes:(NSArray *)scopes } [GIDSignIn.sharedInstance addScopes:scopes -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST presentingViewController:presentingViewController -#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST +#elif TARGET_OS_OSX presentingWindow:presentingWindow -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST completion:completion]; } From 88f02926dae68923312bac97525fce294e267855 Mon Sep 17 00:00:00 2001 From: Peter Andrews Date: Wed, 30 Nov 2022 09:29:14 -0800 Subject: [PATCH 07/10] Add additional triggers for builds workflow (#264) --- .github/workflows/builds.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index e166e164..d5f38a14 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -1,6 +1,11 @@ name: Build GSI for Valid Architectures on: + push: + branches: + - main + pull_request: + workflow_dispatch: schedule: - cron: '0 8 * * *' # Cron uses UTC; run at nightly at midnight PST From a140927c3dbc92e3668846c0d0918a51d8c361e2 Mon Sep 17 00:00:00 2001 From: Peter Andrews Date: Wed, 30 Nov 2022 12:44:29 -0800 Subject: [PATCH 08/10] Bump version for the 7.0.0 release (#251) --- GoogleSignIn.podspec | 2 +- GoogleSignInSwiftSupport.podspec | 4 ++-- Package.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GoogleSignIn.podspec b/GoogleSignIn.podspec index 5bbcf94b..b82e5dcc 100644 --- a/GoogleSignIn.podspec +++ b/GoogleSignIn.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleSignIn' - s.version = '6.2.4' + s.version = '7.0.0' s.summary = 'Enables iOS apps to sign in with Google.' s.description = <<-DESC The Google Sign-In SDK allows users to sign in with their Google account from third-party apps. diff --git a/GoogleSignInSwiftSupport.podspec b/GoogleSignInSwiftSupport.podspec index 73a6ccaa..d0b1e0c3 100644 --- a/GoogleSignInSwiftSupport.podspec +++ b/GoogleSignInSwiftSupport.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleSignInSwiftSupport' - s.version = '6.2.4' + s.version = '7.0.0' s.swift_version = '4.0' s.summary = 'Adds Swift-focused support for Google Sign-In.' s.description = 'Additional Swift support for the Google Sign-In SDK.' @@ -24,7 +24,7 @@ Pod::Spec.new do |s| 'CoreGraphics', 'SwiftUI', ] - s.dependency 'GoogleSignIn', '~> 6.2' + s.dependency 'GoogleSignIn', '~> 7.0' s.test_spec 'unit' do |unit_tests| unit_tests.platforms = { :ios => ios_deployment_target, diff --git a/Package.swift b/Package.swift index 88b82d57..a8b794f9 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ import PackageDescription -let googleSignInVersion = "6.2.4" +let googleSignInVersion = "7.0.0" let package = Package( name: "GoogleSignIn", From da72bf9a8c2d96db341207ee55adc718a406f4ae Mon Sep 17 00:00:00 2001 From: Peter Andrews Date: Wed, 7 Dec 2022 14:55:26 -0800 Subject: [PATCH 09/10] Remove `GIDDisconnectCompletion` (#267) --- GoogleSignIn/Sources/GIDSignIn_Private.h | 3 +++ GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 39df848f..811a6b28 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -33,6 +33,9 @@ NS_ASSUME_NONNULL_BEGIN /// was unsuccessful. typedef void (^GIDUserAuthCompletion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error); +/// Represents a completion block that takes an error if the operation was unsuccessful. +typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); + // Private |GIDSignIn| methods that are used internally in this SDK and other Google SDKs. @interface GIDSignIn () diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index 5abf4f6f..c25442af 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -51,9 +51,6 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { kGIDSignInErrorCodeMismatchWithCurrentUser = -9, }; -/// Represents a completion block that takes an error if the operation was unsuccessful. -typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); - /// This class signs the user in with Google. /// /// For reference, please see "Google Sign-In for iOS" at @@ -102,9 +99,9 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); /// Disconnects the current user from the app and revokes previous authentication. If the operation /// succeeds, the OAuth 2.0 token is also removed from keychain. /// -/// @param completion The optional `GIDDisconnectCompletion` block that is called on completion. +/// @param completion The optional block that is called on completion. /// This block will be called asynchronously on the main queue. -- (void)disconnectWithCompletion:(nullable GIDDisconnectCompletion)completion; +- (void)disconnectWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; #if TARGET_OS_IOS || TARGET_OS_MACCATALYST /// Starts an interactive sign-in flow on iOS. From f43447330e5a2a2023831e0eb6386241b429d843 Mon Sep 17 00:00:00 2001 From: Peter Andrews Date: Thu, 8 Dec 2022 11:25:01 -0800 Subject: [PATCH 10/10] Use a consistent approach to extension availability (#269) --- GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index c25442af..29453cda 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -160,7 +160,8 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth, - NSError *_Nullable error))completion; + NSError *_Nullable error))completion + NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); #elif TARGET_OS_OSX /// Starts an interactive sign-in flow on macOS.