diff --git a/GoogleSignIn/Sources/GIDAuthentication_Private.h b/GoogleSignIn/Sources/GIDAuthentication.h similarity index 67% rename from GoogleSignIn/Sources/GIDAuthentication_Private.h rename to GoogleSignIn/Sources/GIDAuthentication.h index 113728bf..e38ee151 100644 --- a/GoogleSignIn/Sources/GIDAuthentication_Private.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,15 +14,16 @@ * limitations under the License. */ -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h" +#import + +@class OIDAuthState; NS_ASSUME_NONNULL_BEGIN -// Internal methods for the class that are not part of the public API. -@interface GIDAuthentication () +// Internal class for GIDGoogleUser NSCoding backward compatibility. +@interface GIDAuthentication : NSObject -// A representation of the state of the OAuth session for this instance. -@property(nonatomic, readonly) OIDAuthState *authState; +@property(nonatomic) OIDAuthState* authState; - (instancetype)initWithAuthState:(OIDAuthState *)authState; diff --git a/GoogleSignIn/Sources/GIDAuthentication.m b/GoogleSignIn/Sources/GIDAuthentication.m index 927be7a1..d8a400f2 100644 --- a/GoogleSignIn/Sources/GIDAuthentication.m +++ b/GoogleSignIn/Sources/GIDAuthentication.m @@ -1,240 +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"; - -@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 - Public methods - -- (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 - NSSecureCoding + (BOOL)supportsSecureCoding { @@ -244,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/GIDGoogleUser.m b/GoogleSignIn/Sources/GIDGoogleUser.m index 241798c2..0c18a49e 100644 --- a/GoogleSignIn/Sources/GIDGoogleUser.m +++ b/GoogleSignIn/Sources/GIDGoogleUser.m @@ -19,9 +19,10 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" #import "GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h" -#import "GoogleSignIn/Sources/GIDAuthentication_Private.h" +#import "GoogleSignIn/Sources/GIDAuthentication.h" #import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDProfileData_Private.h" +#import "GoogleSignIn/Sources/GIDSignInPreferences.h" #import "GoogleSignIn/Sources/GIDToken_Private.h" #ifdef SWIFT_PACKAGE @@ -36,9 +37,8 @@ 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"; @@ -47,16 +47,21 @@ // 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 *idTokenString = self.idToken.tokenString; if (idTokenString) { - OIDIDToken *idTokenDecoded = - [[OIDIDToken alloc] initWithIDTokenString:idTokenString]; + OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString]; if (idTokenDecoded && idTokenDecoded.subject) { return [idTokenDecoded.subject copy]; } @@ -66,7 +71,7 @@ - (nullable NSString *)userID { - (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. @@ -86,11 +91,11 @@ - (GIDConfiguration *)configuration { @synchronized(self) { // Caches the configuration since it would not change for one GIDGoogleUser instance. if (!_cachedConfiguration) { - NSString *clientID = _authState.lastAuthorizationResponse.request.clientID; + NSString *clientID = self.authState.lastAuthorizationResponse.request.clientID; NSString *serverClientID = - _authState.lastTokenResponse.request.additionalParameters[kAudienceParameter]; + self.authState.lastTokenResponse.request.additionalParameters[kAudienceParameter]; NSString *openIDRealm = - _authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter]; + self.authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter]; _cachedConfiguration = [[GIDConfiguration alloc] initWithClientID:clientID serverClientID:serverClientID @@ -101,12 +106,87 @@ - (GIDConfiguration *)configuration { return _cachedConfiguration; } +- (void)doWithFreshTokens:(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; +} + #pragma mark - Private Methods #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - (nullable NSString *)emmSupport { - return - _authState.lastAuthorizationResponse.request.additionalParameters[kEMMSupportParameterName]; + return self.authState.lastAuthorizationResponse + .request.additionalParameters[kEMMSupportParameterName]; } #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST @@ -114,31 +194,41 @@ - (instancetype)initWithAuthState:(OIDAuthState *)authState profileData:(nullable GIDProfileData *)profileData { self = [super init]; if (self) { - [self updateAuthState:authState profileData:profileData]; - } - return self; -} - -- (void)updateAuthState:(OIDAuthState *)authState - profileData:(nullable GIDProfileData *)profileData { - @synchronized(self) { - _authState = authState; - _authentication = [[GIDAuthentication alloc] initWithAuthState:authState]; + _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]; + [[GIDAppAuthFetcherAuthorizationWithEMMSupport alloc] initWithAuthState:authState] : + [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState]; #elif TARGET_OS_OSX || TARGET_OS_MACCATALYST GTMAppAuthFetcherAuthorization *authorization = - [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:_authState]; + [[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)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 { @@ -195,6 +285,12 @@ - (nullable NSDictionary *)additionalRefreshParameters: #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST } +#pragma mark - OIDAuthStateChangeDelegate + +- (void)didChangeState:(OIDAuthState *)state { + [self updateTokensWithAuthState:state]; +} + #pragma mark - NSSecureCoding + (BOOL)supportsSecureCoding { @@ -204,24 +300,26 @@ + (BOOL)supportsSecureCoding { - (nullable instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; if (self) { - GIDProfileData *profileData = + GIDProfileData *profile = [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey]; + OIDAuthState *authState; - if ([decoder containsValueForKey:kAuthState]) { // Current encoding - authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState]; + if ([decoder containsValueForKey:kAuthStateKey]) { // Current encoding + authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthStateKey]; } else { // Old encoding GIDAuthentication *authentication = [decoder decodeObjectOfClass:[GIDAuthentication class] - forKey:kAuthenticationKey]; + forKey:@"authentication"]; authState = authentication.authState; } - [self updateAuthState:authState profileData:profileData]; + + 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 3687bfa0..94a019ae 100644 --- a/GoogleSignIn/Sources/GIDGoogleUser_Private.h +++ b/GoogleSignIn/Sources/GIDGoogleUser_Private.h @@ -24,12 +24,16 @@ #import #endif +@class OIDAuthState; + NS_ASSUME_NONNULL_BEGIN -@class OIDAuthState; +/// 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; @@ -37,6 +41,9 @@ NS_ASSUME_NONNULL_BEGIN @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; @@ -52,8 +59,9 @@ NS_ASSUME_NONNULL_BEGIN 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 e84682ee..c8504ac5 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -16,7 +16,6 @@ #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" @@ -33,7 +32,6 @@ #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" @@ -183,7 +181,7 @@ - (BOOL)handleURL:(NSURL *)url { } - (BOOL)hasPreviousSignIn { - if ([_currentUser.authentication.authState isAuthorized]) { + if ([_currentUser.authState isAuthorized]) { return YES; } OIDAuthState *authState = [self loadAuthState]; @@ -218,7 +216,8 @@ - (BOOL)restorePreviousSignInNoRefresh { [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; GIDProfileData *profileData = [self profileDataWithIDToken:idToken]; - GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState profileData:profileData]; + GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState + profileData:profileData]; [self setCurrentUserWithKVO:user]; return YES; } @@ -415,8 +414,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. @@ -536,8 +534,8 @@ - (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 doWithFreshTokens:^(GIDGoogleUser *unused, NSError *error) { if (error) { [self authenticateWithOptions:options]; } else { @@ -788,8 +786,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]; @@ -864,8 +863,10 @@ - (void)addCompletionCallback:(GIDAuthFlow *)authFlow { 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]; + 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/Public/GoogleSignIn/GIDAuthentication.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h deleted file mode 100644 index cba4cac6..00000000 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h +++ /dev/null @@ -1,70 +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; - -/// 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 39176735..558d9a9f 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h @@ -24,7 +24,6 @@ #import #endif -@class GIDAuthentication; @class GIDConfiguration; @class GIDToken; @class GIDProfileData; @@ -40,9 +39,6 @@ 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; @@ -67,6 +63,14 @@ NS_ASSUME_NONNULL_BEGIN @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)doWithFreshTokens:(void (^)(GIDGoogleUser *_Nullable user, + NSError *_Nullable error))completion; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h index d1b7afda..1b9d1042 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h @@ -15,7 +15,6 @@ */ #import -#import "GIDAuthentication.h" #import "GIDConfiguration.h" #import "GIDGoogleUser.h" #import "GIDProfileData.h" diff --git a/GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h b/GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h deleted file mode 100644 index 30672902..00000000 --- a/GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h +++ /dev/null @@ -1,25 +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/Sources/Public/GoogleSignIn/GIDAuthentication.h" - -@interface GIDAuthentication (Testing) - -- (BOOL)isEqual:(id)object; -- (BOOL)isEqualToAuthentication:(GIDAuthentication *)other; -- (NSUInteger)hash; - -@end 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 41c094a6..00000000 --- a/GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m +++ /dev/null @@ -1,673 +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)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] timeIntervalSince1970]); - OIDTokenRequest *tokenRequest = - [OIDTokenRequest testInstanceWithAdditionalParameters:_additionalTokenRequestParameters]; - OIDTokenResponse *tokenResponse = - [OIDTokenResponse testInstanceWithIDToken:idToken - accessToken:kAccessToken - expiresIn:accessTokenExpiresIn - refreshToken:kRefreshToken - tokenRequest:tokenRequest]; - return [[GIDAuthentication alloc] - initWithAuthState:[OIDAuthState testInstanceWithTokenResponse:tokenResponse]]; -} - -- (NSString *)idTokenWithExpireTime:(NSTimeInterval)expireTime { - if (!_hasIDToken) { - return nil; - } - return [OIDTokenResponse idTokenWithSub:kUserID exp:@(expireTime)]; -} - -- (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 date] timeIntervalSince1970]); - return [OIDTokenResponse testInstanceWithIDToken:(_hasIDToken ? [self idTokenNew] : nil) - accessToken:kNewAccessToken - expiresIn:expiresIn - refreshToken:kRefreshToken - tokenRequest:_tokenRequest ?: nil]; -} - -- (NSError *)fakeError { - return [NSError errorWithDomain:@"fake.domain" code:-1 userInfo:nil]; -} - -- (void)assertDate:(NSDate *)date equalTime:(NSTimeInterval)time { - XCTAssertEqualWithAccuracy([date timeIntervalSince1970], 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] timeIntervalSince1970] + accessExpire; - _idTokenExpireTime = [[NSDate date] timeIntervalSince1970] + 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/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/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 f6db0e85..3428c896 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m @@ -14,13 +14,19 @@ #import "GoogleSignIn/Tests/Unit/GIDGoogleUser+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/GIDAuthentication+Testing.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 { @@ -34,8 +40,7 @@ - (BOOL)isEqual:(id)object { } - (BOOL)isEqualToGoogleUser:(GIDGoogleUser *)other { - return [self.authentication isEqual:other.authentication] && - [self.userID isEqual:other.userID] && + return [self.userID isEqual:other.userID] && [self.profile isEqual:other.profile] && [self.configuration isEqual:other.configuration] && [self.idToken isEqual:other.idToken] && @@ -45,9 +50,32 @@ - (BOOL)isEqualToGoogleUser:(GIDGoogleUser *)other { // 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.configuration hash] ^ - [self.profile hash] ^ [self.idToken hash] ^ [self.refreshToken hash] ^ - [self.accessToken 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 ae2e561d..488d3f63 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m @@ -16,13 +16,12 @@ #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/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" @@ -31,8 +30,21 @@ #ifdef SWIFT_PACKAGE @import AppAuth; +@import GoogleUtilities_MethodSwizzler; +@import GoogleUtilities_SwizzlerTestHelpers; +@import GTMAppAuth; #else #import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import #endif static NSString *const kNewAccessToken = @"new_access_token"; @@ -45,7 +57,32 @@ @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 @@ -53,10 +90,7 @@ - (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.configuration.hostedDomain, kHostedDomain); @@ -99,6 +133,23 @@ - (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]; @@ -110,17 +161,15 @@ - (void)testUpdateAuthState { refreshToken:kNewRefreshToken]; GIDProfileData *updatedProfileData = [GIDProfileData testInstance]; - [user updateAuthState:updatedAuthState profileData:updatedProfileData]; + [user updateWithTokenResponse:updatedAuthState.lastTokenResponse + authorizationResponse:updatedAuthState.lastAuthorizationResponse + profileData:updatedProfileData]; XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken); - NSDate *expectedAccessTokenExpirationDate = [[NSDate date] dateByAddingTimeInterval:kAccessTokenExpiresIn]; - XCTAssertEqualWithAccuracy([user.accessToken.expirationDate timeIntervalSince1970], - [expectedAccessTokenExpirationDate timeIntervalSince1970], kTimeAccuracy); + [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn]; XCTAssertEqualObjects(user.idToken.tokenString, updatedIDToken); - NSDate *expectedIDTokenExpirationDate = [[NSDate date] dateByAddingTimeInterval:kNewIDTokenExpiresIn]; - XCTAssertEqualWithAccuracy([user.idToken.expirationDate timeIntervalSince1970], - [expectedIDTokenExpirationDate timeIntervalSince1970], kTimeAccuracy); + [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn]; XCTAssertEqualObjects(user.refreshToken.tokenString, kNewRefreshToken); @@ -142,7 +191,9 @@ - (void)testUpdateAuthState_tokensAreNotChanged { GIDToken *refreshTokenBeforeUpdate = user.refreshToken; GIDToken *idTokenBeforeUpdate = user.idToken; - [user updateAuthState:authState profileData:nil]; + [user updateWithTokenResponse:authState.lastTokenResponse + authorizationResponse:authState.lastAuthorizationResponse + profileData:nil]; XCTAssertIdentical(user.accessToken, accessTokenBeforeUpdate); XCTAssertIdentical(user.idToken, idTokenBeforeUpdate); @@ -176,8 +227,219 @@ - (void)testFetcherAuthorizer_returnTheSameInstance { XCTAssertIdentical(fetcherAuthorizer, fetcherAuthorizer2); } +#pragma mark - Test `doWithFreshTokens:` + +- (void)testDoWithFreshTokens_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 doWithFreshTokens:^(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)testDoWithRefreshTokens_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 doWithFreshTokens:^(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)testDoWithFreshTokens_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 doWithFreshTokens:^(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)testDoWithFreshTokens_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 doWithFreshTokens:^(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)testDoWithFreshTokens_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 doWithFreshTokens:^(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)testDoWithFreshTokens_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 doWithFreshTokens:^(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)testDoWithFreshTokens_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 doWithFreshTokens:^(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 doWithFreshTokens:^(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 - 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]; @@ -195,4 +457,21 @@ - (NSString *)idTokenWithExpiresIn:(NSTimeInterval)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/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index a5f0c21f..d60c90a2 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -30,7 +30,6 @@ #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" @@ -208,9 +207,6 @@ @interface GIDSignInTest : XCTestCase { // Mock for |GIDGoogleUser|. id _user; - // Mock for |GIDAuthentication|. - id _authentication; - // Mock for |OIDAuthorizationService| id _oidAuthorizationService; @@ -318,7 +314,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) @@ -369,7 +364,6 @@ - (void)tearDown { OCMVerifyAll(_tokenRequest); OCMVerifyAll(_authorization); OCMVerifyAll(_user); - OCMVerifyAll(_authentication); OCMVerifyAll(_oidAuthorizationService); #if TARGET_OS_IOS || TARGET_OS_MACCATALYST @@ -396,10 +390,11 @@ - (void)testShareInstance { } - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser { - [[[_authorization expect] andReturn:_authState] authState]; + [[[_authorization stub] andReturn:_authState] authState]; [[_authorization expect] setTokenRefreshDelegate:OCMOCK_ANY]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_authState refreshToken]).andReturn(kRefreshToken); + [[_authState expect] setStateChangeDelegate:OCMOCK_ANY]; id idTokenDecoded = OCMClassMock([OIDIDToken class]); OCMStub([idTokenDecoded alloc]).andReturn(idTokenDecoded); @@ -418,7 +413,6 @@ - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser { OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken); OCMStub([_tokenResponse accessTokenExpirationDate]).andReturn(nil); - [_signIn restorePreviousSignInNoRefresh]; [_authorization verify]; @@ -580,8 +574,6 @@ - (void)testAddScopes { id profile = OCMStrictClassMock([GIDProfileData class]); OCMStub([profile email]).andReturn(kUserEmail); - - OCMStub([_user authentication]).andReturn(_authentication); // Mock for the method `addScopes`. OCMStub([_user configuration]).andReturn(_configuration); @@ -1353,14 +1345,19 @@ - (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) @@ -1393,7 +1390,12 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow [_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); @@ -1406,13 +1408,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] doWithFreshTokens:SAVE_TO_ARG_BLOCK(completion)]; XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"]; @@ -1422,7 +1419,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/Samples/ObjC/SignInSample/Source/SignInViewController.m b/Samples/ObjC/SignInSample/Source/SignInViewController.m index f213a631..12a5d82e 100644 --- a/Samples/ObjC/SignInSample/Source/SignInViewController.m +++ b/Samples/ObjC/SignInSample/Source/SignInViewController.m @@ -185,7 +185,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. @@ -198,7 +198,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]; diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift index 1dc4dea4..1de3b3de 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?.do { user, error in + guard let token = user?.accessToken.tokenString else { completion(.failure(.couldNotCreateURLSession(error))) return }