Просмотр исходного кода

Move the method `doWithFreshTokens:` into GIDGoogleUserAPI (#234)

pinlu 3 лет назад
Родитель
Сommit
e43bd7f5a5

+ 7 - 6
GoogleSignIn/Sources/GIDAuthentication_Private.h → 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 <Foundation/Foundation.h>
+
+@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 <NSSecureCoding>
 
-// 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;
 

+ 20 - 220
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 <AppAuth/OIDAuthState.h>
-#import <AppAuth/OIDAuthorizationRequest.h>
-#import <AppAuth/OIDAuthorizationResponse.h>
-#import <AppAuth/OIDAuthorizationService.h>
-#import <AppAuth/OIDError.h>
-#import <AppAuth/OIDIDToken.h>
-#import <AppAuth/OIDTokenRequest.h>
-#import <AppAuth/OIDTokenResponse.h>
+#import <AppAuth/AppAuth.h>
 #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 @@ static NSString *const kNewIOSSystemName = @"iOS";
 - (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

+ 129 - 31
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 @@ NS_ASSUME_NONNULL_BEGIN
 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 @@ 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<GIDGoogleUserCompletion> *_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 @@ static NSString *const kEMMSupportParameterName = @"emm_support";
 
 - (nullable NSArray<NSString *> *)grantedScopes {
   NSArray<NSString *> *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 @@ static NSString *const kEMMSupportParameterName = @"emm_support";
   @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 @@ static NSString *const kEMMSupportParameterName = @"emm_support";
   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<GIDGoogleUserCompletion> *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<GIDGoogleUserCompletion> *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 @@ static NSString *const kEMMSupportParameterName = @"emm_support";
                       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 @@ static NSString *const kEMMSupportParameterName = @"emm_support";
 #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 @@ static NSString *const kEMMSupportParameterName = @"emm_support";
 - (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

+ 12 - 4
GoogleSignIn/Sources/GIDGoogleUser_Private.h

@@ -24,12 +24,16 @@
 #import <GTMAppAuth/GTMAppAuth.h>
 #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 () <GTMAppAuthFetcherAuthorizationTokenRefreshDelegate>
+@interface GIDGoogleUser () <GTMAppAuthFetcherAuthorizationTokenRefreshDelegate,
+                             OIDAuthStateChangeDelegate>
 
 @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<GTMFetcherAuthorizationProtocol> 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
 

+ 13 - 12
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 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
 }
 
 - (BOOL)hasPreviousSignIn {
-  if ([_currentUser.authentication.authState isAuthorized]) {
+  if ([_currentUser.authState isAuthorized]) {
     return YES;
   }
   OIDAuthState *authState = [self loadAuthState];
@@ -218,7 +216,8 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
       [[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 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
 }
 
 - (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 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   }
 
   // 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 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
       }
 
       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 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
           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);
         }
       });

+ 0 - 70
GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h

@@ -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 <Foundation/Foundation.h>
-
-// 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 <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
-#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 <NSSecureCoding>
-
-/// 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

+ 8 - 4
GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h

@@ -24,7 +24,6 @@
 #import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
 #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<NSString *> *grantedScopes;
 
@@ -67,6 +63,14 @@ NS_ASSUME_NONNULL_BEGIN
 @property(nonatomic, readonly) id<GTMFetcherAuthorizationProtocol> 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

+ 0 - 1
GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h

@@ -15,7 +15,6 @@
  */
 #import <TargetConditionals.h>
 
-#import "GIDAuthentication.h"
 #import "GIDConfiguration.h"
 #import "GIDGoogleUser.h"
 #import "GIDProfileData.h"

+ 0 - 25
GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h

@@ -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

+ 0 - 46
GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.m

@@ -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
-

+ 0 - 673
GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m

@@ -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 <XCTest/XCTest.h>
-
-#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 <AppAuth/OIDAuthState.h>
-#import <AppAuth/OIDAuthorizationRequest.h>
-#import <AppAuth/OIDAuthorizationResponse.h>
-#import <AppAuth/OIDAuthorizationService.h>
-#import <AppAuth/OIDError.h>
-#import <AppAuth/OIDIDToken.h>
-#import <AppAuth/OIDServiceConfiguration.h>
-#import <AppAuth/OIDTokenRequest.h>
-#import <AppAuth/OIDTokenResponse.h>
-#import <GoogleUtilities/GULSwizzler.h>
-#import <GoogleUtilities/GULSwizzler+Unswizzle.h>
-#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
-#import <GTMSessionFetcher/GTMSessionFetcher.h>
-#import <OCMock/OCMock.h>
-#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

+ 1 - 0
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 <TargetConditionals.h>
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 9 - 0
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

+ 34 - 6
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)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 @@
 
 // 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

+ 294 - 15
GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m

@@ -16,13 +16,12 @@
 
 #import <XCTest/XCTest.h>
 
-#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 <AppAuth/OIDAuthState.h>
+#import <AppAuth/OIDAuthorizationRequest.h>
+#import <AppAuth/OIDAuthorizationResponse.h>
+#import <AppAuth/OIDAuthorizationService.h>
+#import <AppAuth/OIDError.h>
+#import <AppAuth/OIDIDToken.h>
+#import <AppAuth/OIDTokenRequest.h>
+#import <AppAuth/OIDTokenResponse.h>
+#import <GoogleUtilities/GULSwizzler.h>
+#import <GoogleUtilities/GULSwizzler+Unswizzle.h>
+#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
 #endif
 
 static NSString *const kNewAccessToken = @"new_access_token";
@@ -45,7 +57,32 @@ static NSTimeInterval const kNewIDTokenExpiresIn = 200;
 @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 @@ static NSTimeInterval const kNewIDTokenExpiresIn = 200;
   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 @@ static NSTimeInterval const kNewIDTokenExpiresIn = 200;
 }
 #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 @@ static NSTimeInterval const kNewIDTokenExpiresIn = 200;
                                                             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 @@ static NSTimeInterval const kNewIDTokenExpiresIn = 200;
   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 @@ static NSTimeInterval const kNewIDTokenExpiresIn = 200;
   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 @@ static NSTimeInterval const kNewIDTokenExpiresIn = 200;
   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

+ 18 - 21
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 @@ static void *kTestObserverContext = &kTestObserverContext;
   // Mock for |GIDGoogleUser|.
   id _user;
 
-  // Mock for |GIDAuthentication|.
-  id _authentication;
-
   // Mock for |OIDAuthorizationService|
   id _oidAuthorizationService;
 
@@ -318,7 +314,6 @@ static void *kTestObserverContext = &kTestObserverContext;
         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 @@ static void *kTestObserverContext = &kTestObserverContext;
   OCMVerifyAll(_tokenRequest);
   OCMVerifyAll(_authorization);
   OCMVerifyAll(_user);
-  OCMVerifyAll(_authentication);
   OCMVerifyAll(_oidAuthorizationService);
 
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
@@ -396,10 +390,11 @@ static void *kTestObserverContext = &kTestObserverContext;
 }
 
 - (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 @@ static void *kTestObserverContext = &kTestObserverContext;
   OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken);
   OCMStub([_tokenResponse accessTokenExpirationDate]).andReturn(nil);
   
-
   [_signIn restorePreviousSignInNoRefresh];
 
   [_authorization verify];
@@ -580,8 +574,6 @@ static void *kTestObserverContext = &kTestObserverContext;
 
   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 @@ static void *kTestObserverContext = &kTestObserverContext;
 
   // 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 @@ static void *kTestObserverContext = &kTestObserverContext;
   [_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 @@ static void *kTestObserverContext = &kTestObserverContext;
   _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 @@ static void *kTestObserverContext = &kTestObserverContext;
     XCTAssertNil(error, @"should have no error");
   }];
 
-  completion(_authentication, nil);
+  completion(_user, nil);
 
   [self waitForExpectationsWithTimeout:1 handler:nil];
   XCTAssertFalse(_keychainRemoved, @"should not remove keychain");

+ 2 - 2
Samples/ObjC/SignInSample/Source/SignInViewController.m

@@ -185,7 +185,7 @@ static NSString * const kClientID =
 
 - (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 @@ static NSString * const kClientID =
 // 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];

+ 4 - 5
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<URLSession, Error>) -> 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
       }