Quellcode durchsuchen

Refactor GIDGoogleUser public API (#249)

pinlu vor 3 Jahren
Ursprung
Commit
87eddac9e4
41 geänderte Dateien mit 2143 neuen und 1499 gelöschten Zeilen
  1. 36 0
      GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h
  2. 129 0
      GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.m
  3. 13 6
      GoogleSignIn/Sources/GIDAuthentication.h
  4. 20 357
      GoogleSignIn/Sources/GIDAuthentication.m
  5. 14 28
      GoogleSignIn/Sources/GIDEMMSupport.h
  6. 101 0
      GoogleSignIn/Sources/GIDEMMSupport.m
  7. 256 49
      GoogleSignIn/Sources/GIDGoogleUser.m
  8. 36 4
      GoogleSignIn/Sources/GIDGoogleUser_Private.h
  9. 50 67
      GoogleSignIn/Sources/GIDSignIn.m
  10. 13 8
      GoogleSignIn/Sources/GIDSignInInternalOptions.h
  11. 10 6
      GoogleSignIn/Sources/GIDSignInInternalOptions.m
  12. 59 8
      GoogleSignIn/Sources/GIDSignIn_Private.h
  13. 96 0
      GoogleSignIn/Sources/GIDToken.m
  14. 32 0
      GoogleSignIn/Sources/GIDToken_Private.h
  15. 35 0
      GoogleSignIn/Sources/GIDUserAuth.m
  16. 33 0
      GoogleSignIn/Sources/GIDUserAuth_Private.h
  17. 0 78
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h
  18. 81 14
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h
  19. 19 45
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h
  20. 45 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h
  21. 40 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h
  22. 2 1
      GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h
  23. 0 46
      GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.m
  24. 0 684
      GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m
  25. 6 0
      GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.m
  26. 1 0
      GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m
  27. 229 0
      GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m
  28. 9 0
      GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h
  29. 42 7
      GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m
  30. 499 9
      GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m
  31. 6 5
      GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m
  32. 70 44
      GoogleSignIn/Tests/Unit/GIDSignInTest.m
  33. 94 0
      GoogleSignIn/Tests/Unit/GIDTokenTest.m
  34. 11 0
      GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h
  35. 13 0
      GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m
  36. 5 0
      GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h
  37. 4 2
      GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m
  38. 4 6
      Samples/ObjC/SignInSample/Source/AuthInspectorViewController.m
  39. 8 6
      Samples/ObjC/SignInSample/Source/SignInViewController.m
  40. 4 5
      Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift
  41. 18 14
      Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift

+ 36 - 0
GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#ifdef SWIFT_PACKAGE
+@import GTMAppAuth;
+#else
+#import <GTMAppAuth/GTMAppAuth.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// A specialized GTMAppAuthFetcherAuthorization subclass with EMM support.
+@interface GIDAppAuthFetcherAuthorizationWithEMMSupport : GTMAppAuthFetcherAuthorization
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 129 - 0
GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.m

@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import "GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h"
+
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+@import GTMAppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#import <GTMAppAuth/GTMAppAuth.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// The specialized GTMAppAuthFetcherAuthorization delegate that handles potential EMM error
+// responses.
+@interface GIDAppAuthFetcherAuthorizationEMMChainedDelegate : NSObject
+
+// Initializes with chained delegate and selector.
+- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector;
+
+// The callback method for GTMAppAuthFetcherAuthorization to invoke.
+- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
+               request:(NSMutableURLRequest *)request
+     finishedWithError:(nullable NSError *)error;
+
+@end
+
+@implementation GIDAppAuthFetcherAuthorizationEMMChainedDelegate {
+  // We use a weak reference here to match GTMAppAuthFetcherAuthorization.
+  __weak id _delegate;
+  SEL _selector;
+  // We need to maintain a reference to the chained delegate because GTMAppAuthFetcherAuthorization
+  // only keeps a weak reference.
+  GIDAppAuthFetcherAuthorizationEMMChainedDelegate *_retained_self;
+}
+
+- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector {
+  self = [super init];
+  if (self) {
+    _delegate = delegate;
+    _selector = selector;
+    _retained_self = self;
+  }
+  return self;
+}
+
+- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
+               request:(NSMutableURLRequest *)request
+     finishedWithError:(nullable NSError *)error {
+  [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
+    if (!self->_delegate || !self->_selector) {
+      return;
+    }
+    NSMethodSignature *signature = [self->_delegate methodSignatureForSelector:self->_selector];
+    if (!signature) {
+      return;
+    }
+    id argument1 = auth;
+    id argument2 = request;
+    id argument3 = error;
+    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
+    [invocation setTarget:self->_delegate];  // index 0
+    [invocation setSelector:self->_selector];  // index 1
+    [invocation setArgument:&argument1 atIndex:2];
+    [invocation setArgument:&argument2 atIndex:3];
+    [invocation setArgument:&argument3 atIndex:4];
+    [invocation invoke];
+  }];
+  // Prepare to deallocate the chained delegate instance because the above block will retain the
+  // iVar references it uses.
+  _retained_self = nil;
+}
+
+@end
+
+@implementation GIDAppAuthFetcherAuthorizationWithEMMSupport
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-implementations"
+- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
+                delegate:(id)delegate
+       didFinishSelector:(SEL)sel {
+#pragma clang diagnostic pop
+  GIDAppAuthFetcherAuthorizationEMMChainedDelegate *chainedDelegate =
+      [[GIDAppAuthFetcherAuthorizationEMMChainedDelegate alloc] initWithDelegate:delegate
+                                                                        selector:sel];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+  [super authorizeRequest:request
+                 delegate:chainedDelegate
+        didFinishSelector:@selector(authentication:request:finishedWithError:)];
+#pragma clang diagnostic pop
+}
+
+- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
+       completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)handler {
+  [super authorizeRequest:request completionHandler:^(NSError *_Nullable error) {
+    [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
+      handler(error);
+    }];
+  }];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 13 - 6
GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.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,12 +14,19 @@
  * limitations under the License.
  */
 
-#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
+#import <Foundation/Foundation.h>
 
-@interface GIDAuthentication (Testing)
+@class OIDAuthState;
 
-- (BOOL)isEqual:(id)object;
-- (BOOL)isEqualToAuthentication:(GIDAuthentication *)other;
-- (NSUInteger)hash;
+NS_ASSUME_NONNULL_BEGIN
+
+// Internal class for GIDGoogleUser NSCoding backward compatibility.
+@interface GIDAuthentication : NSObject <NSSecureCoding>
+
+@property(nonatomic) OIDAuthState* authState;
+
+- (instancetype)initWithAuthState:(OIDAuthState *)authState;
 
 @end
+
+NS_ASSUME_NONNULL_END

+ 20 - 357
GoogleSignIn/Sources/GIDAuthentication.m

@@ -1,377 +1,41 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
-
-#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
-
-#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
-#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "GoogleSignIn/Sources/GIDAuthentication.h"
 
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
 #else
-#import <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";
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-// The specialized GTMAppAuthFetcherAuthorization delegate that handles potential EMM error
-// responses.
-@interface GTMAppAuthFetcherAuthorizationEMMChainedDelegate : NSObject
-
-// Initializes with chained delegate and selector.
-- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector;
-
-// The callback method for GTMAppAuthFetcherAuthorization to invoke.
-- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
-               request:(NSMutableURLRequest *)request
-     finishedWithError:(nullable NSError *)error;
-
-@end
-
-@implementation GTMAppAuthFetcherAuthorizationEMMChainedDelegate {
-  // We use a weak reference here to match GTMAppAuthFetcherAuthorization.
-  __weak id _delegate;
-  SEL _selector;
-  // We need to maintain a reference to the chained delegate because GTMAppAuthFetcherAuthorization
-  // only keeps a weak reference.
-  GTMAppAuthFetcherAuthorizationEMMChainedDelegate *_retained_self;
-}
-
-- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector {
-  self = [super init];
-  if (self) {
-    _delegate = delegate;
-    _selector = selector;
-    _retained_self = self;
-  }
-  return self;
-}
-
-- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
-               request:(NSMutableURLRequest *)request
-     finishedWithError:(nullable NSError *)error {
-  [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
-    if (!self->_delegate || !self->_selector) {
-      return;
-    }
-    NSMethodSignature *signature = [self->_delegate methodSignatureForSelector:self->_selector];
-    if (!signature) {
-      return;
-    }
-    id argument1 = auth;
-    id argument2 = request;
-    id argument3 = error;
-    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
-    [invocation setTarget:self->_delegate];  // index 0
-    [invocation setSelector:self->_selector];  // index 1
-    [invocation setArgument:&argument1 atIndex:2];
-    [invocation setArgument:&argument2 atIndex:3];
-    [invocation setArgument:&argument3 atIndex:4];
-    [invocation invoke];
-  }];
-  // Prepare to deallocate the chained delegate instance because the above block will retain the
-  // iVar references it uses.
-  _retained_self = nil;
-}
-
-@end
-
-// A specialized GTMAppAuthFetcherAuthorization subclass with EMM support.
-@interface GTMAppAuthFetcherAuthorizationWithEMMSupport : GTMAppAuthFetcherAuthorization
-@end
-
-@implementation GTMAppAuthFetcherAuthorizationWithEMMSupport
-
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-implementations"
-- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
-                delegate:(id)delegate
-       didFinishSelector:(SEL)sel {
-#pragma clang diagnostic pop
-  GTMAppAuthFetcherAuthorizationEMMChainedDelegate *chainedDelegate =
-      [[GTMAppAuthFetcherAuthorizationEMMChainedDelegate alloc] initWithDelegate:delegate
-                                                                        selector:sel];
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-  [super authorizeRequest:request
-                 delegate:chainedDelegate
-        didFinishSelector:@selector(authentication:request:finishedWithError:)];
-#pragma clang diagnostic pop
-}
-
-- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
-       completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)handler {
-  [super authorizeRequest:request completionHandler:^(NSError *_Nullable error) {
-    [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
-      handler(error);
-    }];
-  }];
-}
-
-@end
-
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-@implementation GIDAuthentication {
-  // A queue for pending authentication handlers so we don't fire multiple requests in parallel.
-  // Access to this ivar should be synchronized.
-  NSMutableArray *_authenticationHandlerQueue;
-}
+@implementation GIDAuthentication
 
 - (instancetype)initWithAuthState:(OIDAuthState *)authState {
-  if (!authState) {
-    return nil;
-  }
   self = [super init];
   if (self) {
-    _authenticationHandlerQueue = [[NSMutableArray alloc] init];
     _authState = authState;
   }
   return self;
 }
 
-#pragma mark - Public property accessors
-
-- (NSString *)clientID {
-  return _authState.lastAuthorizationResponse.request.clientID;
-}
-
-- (NSString *)accessToken {
-  return _authState.lastTokenResponse.accessToken;
-}
-
-- (NSDate *)accessTokenExpirationDate {
-  return _authState.lastTokenResponse.accessTokenExpirationDate;
-}
-
-- (NSString *)refreshToken {
-  return _authState.refreshToken;
-}
-
-- (nullable NSString *)idToken {
-  return _authState.lastTokenResponse.idToken;
-}
-
-- (nullable NSDate *)idTokenExpirationDate {
-  return [[[OIDIDToken alloc] initWithIDTokenString:self.idToken] expiresAt];
-}
-
-#pragma mark - Private property accessors
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-- (NSString *)emmSupport {
-  return
-      _authState.lastAuthorizationResponse.request.additionalParameters[kEMMSupportParameterName];
-}
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-#pragma mark - Public methods
-
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-- (id<GTMFetcherAuthorizationProtocol>)fetcherAuthorizer {
-#pragma clang diagnostic pop
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  GTMAppAuthFetcherAuthorization *authorization = self.emmSupport ?
-      [[GTMAppAuthFetcherAuthorizationWithEMMSupport alloc] initWithAuthState:_authState] :
-      [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:_authState];
-#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
-  GTMAppAuthFetcherAuthorization *authorization =
-      [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:_authState];
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  authorization.tokenRefreshDelegate = self;
-  return authorization;
-}
-
-- (void)doWithFreshTokens:(GIDAuthenticationCompletion)completion {
-  if (!([self.accessTokenExpirationDate timeIntervalSinceNow] < kMinimalTimeToExpire ||
-      (self.idToken && [self.idTokenExpirationDate timeIntervalSinceNow] < kMinimalTimeToExpire))) {
-    dispatch_async(dispatch_get_main_queue(), ^{
-      completion(self, nil);
-    });
-    return;
-  }
-  @synchronized (_authenticationHandlerQueue) {
-    // Push the handler into the callback queue.
-    [_authenticationHandlerQueue addObject:[completion copy]];
-    if (_authenticationHandlerQueue.count > 1) {
-      // This is not the first handler in the queue, no fetch is needed.
-      return;
-    }
-  }
-  // This is the first handler in the queue, a fetch is needed.
-  NSMutableDictionary *additionalParameters = [@{} mutableCopy];
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  [additionalParameters addEntriesFromDictionary:
-      [GIDAuthentication updatedEMMParametersWithParameters:
-          _authState.lastTokenResponse.request.additionalParameters]];
-#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
-  [additionalParameters addEntriesFromDictionary:
-      _authState.lastTokenResponse.request.additionalParameters];
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
-  additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
-
-  OIDTokenRequest *tokenRefreshRequest =
-      [_authState tokenRefreshRequestWithAdditionalParameters:additionalParameters];
-  [OIDAuthorizationService performTokenRequest:tokenRefreshRequest
-                 originalAuthorizationResponse:_authState.lastAuthorizationResponse
-                                      callback:^(OIDTokenResponse *_Nullable tokenResponse,
-                                                 NSError *_Nullable error) {
-    if (tokenResponse) {
-      [self willChangeValueForKey:NSStringFromSelector(@selector(accessToken))];
-      [self willChangeValueForKey:NSStringFromSelector(@selector(accessTokenExpirationDate))];
-      [self willChangeValueForKey:NSStringFromSelector(@selector(idToken))];
-      [self willChangeValueForKey:NSStringFromSelector(@selector(idTokenExpirationDate))];
-      [self->_authState updateWithTokenResponse:tokenResponse error:nil];
-      [self didChangeValueForKey:NSStringFromSelector(@selector(accessToken))];
-      [self didChangeValueForKey:NSStringFromSelector(@selector(accessTokenExpirationDate))];
-      [self didChangeValueForKey:NSStringFromSelector(@selector(idToken))];
-      [self didChangeValueForKey:NSStringFromSelector(@selector(idTokenExpirationDate))];
-    } else {
-      if (error.domain == OIDOAuthTokenErrorDomain) {
-        [self->_authState updateWithAuthorizationError:error];
-      }
-    }
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-    [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
-      // Process the handler queue to call back.
-      NSArray *authenticationHandlerQueue;
-      @synchronized(self->_authenticationHandlerQueue) {
-        authenticationHandlerQueue = [self->_authenticationHandlerQueue copy];
-        [self->_authenticationHandlerQueue removeAllObjects];
-      }
-      for (GIDAuthenticationCompletion completion in authenticationHandlerQueue) {
-        dispatch_async(dispatch_get_main_queue(), ^{
-          completion(error ? nil : self, error);
-        });
-      }
-    }];
-#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
-    NSArray *authenticationHandlerQueue;
-    @synchronized(self->_authenticationHandlerQueue) {
-      authenticationHandlerQueue = [self->_authenticationHandlerQueue copy];
-      [self->_authenticationHandlerQueue removeAllObjects];
-    }
-    for (GIDAuthenticationCompletion completion in authenticationHandlerQueue) {
-      dispatch_async(dispatch_get_main_queue(), ^{
-        completion(error ? nil : self, error);
-      });
-    }
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  }];
-}
-
-#pragma mark - Private methods
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-+ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters
-                                emmSupport:(nullable NSString *)emmSupport
-                    isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired {
-  if (!emmSupport) {
-    return parameters;
-  }
-  NSMutableDictionary *allParameters = [(parameters ?: @{}) mutableCopy];
-  allParameters[kEMMSupportParameterName] = emmSupport;
-  UIDevice *device = [UIDevice currentDevice];
-  NSString *systemName = device.systemName;
-  if ([systemName isEqualToString:kOldIOSSystemName]) {
-    systemName = kNewIOSSystemName;
-  }
-  allParameters[kEMMOSVersionParameterName] =
-      [NSString stringWithFormat:@"%@ %@", systemName, device.systemVersion];
-  if (isPasscodeInfoRequired) {
-    allParameters[kEMMPasscodeInfoParameterName] = [GIDMDMPasscodeState passcodeState].info;
-  }
-  return allParameters;
-}
-
-+ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters {
-  return [self parametersWithParameters:parameters
-                             emmSupport:parameters[kEMMSupportParameterName]
-                 isPasscodeInfoRequired:parameters[kEMMPasscodeInfoParameterName] != nil];
-}
-
-+ (void)handleTokenFetchEMMError:(nullable NSError *)error
-                      completion:(void (^)(NSError *_Nullable))completion {
-  NSDictionary *errorJSON = error.userInfo[OIDOAuthErrorResponseErrorKey];
-  if (errorJSON) {
-    __block BOOL handled = NO;
-    handled = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:errorJSON
-                                                                completion:^() {
-      if (handled) {
-        completion([NSError errorWithDomain:kGIDSignInErrorDomain
-                                       code:kGIDSignInErrorCodeEMM
-                                   userInfo:error.userInfo]);
-      } else {
-        completion(error);
-      }
-    }];
-  } else {
-    completion(error);
-  }
-}
-
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-#pragma mark - GTMAppAuthFetcherAuthorizationTokenRefreshDelegate
-
-- (nullable NSDictionary *)additionalRefreshParameters:
-    (GTMAppAuthFetcherAuthorization *)authorization {
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  return [GIDAuthentication updatedEMMParametersWithParameters:
-      authorization.authState.lastTokenResponse.request.additionalParameters];
-#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
-  return authorization.authState.lastTokenResponse.request.additionalParameters;
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-}
-
 #pragma mark - NSSecureCoding
 
 + (BOOL)supportsSecureCoding {
@@ -381,14 +45,13 @@ 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

+ 14 - 28
GoogleSignIn/Sources/GIDAuthentication_Private.h → GoogleSignIn/Sources/GIDEMMSupport.h

@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 Google LLC
+ * Copyright 2022 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,45 +14,31 @@
  * limitations under the License.
  */
 
-#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
+#import <TargetConditionals.h>
 
-#ifdef SWIFT_PACKAGE
-@import AppAuth;
-@import GTMAppAuth;
-#else
-#import <AppAuth/AppAuth.h>
-#import <GTMAppAuth/GTMAppAuth.h>
-#endif
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
-NS_ASSUME_NONNULL_BEGIN
+#import <Foundation/Foundation.h>
 
-// Internal methods for the class that are not part of the public API.
-@interface GIDAuthentication () <GTMAppAuthFetcherAuthorizationTokenRefreshDelegate>
+NS_ASSUME_NONNULL_BEGIN
 
-// A representation of the state of the OAuth session for this instance.
-@property(nonatomic, readonly) OIDAuthState *authState;
+// A class to support EMM (Enterprise Mobility Management).
+@interface GIDEMMSupport : NSObject
 
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-// A string indicating support for Enterprise Mobility Management.
-@property(nonatomic, readonly) NSString *emmSupport;
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+// Handles potential EMM error from token fetch response.
++ (void)handleTokenFetchEMMError:(nullable NSError *)error
+                      completion:(void (^)(NSError *_Nullable))completion;
 
-- (instancetype)initWithAuthState:(OIDAuthState *)authState;
+// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed.
++ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters;
 
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 // Gets a new set of URL parameters that also contains EMM-related URL parameters if needed.
 + (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters
                                 emmSupport:(nullable NSString *)emmSupport
                     isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired;
 
-// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed.
-+ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters;
-
-// Handles potential EMM error from token fetch response.
-+ (void)handleTokenFetchEMMError:(nullable NSError *)error
-                      completion:(void (^)(NSError *_Nullable))completion;
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 101 - 0
GoogleSignIn/Sources/GIDEMMSupport.m

@@ -0,0 +1,101 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Additional parameter names for EMM.
+static NSString *const kEMMSupportParameterName = @"emm_support";
+static NSString *const kEMMOSVersionParameterName = @"device_os";
+static NSString *const kEMMPasscodeInfoParameterName = @"emm_passcode_info";
+
+// Old UIDevice system name for iOS.
+static NSString *const kOldIOSSystemName = @"iPhone OS";
+
+// New UIDevice system name for iOS.
+static NSString *const kNewIOSSystemName = @"iOS";
+
+@implementation GIDEMMSupport
+
++ (void)handleTokenFetchEMMError:(nullable NSError *)error
+                      completion:(void (^)(NSError *_Nullable))completion {
+  NSDictionary *errorJSON = error.userInfo[OIDOAuthErrorResponseErrorKey];
+  if (errorJSON) {
+    __block BOOL handled = NO;
+    handled = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:errorJSON
+                                                                completion:^() {
+      if (handled) {
+        completion([NSError errorWithDomain:kGIDSignInErrorDomain
+                                       code:kGIDSignInErrorCodeEMM
+                                   userInfo:error.userInfo]);
+      } else {
+        completion(error);
+      }
+    }];
+  } else {
+    completion(error);
+  }
+}
+
++ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters {
+  return [self parametersWithParameters:parameters
+                             emmSupport:parameters[kEMMSupportParameterName]
+                 isPasscodeInfoRequired:parameters[kEMMPasscodeInfoParameterName] != nil];
+}
+
+
++ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters
+                                emmSupport:(nullable NSString *)emmSupport
+                    isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired {
+  if (!emmSupport) {
+    return parameters;
+  }
+  NSMutableDictionary *allParameters = [(parameters ?: @{}) mutableCopy];
+  allParameters[kEMMSupportParameterName] = emmSupport;
+  UIDevice *device = [UIDevice currentDevice];
+  NSString *systemName = device.systemName;
+  if ([systemName isEqualToString:kOldIOSSystemName]) {
+    systemName = kNewIOSSystemName;
+  }
+  allParameters[kEMMOSVersionParameterName] =
+      [NSString stringWithFormat:@"%@ %@", systemName, device.systemVersion];
+  if (isPasscodeInfoRequired) {
+    allParameters[kEMMPasscodeInfoParameterName] = [GIDMDMPasscodeState passcodeState].info;
+  }
+  return allParameters;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 256 - 49
GoogleSignIn/Sources/GIDGoogleUser.m

@@ -1,4 +1,4 @@
-// Copyright 2021 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,10 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
+
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
 
-#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+#import "GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h"
+#import "GoogleSignIn/Sources/GIDAuthentication.h"
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
 #import "GoogleSignIn/Sources/GIDProfileData_Private.h"
+#import "GoogleSignIn/Sources/GIDSignIn_Private.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+#import "GoogleSignIn/Sources/GIDToken_Private.h"
 
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
@@ -29,57 +39,41 @@ 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";
 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 *idToken = [self idToken];
-  if (idToken) {
-    OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken];
+  NSString *idTokenString = self.idToken.tokenString;
+  if (idTokenString) {
+    OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString];
     if (idTokenDecoded && idTokenDecoded.subject) {
       return [idTokenDecoded.subject copy];
     }
   }
-
   return nil;
 }
 
-- (nullable NSString *)hostedDomain {
-  NSString *idToken = [self idToken];
-  if (idToken) {
-    OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken];
-    if (idTokenDecoded && idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) {
-      return [idTokenDecoded.claims[kHostedDomainIDTokenClaimKey] copy];
-    }
-  }
-
-  return nil;
-}
-
-- (nullable NSString *)serverAuthCode {
-  return [_authState.lastTokenResponse.additionalParameters[@"server_code"] copy];
-}
-
-- (nullable NSString *)serverClientID {
-  return [_authState.lastTokenResponse.request.additionalParameters[kAudienceParameter] copy];
-}
-
-- (nullable NSString *)openIDRealm {
-  return [_authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter] copy];
-}
-
 - (nullable NSArray<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.
@@ -95,28 +89,237 @@ static NSString *const kOpenIDRealmParameter = @"openid.realm";
   return grantedScopes;
 }
 
+- (GIDConfiguration *)configuration {
+  @synchronized(self) {
+    // Caches the configuration since it would not change for one GIDGoogleUser instance.
+    if (!_cachedConfiguration) {
+      NSString *clientID = self.authState.lastAuthorizationResponse.request.clientID;
+      NSString *serverClientID =
+          self.authState.lastTokenResponse.request.additionalParameters[kAudienceParameter];
+      NSString *openIDRealm =
+          self.authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter];
+      
+      _cachedConfiguration = [[GIDConfiguration alloc] initWithClientID:clientID
+                                                         serverClientID:serverClientID
+                                                           hostedDomain:[self hostedDomain]
+                                                            openIDRealm:openIDRealm];
+    };
+  }
+  return _cachedConfiguration;
+}
+
+- (void)refreshTokensIfNeededWithCompletion:(GIDGoogleUserCompletion)completion {
+  if (!([self.accessToken.expirationDate timeIntervalSinceNow] < kMinimalTimeToExpire ||
+      (self.idToken && [self.idToken.expirationDate timeIntervalSinceNow] < kMinimalTimeToExpire))) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      completion(self, nil);
+    });
+    return;
+  }
+  @synchronized (_tokenRefreshHandlerQueue) {
+    // Push the handler into the callback queue.
+    [_tokenRefreshHandlerQueue addObject:[completion copy]];
+    if (_tokenRefreshHandlerQueue.count > 1) {
+      // This is not the first handler in the queue, no fetch is needed.
+      return;
+    }
+  }
+  // This is the first handler in the queue, a fetch is needed.
+  NSMutableDictionary *additionalParameters = [@{} mutableCopy];
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  [additionalParameters addEntriesFromDictionary:
+      [GIDEMMSupport updatedEMMParametersWithParameters:
+          self.authState.lastTokenResponse.request.additionalParameters]];
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+  [additionalParameters addEntriesFromDictionary:
+      self.authState.lastTokenResponse.request.additionalParameters];
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
+  additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
+
+  OIDTokenRequest *tokenRefreshRequest =
+      [self.authState tokenRefreshRequestWithAdditionalParameters:additionalParameters];
+  [OIDAuthorizationService performTokenRequest:tokenRefreshRequest
+                 originalAuthorizationResponse:self.authState.lastAuthorizationResponse
+                                      callback:^(OIDTokenResponse *_Nullable tokenResponse,
+                                                 NSError *_Nullable error) {
+    if (tokenResponse) {
+      [self.authState updateWithTokenResponse:tokenResponse error:nil];
+    } else {
+      if (error.domain == OIDOAuthTokenErrorDomain) {
+        [self.authState updateWithAuthorizationError:error];
+      }
+    }
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+    [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
+      // Process the handler queue to call back.
+      NSArray<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;
+}
+
+- (void)addScopes:(NSArray<NSString *> *)scopes
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+    presentingViewController:(UIViewController *)presentingViewController
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+            presentingWindow:(NSWindow *)presentingWindow
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+                  completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                NSError *_Nullable error))completion {
+  if (self != GIDSignIn.sharedInstance.currentUser) {
+    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                         code:kGIDSignInErrorCodeMismatchWithCurrentUser
+                                     userInfo:nil];
+    if (completion) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        completion(nil, error);
+      });
+    }
+    return;
+  }
+  
+  [GIDSignIn.sharedInstance addScopes:scopes
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+             presentingViewController:presentingViewController
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+                     presentingWindow:presentingWindow
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+                           completion:completion];
+}
+
 #pragma mark - Private Methods
 
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+- (nullable NSString *)emmSupport {
+  return self.authState.lastAuthorizationResponse
+      .request.additionalParameters[kEMMSupportParameterName];
+}
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
 - (instancetype)initWithAuthState:(OIDAuthState *)authState
                       profileData:(nullable GIDProfileData *)profileData {
   self = [super init];
   if (self) {
-    [self updateAuthState:authState profileData:profileData];
+    _tokenRefreshHandlerQueue = [[NSMutableArray alloc] init];
+    _profile = profileData;
+    
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+    GTMAppAuthFetcherAuthorization *authorization = self.emmSupport ?
+        [[GIDAppAuthFetcherAuthorizationWithEMMSupport alloc] initWithAuthState:authState] :
+        [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+    GTMAppAuthFetcherAuthorization *authorization =
+        [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+    authorization.tokenRefreshDelegate = self;
+    authorization.authState.stateChangeDelegate = self;
+    self.fetcherAuthorizer = authorization;
+    
+    [self updateTokensWithAuthState:authState];
   }
   return self;
 }
 
-- (void)updateAuthState:(OIDAuthState *)authState
-            profileData:(nullable GIDProfileData *)profileData {
-  _authState = authState;
-  _authentication = [[GIDAuthentication alloc] initWithAuthState:authState];
-  _profile = profileData;
+- (void)updateWithTokenResponse:(OIDTokenResponse *)tokenResponse
+          authorizationResponse:(OIDAuthorizationResponse *)authorizationResponse
+                    profileData:(nullable GIDProfileData *)profileData {
+  @synchronized(self) {
+    _profile = profileData;
+    
+    // We don't want to trigger the delegate before we update authState completely. So we unset the
+    // delegate before the first update. Also the order of updates is important because
+    // `updateWithAuthorizationResponse` would clear the last token reponse and refresh token.
+    // TODO: Rewrite authState update logic when the issue is addressed.(openid/AppAuth-iOS#728)
+    self.authState.stateChangeDelegate = nil;
+    [self.authState updateWithAuthorizationResponse:authorizationResponse error:nil];
+    self.authState.stateChangeDelegate = self;
+    [self.authState updateWithTokenResponse:tokenResponse error:nil];
+  }
+}
+
+- (void)updateTokensWithAuthState:(OIDAuthState *)authState {
+  GIDToken *accessToken =
+      [[GIDToken alloc] initWithTokenString:authState.lastTokenResponse.accessToken
+                             expirationDate:authState.lastTokenResponse.accessTokenExpirationDate];
+  if (![self.accessToken isEqualToToken:accessToken]) {
+    self.accessToken = accessToken;
+  }
+  
+  GIDToken *refreshToken = [[GIDToken alloc] initWithTokenString:authState.refreshToken
+                                                  expirationDate:nil];
+  if (![self.refreshToken isEqualToToken:refreshToken]) {
+    self.refreshToken = refreshToken;
+  }
+  
+  GIDToken *idToken;
+  NSString *idTokenString = authState.lastTokenResponse.idToken;
+  if (idTokenString) {
+    NSDate *idTokenExpirationDate =
+        [[[OIDIDToken alloc] initWithIDTokenString:idTokenString] expiresAt];
+    idToken = [[GIDToken alloc] initWithTokenString:idTokenString
+                                     expirationDate:idTokenExpirationDate];
+  } else {
+    idToken = nil;
+  }
+  if ((self.idToken || idToken) && ![self.idToken isEqualToToken:idToken]) {
+    self.idToken = idToken;
+  }
 }
 
 #pragma mark - Helpers
 
-- (NSString *)idToken {
-  return _authState ? _authState.lastTokenResponse.idToken : nil;
+- (nullable NSString *)hostedDomain {
+  NSString *idTokenString = self.idToken.tokenString;
+  if (idTokenString) {
+    OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString];
+    if (idTokenDecoded && idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) {
+      return idTokenDecoded.claims[kHostedDomainIDTokenClaimKey];
+    }
+  }
+  return nil;
+}
+
+#pragma mark - GTMAppAuthFetcherAuthorizationTokenRefreshDelegate
+
+- (nullable NSDictionary *)additionalRefreshParameters:
+    (GTMAppAuthFetcherAuthorization *)authorization {
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  return [GIDEMMSupport updatedEMMParametersWithParameters:
+      authorization.authState.lastTokenResponse.request.additionalParameters];
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+  return authorization.authState.lastTokenResponse.request.additionalParameters;
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+}
+
+#pragma mark - OIDAuthStateChangeDelegate
+
+- (void)didChangeState:(OIDAuthState *)state {
+   [self updateTokensWithAuthState:state];
 }
 
 #pragma mark - NSSecureCoding
@@ -128,22 +331,26 @@ static NSString *const kOpenIDRealmParameter = @"openid.realm";
 - (nullable instancetype)initWithCoder:(NSCoder *)decoder {
   self = [super init];
   if (self) {
-    _profile = [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey];
-    if ([decoder containsValueForKey:kAuthState]) { // Current encoding
-      _authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState];
+    GIDProfileData *profile =
+        [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey];
+    
+    OIDAuthState *authState;
+    if ([decoder containsValueForKey:kAuthStateKey]) { // Current encoding
+      authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthStateKey];
     } else { // Old encoding
       GIDAuthentication *authentication = [decoder decodeObjectOfClass:[GIDAuthentication class]
-                                                                forKey:kAuthenticationKey];
-      _authState = authentication.authState;
+                                                                forKey:@"authentication"];
+      authState = authentication.authState;
     }
-    _authentication = [[GIDAuthentication alloc] initWithAuthState:_authState];
+    
+    self = [self initWithAuthState:authState profileData:profile];
   }
   return self;
 }
 
 - (void)encodeWithCoder:(NSCoder *)encoder {
   [encoder encodeObject:_profile forKey:kProfileDataKey];
-  [encoder encodeObject:_authState forKey:kAuthState];
+  [encoder encodeObject:self.authState forKey:kAuthStateKey];
 }
 
 @end

+ 36 - 4
GoogleSignIn/Sources/GIDGoogleUser_Private.h

@@ -16,20 +16,52 @@
 
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
 
-NS_ASSUME_NONNULL_BEGIN
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+@import GTMAppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#import <GTMAppAuth/GTMAppAuth.h>
+#endif
 
 @class OIDAuthState;
 
+NS_ASSUME_NONNULL_BEGIN
+
+/// A completion block that takes a `GIDGoogleUser` or an error if the attempt to refresh tokens was unsuccessful.
+typedef void (^GIDGoogleUserCompletion)(GIDGoogleUser *_Nullable user, NSError *_Nullable error);
+
 // Internal methods for the class that are not part of the public API.
-@interface GIDGoogleUser ()
+@interface GIDGoogleUser () <GTMAppAuthFetcherAuthorizationTokenRefreshDelegate,
+                             OIDAuthStateChangeDelegate>
+
+@property(nonatomic, readwrite) GIDToken *accessToken;
+
+@property(nonatomic, readwrite) GIDToken *refreshToken;
+
+@property(nonatomic, readwrite, nullable) GIDToken *idToken;
+
+// A representation of the state of the OAuth session for this instance.
+@property(nonatomic, readonly) OIDAuthState *authState;
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+@property(nonatomic, readwrite) id<GTMFetcherAuthorizationProtocol> fetcherAuthorizer;
+#pragma clang diagnostic pop
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+// A string indicating support for Enterprise Mobility Management.
+@property(nonatomic, readonly, nullable) NSString *emmSupport;
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
 // Create a object with an auth state, scopes, and profile data.
 - (instancetype)initWithAuthState:(OIDAuthState *)authState
                       profileData:(nullable GIDProfileData *)profileData;
 
 // Update the auth state and profile data.
-- (void)updateAuthState:(OIDAuthState *)authState
-            profileData:(nullable GIDProfileData *)profileData;
+- (void)updateWithTokenResponse:(OIDTokenResponse *)tokenResponse
+          authorizationResponse:(OIDAuthorizationResponse *)authorizationResponse
+                    profileData:(nullable GIDProfileData *)profileData;
 
 @end
 

+ 50 - 67
GoogleSignIn/Sources/GIDSignIn.m

@@ -16,11 +16,12 @@
 
 #import "GoogleSignIn/Sources/GIDSignIn_Private.h"
 
-#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h"
 
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
 #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
 #import "GoogleSignIn/Sources/GIDCallbackQueue.h"
@@ -31,9 +32,9 @@
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
-#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
 #import "GoogleSignIn/Sources/GIDProfileData_Private.h"
+#import "GoogleSignIn/Sources/GIDUserAuth_Private.h"
 
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
@@ -186,15 +187,23 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 }
 
 - (BOOL)hasPreviousSignIn {
-  if ([_currentUser.authentication.authState isAuthorized]) {
+  if ([_currentUser.authState isAuthorized]) {
     return YES;
   }
   OIDAuthState *authState = [self loadAuthState];
   return [authState isAuthorized];
 }
 
-- (void)restorePreviousSignInWithCompletion:(nullable GIDSignInCompletion)completion {
-  [self signInWithOptions:[GIDSignInInternalOptions silentOptionsWithCompletion:completion]];
+- (void)restorePreviousSignInWithCompletion:(nullable void (^)(GIDGoogleUser *_Nullable user,
+                                                               NSError *_Nullable error))completion {
+  [self signInWithOptions:[GIDSignInInternalOptions silentOptionsWithCompletion:
+                           ^(GIDUserAuth *userAuth, NSError *error) {
+    if (userAuth) {
+      completion(userAuth.user, nil);
+    } else {
+      completion(nil, error);
+    }
+  }]];
 }
 
 - (BOOL)restorePreviousSignInNoRefresh {
@@ -222,7 +231,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
                                       hint:(nullable NSString *)hint
-                                completion:(nullable GIDSignInCompletion)completion {
+                                completion:(nullable GIDUserAuthCompletion)completion {
   GIDSignInInternalOptions *options =
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
                                        presentingViewController:presentingViewController
@@ -235,7 +244,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
                                       hint:(nullable NSString *)hint
                           additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
-                                completion:(nullable GIDSignInCompletion)completion {
+                                completion:(nullable GIDUserAuthCompletion)completion {
   GIDSignInInternalOptions *options =
     [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
                                      presentingViewController:presentingViewController
@@ -247,7 +256,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 }
 
 - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
-                                completion:(nullable GIDSignInCompletion)completion {
+                                completion:(nullable GIDUserAuthCompletion)completion {
   [self signInWithPresentingViewController:presentingViewController
                                       hint:nil
                                 completion:completion];
@@ -255,26 +264,8 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 - (void)addScopes:(NSArray<NSString *> *)scopes
     presentingViewController:(UIViewController *)presentingViewController
-                  completion:(nullable GIDSignInCompletion)completion {
-  // A currentUser must be available in order to complete this flow.
-  if (!self.currentUser) {
-    // No currentUser is set, notify callback of failure.
-    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
-                                         code:kGIDSignInErrorCodeNoCurrentUser
-                                     userInfo:nil];
-    if (completion) {
-      dispatch_async(dispatch_get_main_queue(), ^{
-        completion(nil, error);
-      });
-    }
-    return;
-  }
-
-  GIDConfiguration *configuration =
-      [[GIDConfiguration alloc] initWithClientID:self.currentUser.authentication.clientID
-                                  serverClientID:self.currentUser.serverClientID
-                                    hostedDomain:self.currentUser.hostedDomain
-                                     openIDRealm:self.currentUser.openIDRealm];
+                  completion:(nullable GIDUserAuthCompletion)completion {
+  GIDConfiguration *configuration = self.currentUser.configuration;
   GIDSignInInternalOptions *options =
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
                                        presentingViewController:presentingViewController
@@ -311,7 +302,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
                               hint:(nullable NSString *)hint
-                        completion:(nullable GIDSignInCompletion)completion {
+                        completion:(nullable GIDUserAuthCompletion)completion {
   GIDSignInInternalOptions *options =
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
                                                presentingWindow:presentingWindow
@@ -322,7 +313,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 }
 
 - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
-                        completion:(nullable GIDSignInCompletion)completion {
+                        completion:(nullable GIDUserAuthCompletion)completion {
   [self signInWithPresentingWindow:presentingWindow
                               hint:nil
                         completion:completion];
@@ -331,7 +322,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
                               hint:(nullable NSString *)hint
                   additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
-                        completion:(nullable GIDSignInCompletion)completion {
+                        completion:(nullable GIDUserAuthCompletion)completion {
   GIDSignInInternalOptions *options =
     [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
                                              presentingWindow:presentingWindow
@@ -344,26 +335,8 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 - (void)addScopes:(NSArray<NSString *> *)scopes
  presentingWindow:(NSWindow *)presentingWindow
-       completion:(nullable GIDSignInCompletion)completion {
-  // A currentUser must be available in order to complete this flow.
-  if (!self.currentUser) {
-    // No currentUser is set, notify callback of failure.
-    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
-                                         code:kGIDSignInErrorCodeNoCurrentUser
-                                     userInfo:nil];
-    if (completion) {
-      dispatch_async(dispatch_get_main_queue(), ^{
-        completion(nil, error);
-      });
-    }
-    return;
-  }
-
-  GIDConfiguration *configuration =
-      [[GIDConfiguration alloc] initWithClientID:self.currentUser.authentication.clientID
-                                  serverClientID:self.currentUser.serverClientID
-                                    hostedDomain:self.currentUser.hostedDomain
-                                     openIDRealm:self.currentUser.openIDRealm];
+       completion:(nullable GIDUserAuthCompletion)completion {
+  GIDConfiguration *configuration = self.currentUser.configuration;
   GIDSignInInternalOptions *options =
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
                                                presentingWindow:presentingWindow
@@ -408,8 +381,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 }
 
 - (void)disconnectWithCompletion:(nullable GIDDisconnectCompletion)completion {
-  GIDGoogleUser *user = _currentUser;
-  OIDAuthState *authState = user.authentication.authState;
+  OIDAuthState *authState = _currentUser.authState;
   if (!authState) {
     // Even the user is not signed in right now, we still need to remove any token saved in the
     // keychain.
@@ -545,15 +517,16 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   }
 
   // If this is a non-interactive flow, use cached authentication if possible.
-  if (!options.interactive && _currentUser.authentication) {
-    [_currentUser.authentication doWithFreshTokens:^(GIDAuthentication *unused, NSError *error) {
+  if (!options.interactive && _currentUser) {
+    [_currentUser refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *unused, NSError *error) {
       if (error) {
         [self authenticateWithOptions:options];
       } else {
         if (options.completion) {
           self->_currentOptions = nil;
           dispatch_async(dispatch_get_main_queue(), ^{
-            options.completion(self->_currentUser, nil);
+            GIDUserAuth *userAuth = [[GIDUserAuth alloc] initWithGoogleUser:self->_currentUser serverAuthCode:nil];
+            options.completion(userAuth, nil);
           });
         }
       }
@@ -592,9 +565,9 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
   [additionalParameters addEntriesFromDictionary:
-      [GIDAuthentication parametersWithParameters:options.extraParams
-                                       emmSupport:emmSupport
-                           isPasscodeInfoRequired:NO]];
+      [GIDEMMSupport parametersWithParameters:options.extraParams
+                                   emmSupport:emmSupport
+                       isPasscodeInfoRequired:NO]];
 #elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
   [additionalParameters addEntriesFromDictionary:options.extraParams];
 #endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
@@ -741,9 +714,9 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
       authState.lastAuthorizationResponse.additionalParameters;
   NSString *passcodeInfoRequired = (NSString *)params[kEMMPasscodeInfoRequiredKeyName];
   [additionalParameters addEntriesFromDictionary:
-      [GIDAuthentication parametersWithParameters:@{}
-                                       emmSupport:authFlow.emmSupport
-                           isPasscodeInfoRequired:passcodeInfoRequired.length > 0]];
+      [GIDEMMSupport parametersWithParameters:@{}
+                                   emmSupport:authFlow.emmSupport
+                       isPasscodeInfoRequired:passcodeInfoRequired.length > 0]];
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
   additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
   additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
@@ -769,7 +742,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
     if (authFlow.emmSupport) {
-      [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *error) {
+      [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *error) {
         authFlow.error = error;
         [authFlow next];
       }];
@@ -796,8 +769,9 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
       }
 
       if (self->_currentOptions.addScopesFlow) {
-        [self->_currentUser updateAuthState:authState
-                                profileData:handlerAuthFlow.profileData];
+        [self->_currentUser updateWithTokenResponse:authState.lastTokenResponse
+                              authorizationResponse:authState.lastAuthorizationResponse
+                                        profileData:handlerAuthFlow.profileData];
       } else {
         GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState
                                                            profileData:handlerAuthFlow.profileData];
@@ -865,10 +839,19 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   [authFlow addCallback:^() {
     GIDAuthFlow *handlerAuthFlow = weakAuthFlow;
     if (self->_currentOptions.completion) {
-      GIDSignInCompletion completion = self->_currentOptions.completion;
+      GIDUserAuthCompletion completion = self->_currentOptions.completion;
       self->_currentOptions = nil;
       dispatch_async(dispatch_get_main_queue(), ^{
-        completion(self->_currentUser, handlerAuthFlow.error);
+        if (handlerAuthFlow.error) {
+          completion(nil, handlerAuthFlow.error);
+        } else {
+          OIDAuthState *authState = handlerAuthFlow.authState;
+          NSString *_Nullable serverAuthCode =
+              [authState.lastTokenResponse.additionalParameters[@"server_code"] copy];
+          GIDUserAuth *userAuth = [[GIDUserAuth alloc] initWithGoogleUser:self->_currentUser
+                                                           serverAuthCode:serverAuthCode];
+          completion(userAuth, nil);
+        }
       });
     }
   }];

+ 13 - 8
GoogleSignIn/Sources/GIDSignInInternalOptions.h

@@ -22,9 +22,8 @@
 #import <AppKit/AppKit.h>
 #endif
 
-#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
-
 @class GIDConfiguration;
+@class GIDUserAuth;
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -55,7 +54,8 @@ NS_ASSUME_NONNULL_BEGIN
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
 
 /// The completion block to be called at the completion of the flow.
-@property(nonatomic, readonly, nullable) GIDSignInCompletion completion;
+@property(nonatomic, readonly, nullable) void (^completion)(GIDUserAuth *_Nullable userAuth,
+                                                            NSError *_Nullable error);
 
 /// The scopes to be used during the flow.
 @property(nonatomic, copy, nullable) NSArray<NSString *> *scopes;
@@ -69,32 +69,37 @@ NS_ASSUME_NONNULL_BEGIN
                        presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
-                                     completion:(nullable GIDSignInCompletion)completion;
+                                     completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                                   NSError *_Nullable error))completion;
 
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                        presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
-                                     completion:(nullable GIDSignInCompletion)completion;
+                                     completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                                   NSError *_Nullable error))completion;
 
 #elif TARGET_OS_OSX
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                                presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
-                                     completion:(nullable GIDSignInCompletion)completion;
+                                     completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                                   NSError *_Nullable error))completion;
 
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                                presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
-                                     completion:(nullable GIDSignInCompletion)completion;
+                                     completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                                   NSError *_Nullable error))completion;
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
 
 /// Creates the options to sign in silently.
-+ (instancetype)silentOptionsWithCompletion:(GIDSignInCompletion)completion;
++ (instancetype)silentOptionsWithCompletion:(void (^)(GIDUserAuth *_Nullable userAuth,
+                                                      NSError *_Nullable error))completion;
 
 /// Creates options with the same values as the receiver, except for the "extra parameters", and
 /// continuation flag, which are replaced by the arguments passed to this method.

+ 10 - 6
GoogleSignIn/Sources/GIDSignInInternalOptions.m

@@ -31,16 +31,17 @@ NS_ASSUME_NONNULL_BEGIN
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
-                                     completion:(nullable GIDSignInCompletion)completion {
+                                     completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                                   NSError *_Nullable error))completion {
 #elif TARGET_OS_OSX
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                                presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
-                                     completion:(nullable GIDSignInCompletion)completion {
+                                     completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                                   NSError *_Nullable error))completion {
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
-  
   GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
   if (options) {
     options->_interactive = YES;
@@ -64,13 +65,15 @@ NS_ASSUME_NONNULL_BEGIN
                        presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
-                                     completion:(nullable GIDSignInCompletion)completion {
+                                     completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                                   NSError *_Nullable error))completion {
 #elif TARGET_OS_OSX
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                                presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
-                                     completion:(nullable GIDSignInCompletion)completion {
+                                     completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                                   NSError *_Nullable error))completion {
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
     GIDSignInInternalOptions *options = [self defaultOptionsWithConfiguration:configuration
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
@@ -85,7 +88,8 @@ NS_ASSUME_NONNULL_BEGIN
   return options;
 }
 
-+ (instancetype)silentOptionsWithCompletion:(GIDSignInCompletion)completion {
++ (instancetype)silentOptionsWithCompletion:(void (^)(GIDUserAuth *_Nullable userAuth,
+                                                      NSError *_Nullable error))completion {
   GIDSignInInternalOptions *options = [self defaultOptionsWithConfiguration:nil
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
                                                    presentingViewController:nil

+ 59 - 8
GoogleSignIn/Sources/GIDSignIn_Private.h

@@ -14,32 +14,83 @@
  * limitations under the License.
  */
 
+#import <TargetConditionals.h>
+
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
 
+#if __has_include(<UIKit/UIKit.h>)
+#import <UIKit/UIKit.h>
+#elif __has_include(<AppKit/AppKit.h>)
+#import <AppKit/AppKit.h>
+#endif
+
 NS_ASSUME_NONNULL_BEGIN
 
 @class GIDGoogleUser;
 @class GIDSignInInternalOptions;
 
+/// Represents a completion block that takes a `GIDUserAuth` on success or an error if the operation
+/// was unsuccessful.
+typedef void (^GIDUserAuthCompletion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error);
+
 // Private |GIDSignIn| methods that are used internally in this SDK and other Google SDKs.
 @interface GIDSignIn ()
 
-// Redeclare |currentUser| as readwrite for internal use.
+/// Redeclare |currentUser| as readwrite for internal use.
 @property(nonatomic, readwrite, nullable) GIDGoogleUser *currentUser;
 
-// Private initializer for |GIDSignIn|.
+/// Private initializer for |GIDSignIn|.
 - (instancetype)initPrivate;
 
-// Authenticates with extra options.
+/// Authenticates with extra options.
 - (void)signInWithOptions:(GIDSignInInternalOptions *)options;
 
-// Restores a previously authenticated user from the keychain synchronously without refreshing
-// the access token or making a userinfo request. The currentUser.profile will be nil unless
-// the profile data can be extracted from the ID token.
-//
-// @return NO if there is no user restored from the keychain.
+/// Restores a previously authenticated user from the keychain synchronously without refreshing
+/// the access token or making a userinfo request.
+/// 
+/// The currentUser.profile will be nil unless the profile data can be extracted from the ID token.
+///
+/// @return NO if there is no user restored from the keychain.
 - (BOOL)restorePreviousSignInNoRefresh;
 
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+
+/// Starts an interactive consent flow on iOS to add scopes to the current user's grants.
+///
+/// The completion will be called at the end of this process.  If successful, a `GIDUserAuth`
+/// instance will be returned reflecting the new scopes and saved sign-in state will be updated.
+///
+/// @param scopes The scopes to ask the user to consent to.
+/// @param presentingViewController The view controller used to present `SFSafariViewContoller` on
+///     iOS 9 and 10 and to supply `presentationContextProvider` for `ASWebAuthenticationSession` on
+///     iOS 13+.
+/// @param completion The block that is called on completion.  This block will be called asynchronously
+///     on the main queue.
+- (void)addScopes:(NSArray<NSString *> *)scopes
+    presentingViewController:(UIViewController *)presentingViewController
+                  completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                NSError *_Nullable error))completion
+    NS_EXTENSION_UNAVAILABLE("The add scopes flow is not supported in App Extensions.");
+
+#elif TARGET_OS_OSX
+
+/// Starts an interactive consent flow on macOS to add scopes to the current user's grants.
+///
+/// The completion will be called at the end of this process.  If successful, a `GIDUserAuth`
+/// instance will be returned reflecting the new scopes and saved sign-in state will be updated.
+///
+/// @param scopes An array of scopes to ask the user to consent to.
+/// @param presentingWindow The window used to supply `presentationContextProvider` for
+///     `ASWebAuthenticationSession`.
+/// @param completion The block that is called on completion.  This block will be called asynchronously
+///     on the main queue.
+- (void)addScopes:(NSArray<NSString *> *)scopes
+    presentingWindow:(NSWindow *)presentingWindow
+          completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                        NSError *_Nullable error))completion;
+
+#endif
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 96 - 0
GoogleSignIn/Sources/GIDToken.m

@@ -0,0 +1,96 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h"
+
+#import "GoogleSignIn/Sources/GIDToken_Private.h"
+
+// Key constants used for encode and decode.
+static NSString *const kTokenStringKey = @"tokenString";
+static NSString *const kExpirationDateKey = @"expirationDate";
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation GIDToken
+
+- (instancetype)initWithTokenString:(NSString *)tokenString
+                     expirationDate:(nullable NSDate *)expirationDate {
+  self = [super init];
+  if (self) {
+    _tokenString = [tokenString copy];
+    _expirationDate  = expirationDate;
+  }
+  
+  return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)decoder {
+  self = [super init];
+  if (self) {
+    _tokenString = [decoder decodeObjectOfClass:[NSString class] forKey:kTokenStringKey];
+    _expirationDate = [decoder decodeObjectOfClass:[NSDate class] forKey:kExpirationDateKey];
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+  [encoder encodeObject:_tokenString forKey:kTokenStringKey];
+  [encoder encodeObject:_expirationDate forKey:kExpirationDateKey];
+}
+
+#pragma mark - isEqual
+
+- (BOOL)isEqual:(nullable id)object {
+  if (object == nil) {
+    return NO;
+  }
+  if (self == object) {
+    return YES;
+  }
+  if (![object isKindOfClass:[GIDToken class]]) {
+    return NO;
+  }
+  return [self isEqualToToken:(GIDToken *)object];
+}
+
+- (BOOL)isEqualToToken:(GIDToken *)otherToken {
+  return [_tokenString isEqual:otherToken.tokenString] &&
+      [self isTheSameDate:_expirationDate with:otherToken.expirationDate];
+}
+
+// The date is nullable in GIDToken. Two `nil` dates are considered equal so
+// token equality check succeeds if token strings are equal and have no expiration.
+- (BOOL)isTheSameDate:(nullable NSDate *)date1
+                 with:(nullable NSDate *)date2 {
+  if (!date1 && !date2) {
+    return YES;
+  }
+  return [date1 isEqualToDate:date2];
+}
+
+- (NSUInteger)hash {
+  return [self.tokenString hash] ^ [self.expirationDate hash];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 32 - 0
GoogleSignIn/Sources/GIDToken_Private.h

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Private |GIDToken| methods that are used in this SDK.
+@interface GIDToken ()
+
+// Private initializer for |GIDToken|.
+// @param token The token String.
+// @param expirationDate The expiration date of the token.
+- (instancetype)initWithTokenString:(NSString *)tokenString
+                     expirationDate:(nullable NSDate *)expirationDate;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 35 - 0
GoogleSignIn/Sources/GIDUserAuth.m

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h"
+
+#import "GoogleSignIn/Sources/GIDUserAuth_Private.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
+
+@implementation GIDUserAuth
+
+- (instancetype)initWithGoogleUser:(GIDGoogleUser *)user
+                    serverAuthCode:(nullable NSString *)serverAuthCode {
+  self = [super init];
+  if (self) {
+    _user = user;
+    _serverAuthCode = serverAuthCode;
+  }
+  
+  return self;
+}
+
+@end

+ 33 - 0
GoogleSignIn/Sources/GIDUserAuth_Private.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Private |GIDUserAuth| methods that are used in this SDK.
+@interface GIDUserAuth ()
+
+// Private initializer for |GIDUserAuth|.
+// @param user The current GIDGoogleUser.
+// @param severAuthCode The one-time authorization code for backend to exchange
+//     access and refresh tokens.
+- (instancetype)initWithGoogleUser:(GIDGoogleUser *)user
+                    serverAuthCode:(nullable NSString *)serverAuthCode;
+
+@end
+
+NS_ASSUME_NONNULL_END

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

@@ -1,78 +0,0 @@
-/*
- * Copyright 2021 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#import <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;
-
-/// Gets a new authorizer for `GTLService`, `GTMSessionFetcher`, or `GTMHTTPFetcher`.
-///
-/// @return A new authorizer
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-- (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 `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

+ 81 - 14
GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h

@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 Google LLC
+ * Copyright 2022 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,12 +15,29 @@
  */
 
 #import <Foundation/Foundation.h>
+#import <TargetConditionals.h>
 
-NS_ASSUME_NONNULL_BEGIN
+#if __has_include(<UIKit/UIKit.h>)
+#import <UIKit/UIKit.h>
+#elif __has_include(<AppKit/AppKit.h>)
+#import <AppKit/AppKit.h>
+#endif
+
+// We have to import GTMAppAuth because forward declaring the protocol does
+// not generate the `fetcherAuthorizer` property below for Swift.
+#ifdef SWIFT_PACKAGE
+@import GTMAppAuth;
+#else
+#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
+#endif
 
-@class GIDAuthentication;
+@class GIDConfiguration;
+@class GIDUserAuth;
+@class GIDToken;
 @class GIDProfileData;
 
+NS_ASSUME_NONNULL_BEGIN
+
 /// This class represents a user account.
 @interface GIDGoogleUser : NSObject <NSSecureCoding>
 
@@ -30,24 +47,74 @@ NS_ASSUME_NONNULL_BEGIN
 /// Representation of basic profile data for the user.
 @property(nonatomic, readonly, nullable) GIDProfileData *profile;
 
-/// The authentication object for the user.
-@property(nonatomic, readonly) GIDAuthentication *authentication;
-
 /// The API scopes granted to the app in an array of `NSString`.
 @property(nonatomic, readonly, nullable) NSArray<NSString *> *grantedScopes;
 
-/// For Google Apps hosted accounts, the domain of the user.
-@property(nonatomic, readonly, nullable) NSString *hostedDomain;
+/// The configuration that was used to sign in this user.
+@property(nonatomic, readonly) GIDConfiguration *configuration;
+
+/// The OAuth2 access token to access Google services.
+@property(nonatomic, readonly) GIDToken *accessToken;
+
+/// The OAuth2 refresh token to exchange for new access tokens.
+@property(nonatomic, readonly) GIDToken *refreshToken;
+
+/// An OpenID Connect ID token that identifies the user.
+///
+/// Send this token to your server to authenticate the user there. For more information on this topic,
+/// see https://developers.google.com/identity/sign-in/ios/backend-auth.
+@property(nonatomic, readonly, nullable) GIDToken *idToken;
+
+/// The authorizer for `GTLService`, `GTMSessionFetcher`, or `GTMHTTPFetcher`.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+@property(nonatomic, readonly) id<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)refreshTokensIfNeededWithCompletion:(void (^)(GIDGoogleUser *_Nullable user,
+                                                      NSError *_Nullable error))completion;
+
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 
-/// The client ID of the home server.
-@property(nonatomic, readonly, nullable) NSString *serverClientID;
+/// Starts an interactive consent flow on iOS to add scopes to the current user's grants.
+///
+/// The completion will be called at the end of this process.  If successful, a `GIDUserAuth`
+/// instance will be returned reflecting the new scopes and saved sign-in state will be updated.
+///
+/// @param scopes The scopes to ask the user to consent to.
+/// @param presentingViewController The view controller used to present `SFSafariViewContoller` on
+///     iOS 9 and 10 and to supply `presentationContextProvider` for `ASWebAuthenticationSession` on
+///     iOS 13+.
+/// @param completion The block that is called on completion.  This block will be called asynchronously
+///     on the main queue.
+- (void)addScopes:(NSArray<NSString *> *)scopes
+    presentingViewController:(UIViewController *)presentingViewController
+                  completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                NSError *_Nullable error))completion
+    NS_EXTENSION_UNAVAILABLE("The add scopes flow is not supported in App Extensions.");
 
-/// An OAuth2 authorization code for the home server.
-@property(nonatomic, readonly, nullable) NSString *serverAuthCode;
+#elif TARGET_OS_OSX
 
-/// The OpenID2 realm of the home server.
-@property(nonatomic, readonly, nullable) NSString *openIDRealm;
+/// Starts an interactive consent flow on macOS to add scopes to the current user's grants.
+///
+/// The completion will be called at the end of this process.  If successful, a `GIDUserAuth`
+/// instance will be returned reflecting the new scopes and saved sign-in state will be updated.
+///
+/// @param scopes An array of scopes to ask the user to consent to.
+/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`.
+/// @param completion The block that is called on completion.  This block will be called asynchronously
+///     on the main queue.
+- (void)addScopes:(NSArray<NSString *> *)scopes
+    presentingWindow:(NSWindow *)presentingWindow
+          completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                        NSError *_Nullable error))completion;
 
+#endif
 
 @end
 

+ 19 - 45
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h

@@ -25,6 +25,7 @@
 
 @class GIDConfiguration;
 @class GIDGoogleUser;
+@class GIDUserAuth;
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -44,16 +45,12 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) {
   kGIDSignInErrorCodeCanceled = -5,
   /// Indicates an Enterprise Mobility Management related error has occurred.
   kGIDSignInErrorCodeEMM = -6,
-  /// Indicates there is no `currentUser`.
-  kGIDSignInErrorCodeNoCurrentUser = -7,
   /// Indicates the requested scopes have already been granted to the `currentUser`.
   kGIDSignInErrorCodeScopesAlreadyGranted = -8,
+  /// Indicates there is an operation on a previous user.
+  kGIDSignInErrorCodeMismatchWithCurrentUser = -9,
 };
 
-/// Represents a completion block that takes a `GIDGoogleUser` on success or an error if the operation
-/// was unsuccessful.
-typedef void (^GIDSignInCompletion)(GIDGoogleUser *_Nullable user, NSError *_Nullable error);
-
 /// Represents a completion block that takes an error if the operation was unsuccessful.
 typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 
@@ -94,9 +91,10 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 
 /// Attempts to restore a previously authenticated user without interaction.
 ///
-/// @param completion The `GIDSignInCompletion` block that is called on completion.  This block will
-///     be called asynchronously on the main queue.
-- (void)restorePreviousSignInWithCompletion:(nullable GIDSignInCompletion)completion;
+/// @param completion The block that is called on completion.  This block will be called asynchronously
+///     on the main queue.
+- (void)restorePreviousSignInWithCompletion:(nullable void (^)(GIDGoogleUser *_Nullable user,
+                                                               NSError *_Nullable error))completion;
 
 /// Marks current user as being in the signed out state.
 - (void)signOut;
@@ -122,7 +120,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 /// @param completion The `GIDSignInCompletion` block that is called on completion.  This block will
 ///     be called asynchronously on the main queue.
 - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
-                                completion:(nullable GIDSignInCompletion)completion
+                                completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                              NSError *_Nullable error))completion
     NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
 
 /// Starts an interactive sign-in flow on iOS using the provided hint.
@@ -141,7 +140,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 ///     be called asynchronously on the main queue.
 - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
                                       hint:(nullable NSString *)hint
-                                completion:(nullable GIDSignInCompletion)completion
+                                completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                              NSError *_Nullable error))completion
     NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
 
 /// Starts an interactive sign-in flow on iOS using the provided hint and additional scopes.
@@ -162,23 +162,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
                                       hint:(nullable NSString *)hint
                           additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
-                                completion:(nullable GIDSignInCompletion)completion;
-
-/// Starts an interactive consent flow on iOS to add scopes to the current user's grants.
-///
-/// The completion will be called at the end of this process.  If successful, a new `GIDGoogleUser`
-/// instance will be returned reflecting the new scopes and saved sign-in state will be updated.
-///
-/// @param scopes The scopes to ask the user to consent to.
-/// @param presentingViewController The view controller used to present `SFSafariViewContoller` on
-///     iOS 9 and 10 and to supply `presentationContextProvider` for `ASWebAuthenticationSession` on
-///     iOS 13+.
-/// @param completion The `GIDSignInCompletion` block that is called on completion.  This block will
-///     be called asynchronously on the main queue.
-- (void)addScopes:(NSArray<NSString *> *)scopes
-    presentingViewController:(UIViewController *)presentingViewController
-                  completion:(nullable GIDSignInCompletion)completion
-    NS_EXTENSION_UNAVAILABLE("The add scopes flow is not supported in App Extensions."); 
+                                completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                              NSError *_Nullable error))completion;
 
 #elif TARGET_OS_OSX
 /// Starts an interactive sign-in flow on macOS.
@@ -192,7 +177,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 /// @param completion The `GIDSignInCompletion` block that is called on completion.  This block will
 ///     be called asynchronously on the main queue.
 - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
-                        completion:(nullable GIDSignInCompletion)completion;
+                        completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                      NSError *_Nullable error))completion;
 
 /// Starts an interactive sign-in flow on macOS using the provided hint.
 ///
@@ -208,7 +194,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 ///     be called asynchronously on the main queue.
 - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
                               hint:(nullable NSString *)hint
-                        completion:(nullable GIDSignInCompletion)completion;
+                        completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                      NSError *_Nullable error))completion;
 
 /// Starts an interactive sign-in flow on macOS using the provided hint.
 ///
@@ -223,24 +210,11 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 /// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes.
 /// @param completion The `GIDSignInCompletion` block that is called on completion.  This block will
 ///     be called asynchronously on the main queue.
-
 - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
                               hint:(nullable NSString *)hint
                   additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
-                        completion:(nullable GIDSignInCompletion)completion;
-
-/// Starts an interactive consent flow on macOS to add scopes to the current user's grants.
-///
-/// The completion will be called at the end of this process.  If successful, a new `GIDGoogleUser`
-/// instance will be returned reflecting the new scopes and saved sign-in state will be updated.
-///
-/// @param scopes An array of scopes to ask the user to consent to.
-/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`.
-/// @param completion The `GIDSignInCompletion` block that is called on completion.  This block will
-///     be called asynchronously on the main queue.
-- (void)addScopes:(NSArray<NSString *> *)scopes
- presentingWindow:(NSWindow *)presentingWindow
-       completion:(nullable GIDSignInCompletion)completion;
+                        completion:(nullable void (^)(GIDUserAuth *_Nullable userAuth,
+                                                      NSError *_Nullable error))completion;
 
 #endif
 

+ 45 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// This class represents the basic information of a token.
+@interface GIDToken : NSObject <NSSecureCoding>
+
+/// The token string.
+@property(nonatomic, copy, readonly) NSString *tokenString;
+
+/// The estimated expiration date of the token.
+@property(nonatomic, readonly, nullable) NSDate *expirationDate;
+
+/// Check if current token is equal to another one.
+///
+/// @param otherToken Another token to compare.
+- (BOOL)isEqualToToken:(GIDToken *)otherToken;
+
+/// Unavailable.
+/// :nodoc:
++ (instancetype)new NS_UNAVAILABLE;
+
+/// Unavailable.
+/// :nodoc:
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 40 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h

@@ -0,0 +1,40 @@
+/*
+* Copyright 2022 Google LLC
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+#import <Foundation/Foundation.h>
+
+@class GIDGoogleUser;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A helper object that contains the outcome of a successful signIn or addScopes flow.
+@interface GIDUserAuth : NSObject
+
+/// The updated `GIDGoogleUser` instance for the user who just completed the flow.
+@property(nonatomic, readonly) GIDGoogleUser *user;
+
+/// An OAuth2 authorization code for the home server.
+@property(nonatomic, readonly, nullable) NSString *serverAuthCode;
+
+/// Unsupported.
++ (instancetype)new NS_UNAVAILABLE;
+
+/// Unsupported.
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END

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

@@ -15,11 +15,12 @@
  */
 #import <TargetConditionals.h>
 
-#import "GIDAuthentication.h"
 #import "GIDConfiguration.h"
 #import "GIDGoogleUser.h"
 #import "GIDProfileData.h"
 #import "GIDSignIn.h"
+#import "GIDToken.h"
+#import "GIDUserAuth.h"
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 #import "GIDSignInButton.h"
 #endif

+ 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 - 684
GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m

@@ -1,684 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#import <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)testFetcherAuthorizer {
-  // This is really hard to test without assuming how GTMAppAuthFetcherAuthorization works
-  // internally, so let's just take the shortcut here by asserting we get a
-  // GTMAppAuthFetcherAuthorization object.
-  GIDAuthentication *auth = [self auth];
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-  id<GTMFetcherAuthorizationProtocol> fetcherAuthroizer = auth.fetcherAuthorizer;
-#pragma clang diagnostic pop
-  XCTAssertTrue([fetcherAuthroizer isKindOfClass:[GTMAppAuthFetcherAuthorization class]]);
-  XCTAssertTrue([fetcherAuthroizer canAuthorize]);
-}
-
-- (void)testDoWithFreshTokensWithBothExpired {
-  // Both tokens expired 10 seconds ago.
-  [self setExpireTimeForAccessToken:-10 IDToken:-10];
-  [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
-}
-
-- (void)testDoWithFreshTokensWithAccessTokenExpired {
-  // Access token expired 10 seconds ago while ID token to expire in 10 minutes.
-  [self setExpireTimeForAccessToken:-10 IDToken:10 * 60];
-  [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
-}
-
-- (void)testDoWithFreshTokensWithIDTokenToExpire {
-  // Access token to expire in 10 minutes while ID token to expire in 10 seconds.
-  [self setExpireTimeForAccessToken:10 * 60 IDToken:10];
-  [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
-}
-
-- (void)testDoWithFreshTokensWithBothFresh {
-  // Both tokens to expire in 10 minutes.
-  [self setExpireTimeForAccessToken:10 * 60 IDToken:10 * 60];
-  [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)];
-}
-
-- (void)testDoWithFreshTokensWithAccessTokenExpiredAndNoIDToken {
-  _hasIDToken = NO;
-  [self setExpireTimeForAccessToken:-10 IDToken:10 * 60];  // access token expired 10 seconds ago
-  [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
-}
-
-- (void)testDoWithFreshTokensWithAccessTokenFreshAndNoIDToken {
-  _hasIDToken = NO;
-  [self setExpireTimeForAccessToken:10 * 60 IDToken:-10];  // access token to expire in 10 minutes
-  [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)];
-}
-
-- (void)testDoWithFreshTokensError {
-  [self setTokensExpireTime:-10];  // expired 10 seconds ago
-  GIDAuthentication *auth = [self observedAuth];
-  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
-  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
-    [expectation fulfill];
-    XCTAssertNil(authentication);
-    XCTAssertNotNil(error);
-  }];
-  _tokenFetchHandler(nil, [self fakeError]);
-  [self waitForExpectationsWithTimeout:1 handler:nil];
-  [self assertOldTokensInAuth:auth];
-}
-
-- (void)testDoWithFreshTokensQueue {
-  GIDAuthentication *auth = [self observedAuth];
-  XCTestExpectation *firstExpectation =
-      [self expectationWithDescription:@"First callback is called"];
-  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
-    [firstExpectation fulfill];
-    [self assertNewTokensInAuth:authentication];
-    XCTAssertNil(error);
-  }];
-  XCTestExpectation *secondExpectation =
-      [self expectationWithDescription:@"Second callback is called"];
-  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
-    [secondExpectation fulfill];
-    [self assertNewTokensInAuth:authentication];
-    XCTAssertNil(error);
-  }];
-  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
-  [self waitForExpectationsWithTimeout:1 handler:nil];
-  [self assertNewTokensInAuth:auth];
-}
-
-#pragma mark - EMM Support
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-- (void)testEMMSupport {
-  _additionalTokenRequestParameters = @{
-    @"emm_support" : @"xyz",
-  };
-  GIDAuthentication *auth = [self auth];
-  [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
-                            NSError * _Nullable error) {}];
-  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
-  NSDictionary *expectedParameters = @{
-    @"emm_support" : @"xyz",
-    @"device_os" : [NSString stringWithFormat:@"%@ %@",
-        _fakeSystemName, [UIDevice currentDevice].systemVersion],
-    kSDKVersionLoggingParameter : GIDVersion(),
-    kEnvironmentLoggingParameter : GIDEnvironment(),
-  };
-  XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
-                        expectedParameters);
-}
-
-- (void)testSystemNameNormalization {
-  _fakeSystemName = kOldIOSName;
-  _additionalTokenRequestParameters = @{
-    @"emm_support" : @"xyz",
-  };
-  GIDAuthentication *auth = [self auth];
-  [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
-                            NSError * _Nullable error) {}];
-  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
-  NSDictionary *expectedParameters = @{
-    @"emm_support" : @"xyz",
-    @"device_os" : [NSString stringWithFormat:@"%@ %@",
-        kNewIOSName, [UIDevice currentDevice].systemVersion],
-    kSDKVersionLoggingParameter : GIDVersion(),
-    kEnvironmentLoggingParameter : GIDEnvironment(),
-  };
-  XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
-                        expectedParameters);
-}
-
-- (void)testEMMPasscodeInfo {
-  _additionalTokenRequestParameters = @{
-    @"emm_support" : @"xyz",
-    @"device_os" : @"old one",
-    @"emm_passcode_info" : @"something",
-  };
-  GIDAuthentication *auth = [self auth];
-  [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
-                            NSError * _Nullable error) {}];
-  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
-  NSDictionary *expectedParameters = @{
-    @"emm_support" : @"xyz",
-    @"device_os" : [NSString stringWithFormat:@"%@ %@",
-        _fakeSystemName, [UIDevice currentDevice].systemVersion],
-    @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info,
-    kSDKVersionLoggingParameter : GIDVersion(),
-    kEnvironmentLoggingParameter : GIDEnvironment(),
-  };
-  XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
-                        expectedParameters);
-}
-
-- (void)testEMMError {
-  // Set expectations.
-  NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" };
-  NSError *emmError = [NSError errorWithDomain:@"anydomain"
-                                          code:12345
-                                      userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
-  id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
-  [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
-  __block void (^completion)(void);
-  [[[mockEMMErrorHandler expect] andReturnValue:@YES]
-      handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
-    completion = arg;
-    return YES;
-  }]];
-
-  // Start testing.
-  _additionalTokenRequestParameters = @{
-    @"emm_support" : @"xyz",
-  };
-  GIDAuthentication *auth = [self auth];
-  XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"];
-  notCalled.inverted = YES;
-  XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"];
-  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
-    [notCalled fulfill];
-    [called fulfill];
-    XCTAssertNil(authentication);
-    XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain);
-    XCTAssertEqual(error.code, kGIDSignInErrorCodeEMM);
-  }];
-  _tokenFetchHandler(nil, emmError);
-
-  // Verify and clean up.
-  [mockEMMErrorHandler verify];
-  [mockEMMErrorHandler stopMocking];
-  [self waitForExpectations:@[ notCalled ] timeout:1];
-  completion();
-  [self waitForExpectations:@[ called ] timeout:1];
-  [self assertOldTokensInAuth:auth];
-}
-
-- (void)testNonEMMError {
-  // Set expectations.
-  NSDictionary *errorJSON = @{ @"error" : @"Not EMM Specific Error" };
-  NSError *emmError = [NSError errorWithDomain:@"anydomain"
-                                          code:12345
-                                      userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
-  id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
-  [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
-  __block void (^completion)(void);
-  [[[mockEMMErrorHandler expect] andReturnValue:@NO]
-      handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
-    completion = arg;
-    return YES;
-  }]];
-
-  // Start testing.
-  _additionalTokenRequestParameters = @{
-    @"emm_support" : @"xyz",
-  };
-  GIDAuthentication *auth = [self auth];
-  XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"];
-  notCalled.inverted = YES;
-  XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"];
-  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
-    [notCalled fulfill];
-    [called fulfill];
-    XCTAssertNil(authentication);
-    XCTAssertEqualObjects(error.domain, @"anydomain");
-    XCTAssertEqual(error.code, 12345);
-  }];
-  _tokenFetchHandler(nil, emmError);
-
-  // Verify and clean up.
-  [mockEMMErrorHandler verify];
-  [mockEMMErrorHandler stopMocking];
-  [self waitForExpectations:@[ notCalled ] timeout:1];
-  completion();
-  [self waitForExpectations:@[ called ] timeout:1];
-  [self assertOldTokensInAuth:auth];
-}
-
-- (void)testCodingPreserveEMMParameters {
-  _additionalTokenRequestParameters = @{
-    @"emm_support" : @"xyz",
-    @"device_os" : @"old one",
-    @"emm_passcode_info" : @"something",
-  };
-  NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[self auth]];
-  GIDAuthentication *auth = [NSKeyedUnarchiver unarchiveObjectWithData:data];
-  [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
-                            NSError * _Nullable error) {}];
-  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
-  NSDictionary *expectedParameters = @{
-    @"emm_support" : @"xyz",
-    @"device_os" : [NSString stringWithFormat:@"%@ %@",
-        [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion],
-    @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info,
-    kSDKVersionLoggingParameter : GIDVersion(),
-    kEnvironmentLoggingParameter : GIDEnvironment(),
-  };
-  XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
-                        expectedParameters);
-}
-
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-#pragma mark - NSKeyValueObserving
-
-- (void)observeValueForKeyPath:(NSString *)keyPath
-                      ofObject:(id)object
-                        change:(NSDictionary *)change
-                       context:(void *)context {
-  GIDAuthentication *auth = (GIDAuthentication *)object;
-  ChangeType changeType;
-  if ([keyPath isEqualToString:@"accessToken"]) {
-    if (change[NSKeyValueChangeNotificationIsPriorKey]) {
-      XCTAssertEqualObjects(auth.accessToken, kAccessToken);
-      changeType = kChangeTypeAccessTokenPrior;
-    } else {
-      XCTAssertEqualObjects(auth.accessToken, kNewAccessToken);
-      changeType = kChangeTypeAccessToken;
-    }
-  } else if ([keyPath isEqualToString:@"accessTokenExpirationDate"]) {
-    if (change[NSKeyValueChangeNotificationIsPriorKey]) {
-      [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime];
-      changeType = kChangeTypeAccessTokenExpirationDatePrior;
-    } else {
-      [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime];
-      changeType = kChangeTypeAccessTokenExpirationDate;
-    }
-  } else if ([keyPath isEqualToString:@"idToken"]) {
-    if (change[NSKeyValueChangeNotificationIsPriorKey]) {
-      XCTAssertEqualObjects(auth.idToken, [self idToken]);
-      changeType = kChangeTypeIDTokenPrior;
-    } else {
-      XCTAssertEqualObjects(auth.idToken, [self idTokenNew]);
-      changeType = kChangeTypeIDToken;
-    }
-  } else if ([keyPath isEqualToString:@"idTokenExpirationDate"]) {
-    if (change[NSKeyValueChangeNotificationIsPriorKey]) {
-      if (_hasIDToken) {
-        [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime];
-      }
-      changeType = kChangeTypeIDTokenExpirationDatePrior;
-    } else {
-      if (_hasIDToken) {
-        [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2];
-      }
-      changeType = kChangeTypeIDTokenExpirationDate;
-    }
-  } else {
-    XCTFail(@"unexpected keyPath");
-    return;  // so compiler knows |changeType| is always assigned
-  }
-  NSUInteger changeMask = 1u << changeType;
-  XCTAssertFalse(_changesObserved & changeMask);  // each change type should only fire once
-  _changesObserved |= changeMask;
-}
-
-#pragma mark - Helpers
-
-- (GIDAuthentication *)auth {
-  NSString *idToken = [self idToken];
-  NSNumber *accessTokenExpiresIn =
-      @(_accessTokenExpireTime - [[NSDate date] timeIntervalSinceReferenceDate]);
-  OIDTokenRequest *tokenRequest =
-      [OIDTokenRequest testInstanceWithAdditionalParameters:_additionalTokenRequestParameters];
-  OIDTokenResponse *tokenResponse =
-      [OIDTokenResponse testInstanceWithIDToken:idToken
-                                    accessToken:kAccessToken
-                                      expiresIn:accessTokenExpiresIn
-                                   tokenRequest:tokenRequest];
-  return [[GIDAuthentication alloc]
-      initWithAuthState:[OIDAuthState testInstanceWithTokenResponse:tokenResponse]];
-}
-
-- (NSString *)idTokenWithExpireTime:(NSTimeInterval)expireTime {
-  if (!_hasIDToken) {
-    return nil;
-  }
-  return [OIDTokenResponse idTokenWithSub:kUserID exp:@(expireTime + NSTimeIntervalSince1970)];
-}
-
-- (NSString *)idToken {
-  return [self idTokenWithExpireTime:_idTokenExpireTime];
-}
-
-- (NSString *)idTokenNew {
-  return [self idTokenWithExpireTime:kNewExpireTime2];
-}
-
-// Return the auth object that has certain property changes observed.
-- (GIDAuthentication *)observedAuth {
-  GIDAuthentication *auth = [self auth];
-  for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) {
-    [auth addObserver:self
-           forKeyPath:kObservedProperties[i]
-              options:NSKeyValueObservingOptionPrior
-              context:NULL];
-  }
-  [_observedAuths addObject:auth];
-  return auth;
-}
-
-- (OIDTokenResponse *)tokenResponseWithNewTokens {
-  NSNumber *expiresIn = @(kNewExpireTime - [NSDate timeIntervalSinceReferenceDate]);
-  return [OIDTokenResponse testInstanceWithIDToken:(_hasIDToken ? [self idTokenNew] : nil)
-                                       accessToken:kNewAccessToken
-                                         expiresIn:expiresIn
-                                      tokenRequest:_tokenRequest ?: nil];
-}
-
-- (NSError *)fakeError {
-  return [NSError errorWithDomain:@"fake.domain" code:-1 userInfo:nil];
-}
-
-- (void)assertDate:(NSDate *)date equalTime:(NSTimeInterval)time {
-  XCTAssertEqualWithAccuracy([date timeIntervalSinceReferenceDate], time, kTimeAccuracy);
-}
-
-- (void)assertOldAccessTokenInAuth:(GIDAuthentication *)auth {
-  XCTAssertEqualObjects(auth.accessToken, kAccessToken);
-  [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime];
-  XCTAssertEqual(_changesObserved, kChangeNone);
-}
-
-- (void)assertNewAccessTokenInAuth:(GIDAuthentication *)auth {
-  XCTAssertEqualObjects(auth.accessToken, kNewAccessToken);
-  [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime];
-  XCTAssertEqual(_changesObserved, kChangeAll);
-}
-
-- (void)assertOldTokensInAuth:(GIDAuthentication *)auth {
-  [self assertOldAccessTokenInAuth:auth];
-  XCTAssertEqualObjects(auth.idToken, [self idToken]);
-  if (_hasIDToken) {
-    [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime];
-  }
-}
-
-- (void)assertNewTokensInAuth:(GIDAuthentication *)auth {
-  [self assertNewAccessTokenInAuth:auth];
-  XCTAssertEqualObjects(auth.idToken, [self idTokenNew]);
-  if (_hasIDToken) {
-    [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2];
-  }
-}
-
-- (void)setTokensExpireTime:(NSTimeInterval)fromNow {
-  [self setExpireTimeForAccessToken:fromNow IDToken:fromNow];
-}
-
-- (void)setExpireTimeForAccessToken:(NSTimeInterval)accessExpire IDToken:(NSTimeInterval)idExpire {
-  _accessTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + accessExpire;
-  _idTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + idExpire;
-}
-
-- (void)verifyTokensRefreshedWithMethod:(SEL)sel {
-  GIDAuthentication *auth = [self observedAuth];
-  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
-  // We know the method doesn't return anything, so there is no risk of leaking.
-  [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) {
-#pragma clang diagnostic pop
-    [expectation fulfill];
-    [self assertNewTokensInAuth:authentication];
-    XCTAssertNil(error);
-  }];
-  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
-  [self waitForExpectationsWithTimeout:1 handler:nil];
-  [self assertNewTokensInAuth:auth];
-}
-
-- (void)verifyTokensNotRefreshedWithMethod:(SEL)sel {
-  GIDAuthentication *auth = [self observedAuth];
-  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
-  // We know the method doesn't return anything, so there is no risk of leaking.
-  [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) {
-#pragma clang diagnostic pop
-    [expectation fulfill];
-    [self assertOldTokensInAuth:authentication];
-    XCTAssertNil(error);
-  }];
-  XCTAssertNil(_tokenFetchHandler);
-  [self waitForExpectationsWithTimeout:1 handler:nil];
-  [self assertOldTokensInAuth:auth];
-}
-
-@end

+ 6 - 0
GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.m

@@ -46,6 +46,12 @@ NSString *const kOpenIDRealm = @"fakeOpenIDRealm";
           self.openIDRealm == other.openIDRealm);
 }
 
+// Not the hash implemention you want to use on prod, but just to match |isEqual:| here.
+- (NSUInteger)hash {
+  return [self.clientID hash] ^ [self.serverClientID hash] ^ [self.hostedDomain hash] ^
+      [self.openIDRealm hash];
+}
+
 + (instancetype)testInstance {
   return [[GIDConfiguration alloc] initWithClientID:OIDAuthorizationRequestTestingClientID
                                      serverClientID:kServerClientID

+ 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

+ 229 - 0
GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m

@@ -0,0 +1,229 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <XCTest/XCTest.h>
+
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+@import GoogleUtilities_MethodSwizzler;
+@import GoogleUtilities_SwizzlerTestHelpers;
+@import OCMock;
+#else
+#import <GoogleUtilities/GULSwizzler.h>
+#import <GoogleUtilities/GULSwizzler+Unswizzle.h>
+#import <AppAuth/OIDError.h>
+#import <OCMock/OCMock.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// The system name in old iOS versions.
+static NSString *const kOldIOSName = @"iPhone OS";
+
+// The system name in new iOS versions.
+static NSString *const kNewIOSName = @"iOS";
+
+// They keys in EMM dictionary.
+static NSString *const kEMMKey = @"emm_support";
+static NSString *const kDeviceOSKey = @"device_os";
+static NSString *const kEMMPasscodeInfoKey = @"emm_passcode_info";
+
+@interface GIDEMMSupportTest : XCTestCase
+@end
+
+@implementation GIDEMMSupportTest
+
+- (void)testUpdatedEMMParametersWithParameters_NoEMMKey {
+  NSDictionary *originalParameters = @{
+    @"not_emm_support_key" : @"xyz",
+  };
+
+  NSDictionary *updatedEMMParameters =
+      [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters];
+
+  XCTAssertEqualObjects(updatedEMMParameters, originalParameters);
+}
+
+- (void)testUpdateEMMParametersWithParameters_systemName {
+  [GULSwizzler swizzleClass:[UIDevice class]
+                   selector:@selector(systemName)
+            isClassSelector:NO
+                  withBlock:^(id sender) { return kNewIOSName; }];
+
+  NSDictionary *originalParameters = @{
+    kEMMKey : @"xyz",
+  };
+
+  NSDictionary *updatedEMMParameters =
+      [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters];
+
+  NSDictionary *expectedParameters = @{
+    kEMMKey : @"xyz",
+    kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]]
+  };
+
+  XCTAssertEqualObjects(updatedEMMParameters, expectedParameters);
+
+  [self addTeardownBlock:^{
+    [GULSwizzler unswizzleClass:[UIDevice class]
+                       selector:@selector(systemName)
+                isClassSelector:NO];
+  }];
+}
+
+// When the systemName is @"iPhone OS" we still get "iOS".
+- (void)testUpdateEMMParametersWithParameters_systemNameNormalization {
+  [GULSwizzler swizzleClass:[UIDevice class]
+                    selector:@selector(systemName)
+             isClassSelector:NO
+                   withBlock:^(id sender) { return kOldIOSName; }];
+
+  NSDictionary *originalParameters = @{
+    kEMMKey : @"xyz",
+  };
+
+  NSDictionary *updatedEMMParameters =
+      [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters];
+
+  NSDictionary *expectedParameters = @{
+    kEMMKey : @"xyz",
+    kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]]
+  };
+
+  XCTAssertEqualObjects(updatedEMMParameters, expectedParameters);
+
+  [self addTeardownBlock:^{
+    [GULSwizzler unswizzleClass:[UIDevice class]
+                       selector:@selector(systemName)
+                isClassSelector:NO];
+  }];
+}
+
+- (void)testUpdateEMMParametersWithParameters_passcodInfo {
+  [GULSwizzler swizzleClass:[UIDevice class]
+                   selector:@selector(systemName)
+            isClassSelector:NO
+                  withBlock:^(id sender) { return kOldIOSName; }];
+
+  NSDictionary *originalParameters = @{
+    kEMMKey : @"xyz",
+    kDeviceOSKey : @"old one",
+    kEMMPasscodeInfoKey : @"something",
+  };
+
+  NSDictionary *updatedEMMParameters =
+      [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters];
+
+  NSDictionary *expectedParameters = @{
+    kEMMKey : @"xyz",
+    kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]],
+    kEMMPasscodeInfoKey : [GIDMDMPasscodeState passcodeState].info,
+  };
+
+  XCTAssertEqualObjects(updatedEMMParameters, expectedParameters);
+
+  [self addTeardownBlock:^{
+    [GULSwizzler unswizzleClass:[UIDevice class]
+                       selector:@selector(systemName)
+                isClassSelector:NO];
+  }];
+  
+}
+
+- (void)testHandleTokenFetchEMMError_errorIsEMM {
+  // Set expectations.
+  NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" };
+  NSError *emmError = [NSError errorWithDomain:@"anydomain"
+                                          code:12345
+                                      userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
+  id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
+  [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
+  __block void (^savedCompletion)(void);
+  [[[mockEMMErrorHandler stub] andReturnValue:@YES]
+      handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
+    savedCompletion = arg;
+    return YES;
+  }]];
+
+  XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"];
+  notCalled.inverted = YES;
+  XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"];
+
+  [GIDEMMSupport handleTokenFetchEMMError:emmError completion:^(NSError *error) {
+    [notCalled fulfill];
+    [called fulfill];
+    XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain);
+    XCTAssertEqual(error.code, kGIDSignInErrorCodeEMM);
+  }];
+  
+  [self waitForExpectations:@[ notCalled ] timeout:1];
+  savedCompletion();
+  [self waitForExpectations:@[ called ] timeout:1];
+}
+
+- (void)testHandleTokenFetchEMMError_errorIsNotEMM {
+  // Set expectations.
+  NSDictionary *errorJSON = @{ @"error" : @"Not EMM Specific Error" };
+  NSError *emmError = [NSError errorWithDomain:@"anydomain"
+                                          code:12345
+                                      userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
+  id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
+  [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
+  __block void (^savedCompletion)(void);
+  [[[mockEMMErrorHandler stub] andReturnValue:@NO]
+      handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
+    savedCompletion = arg;
+    return YES;
+  }]];
+
+  XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"];
+  notCalled.inverted = YES;
+  XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"];
+  
+  [GIDEMMSupport handleTokenFetchEMMError:emmError completion:^(NSError *error) {
+    [notCalled fulfill];
+    [called fulfill];
+    XCTAssertEqualObjects(error.domain, @"anydomain");
+    XCTAssertEqual(error.code, 12345);
+  }];
+
+  [self waitForExpectations:@[ notCalled ] timeout:1];
+  savedCompletion();
+  [self waitForExpectations:@[ called ] timeout:1];
+}
+
+# pragma mark - Helpers
+
+- (NSString *)systemVersion {
+  return [UIDevice currentDevice].systemVersion;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 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

+ 42 - 7
GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m

@@ -14,9 +14,19 @@
 
 #import "GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h"
 
-#import "GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h"
+#import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
+
+#import "GoogleSignIn/Sources/GIDAuthentication.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h"
+
+#import "GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.h"
 #import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h"
 
+// Key constants used for encode and decode.
+static NSString *const kProfileDataKey = @"profileData";
+static NSString *const kAuthentication = @"authentication";
+
 @implementation GIDGoogleUser (Testing)
 
 - (BOOL)isEqual:(id)object {
@@ -30,17 +40,42 @@
 }
 
 - (BOOL)isEqualToGoogleUser:(GIDGoogleUser *)other {
-  return [self.authentication isEqual:other.authentication] &&
-      [self.userID isEqual:other.userID] &&
-      [self.serverAuthCode isEqual:other.serverAuthCode] &&
+  return [self.userID isEqual:other.userID] &&
       [self.profile isEqual:other.profile] &&
-      [self.hostedDomain isEqual:other.hostedDomain];
+      [self.configuration isEqual:other.configuration] &&
+      [self.idToken isEqual:other.idToken] &&
+      [self.refreshToken isEqual:other.refreshToken] &&
+      [self.accessToken isEqual:other.accessToken];
 }
 
 // Not the hash implemention you want to use on prod, but just to match |isEqual:| here.
 - (NSUInteger)hash {
-  return [self.authentication hash] ^ [self.userID hash] ^ [self.serverAuthCode hash] ^
-      [self.profile hash] ^ [self.hostedDomain hash];
+  return [self.userID hash] ^ [self.configuration hash] ^ [self.profile hash] ^
+      [self.idToken hash] ^ [self.refreshToken hash] ^ [self.accessToken hash];
+}
+
+@end
+
+@implementation GIDGoogleUserOldFormat {
+  GIDAuthentication *_authentication;
+  GIDProfileData *_profile;
+}
+
+- (instancetype)initWithAuthState:(OIDAuthState *)authState
+                      profileData:(GIDProfileData *)profileData {
+  self = [super initWithAuthState:authState profileData:profileData];
+  if (self) {
+    _authentication = [[GIDAuthentication alloc] initWithAuthState:authState];
+    _profile = profileData;
+  }
+  return self;
+}
+
+#pragma mark - NSSecureCoding
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+  [encoder encodeObject:_profile forKey:kProfileDataKey];
+  [encoder encodeObject:_authentication forKey:kAuthentication];
 }
 
 @end

+ 499 - 9
GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m

@@ -15,27 +15,80 @@
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
 
 #import <XCTest/XCTest.h>
+#import <TargetConditionals.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/GIDSignIn.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h"
 
-#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
+#import "GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h"
 #import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h"
 #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
 #import "GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenRequest+Testing.h"
 #import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
 
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
+@import GoogleUtilities_MethodSwizzler;
+@import GoogleUtilities_SwizzlerTestHelpers;
+@import GTMAppAuth;
+@import OCMock;
 #else
 #import <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>
+#import <OCMock/OCMock.h>
 #endif
 
+static NSString *const kNewAccessToken = @"new_access_token";
+static NSString *const kNewRefreshToken = @"new_refresh_token";
+
+static NSTimeInterval const kTimeAccuracy = 10;
+static NSTimeInterval const kIDTokenExpiresIn = 100;
+static NSTimeInterval const kNewIDTokenExpiresIn = 200;
+
+static NSString *const kNewScope = @"newScope";
+
 @interface GIDGoogleUserTest : XCTestCase
 @end
 
-@implementation GIDGoogleUserTest
+@implementation GIDGoogleUserTest {
+  // The saved token fetch handler.
+  OIDTokenCallback _tokenFetchHandler;
+}
+
+- (void)setUp {
+  _tokenFetchHandler = nil;
+  
+  // We need to use swizzle here because OCMock can not stub class method with arguments.
+  [GULSwizzler swizzleClass:[OIDAuthorizationService class]
+                    selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
+            isClassSelector:YES
+                  withBlock:^(id sender,
+                              OIDTokenRequest *request,
+                              OIDAuthorizationResponse *authorizationResponse,
+                              OIDTokenCallback callback) {
+    // Save the OIDTokenCallback.
+    self->_tokenFetchHandler = [callback copy];
+  }];
+}
+
+- (void)tearDown {
+  [GULSwizzler unswizzleClass:[OIDAuthorizationService class]
+                     selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
+              isClassSelector:YES];
+}
 
 #pragma mark - Tests
 
@@ -43,15 +96,18 @@
   OIDAuthState *authState = [OIDAuthState testInstance];
   GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState
                                                      profileData:[GIDProfileData testInstance]];
-  GIDAuthentication *authentication =
-      [[GIDAuthentication alloc] initWithAuthState:authState];
-
-  XCTAssertEqualObjects(user.authentication, authentication);
+  
   XCTAssertEqualObjects(user.grantedScopes, @[ OIDAuthorizationRequestTestingScope2 ]);
   XCTAssertEqualObjects(user.userID, kUserID);
-  XCTAssertEqualObjects(user.hostedDomain, kHostedDomain);
-  XCTAssertEqualObjects(user.serverAuthCode, kServerAuthCode);
+  XCTAssertEqualObjects(user.configuration.hostedDomain, kHostedDomain);
+  XCTAssertEqualObjects(user.configuration.clientID, OIDAuthorizationRequestTestingClientID);
   XCTAssertEqualObjects(user.profile, [GIDProfileData testInstance]);
+  XCTAssertEqualObjects(user.accessToken.tokenString, kAccessToken);
+  XCTAssertEqualObjects(user.refreshToken.tokenString, kRefreshToken);
+  
+  OIDIDToken *idToken = [[OIDIDToken alloc]
+      initWithIDTokenString:authState.lastTokenResponse.idToken];
+  XCTAssertEqualObjects(user.idToken.expirationDate, [idToken expiresAt]);
 }
 
 - (void)testCoding {
@@ -83,4 +139,438 @@
 }
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
 
+// Test the old encoding format for backword compatability.
+- (void)testOldFormatCoding {
+  if (@available(iOS 11, macOS 10.13, *)) {
+    OIDAuthState *authState = [OIDAuthState testInstance];
+    GIDProfileData *profileDate = [GIDProfileData testInstance];
+    GIDGoogleUserOldFormat *user = [[GIDGoogleUserOldFormat alloc] initWithAuthState:authState
+                                                                         profileData:profileDate];
+    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:user
+                                         requiringSecureCoding:YES
+                                                         error:nil];
+    GIDGoogleUser *newUser = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDGoogleUser class]
+                                                               fromData:data
+                                                                  error:nil];
+    XCTAssertEqualObjects(user, newUser);
+  }
+}
+
+- (void)testUpdateAuthState {
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn
+                                                idTokenExpiresIn:kIDTokenExpiresIn];
+  
+  NSString *updatedIDToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn];
+  OIDAuthState *updatedAuthState = [OIDAuthState testInstanceWithIDToken:updatedIDToken
+                                                             accessToken:kNewAccessToken
+                                                    accessTokenExpiresIn:kAccessTokenExpiresIn
+                                                            refreshToken:kNewRefreshToken];
+  GIDProfileData *updatedProfileData = [GIDProfileData testInstance];
+  
+  [user updateWithTokenResponse:updatedAuthState.lastTokenResponse
+          authorizationResponse:updatedAuthState.lastAuthorizationResponse
+                    profileData:updatedProfileData];
+  
+  XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken);
+  [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn];
+  
+  XCTAssertEqualObjects(user.idToken.tokenString, updatedIDToken);
+  [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn];
+  
+  XCTAssertEqualObjects(user.refreshToken.tokenString, kNewRefreshToken);
+  
+  XCTAssertEqual(user.profile, updatedProfileData);
+}
+
+// When updating with a new OIDAuthState in which token information is not changed, the token objects
+// should remain the same.
+- (void)testUpdateAuthState_tokensAreNotChanged {
+  NSString *idToken = [self idTokenWithExpiresIn:kIDTokenExpiresIn];
+  OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:idToken
+                                                      accessToken:kAccessToken
+                                             accessTokenExpiresIn:kAccessTokenExpiresIn
+                                                     refreshToken:kRefreshToken];
+  
+  GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState profileData:nil];
+  
+  GIDToken *accessTokenBeforeUpdate = user.accessToken;
+  GIDToken *refreshTokenBeforeUpdate = user.refreshToken;
+  GIDToken *idTokenBeforeUpdate = user.idToken;
+  
+  [user updateWithTokenResponse:authState.lastTokenResponse
+          authorizationResponse:authState.lastAuthorizationResponse
+                    profileData:nil];
+  
+  XCTAssertIdentical(user.accessToken, accessTokenBeforeUpdate);
+  XCTAssertIdentical(user.idToken, idTokenBeforeUpdate);
+  XCTAssertIdentical(user.refreshToken, refreshTokenBeforeUpdate);
+}
+
+- (void)testFetcherAuthorizer {
+  // This is really hard to test without assuming how GTMAppAuthFetcherAuthorization works
+  // internally, so let's just take the shortcut here by asserting we get a
+  // GTMAppAuthFetcherAuthorization object.
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn
+                                                idTokenExpiresIn:kIDTokenExpiresIn];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+  id<GTMFetcherAuthorizationProtocol> fetcherAuthorizer = user.fetcherAuthorizer;
+#pragma clang diagnostic pop
+  XCTAssertTrue([fetcherAuthorizer isKindOfClass:[GTMAppAuthFetcherAuthorization class]]);
+  XCTAssertTrue([fetcherAuthorizer canAuthorize]);
+}
+
+- (void)testFetcherAuthorizer_returnTheSameInstance {
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn
+                                                idTokenExpiresIn:kIDTokenExpiresIn];
+  
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+  id<GTMFetcherAuthorizationProtocol> fetcherAuthorizer = user.fetcherAuthorizer;
+  id<GTMFetcherAuthorizationProtocol> fetcherAuthorizer2 = user.fetcherAuthorizer;
+#pragma clang diagnostic pop
+
+  XCTAssertIdentical(fetcherAuthorizer, fetcherAuthorizer2);
+}
+
+#pragma mark - Test `refreshTokensIfNeededWithCompletion:`
+
+- (void)testRefreshTokensIfNeededWithCompletion_refresh_givenBothTokensExpired {
+  // Both tokens expired 10 seconds ago.
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10];
+  NSString *newIdToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn];
+  
+  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
+  
+  // Save the intermediate states.
+  [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user,
+                                              NSError * _Nullable error) {
+    [expectation fulfill];
+    XCTAssertNil(error);
+    XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken);
+    [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn];
+    XCTAssertEqualObjects(user.idToken.tokenString, newIdToken);
+    [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn];
+  }];
+  
+  // Creates a fake response.
+  OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:newIdToken
+                                                                 accessToken:kNewAccessToken
+                                                                   expiresIn:@(kAccessTokenExpiresIn)
+                                                                refreshToken:kRefreshToken
+                                                                tokenRequest:nil];
+  
+  _tokenFetchHandler(fakeResponse, nil);
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testRefreshTokens_refresh_givenBothTokensExpired_NoNewIDToken {
+  // Both tokens expired 10 seconds ago.
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10];
+  // Creates a fake response without ID token.
+  
+  OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:nil
+                                                                 accessToken:kNewAccessToken
+                                                                   expiresIn:@(kAccessTokenExpiresIn)
+                                                                refreshToken:kRefreshToken
+                                                                tokenRequest:nil];
+  
+  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
+  
+  // Save the intermediate states.
+  [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user,
+                                              NSError * _Nullable error) {
+    [expectation fulfill];
+    XCTAssertNil(error);
+    XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken);
+    [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn];
+    XCTAssertNil(user.idToken);
+  }];
+  
+  
+  _tokenFetchHandler(fakeResponse, nil);
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testRefreshTokensIfNeededWithCompletion_refresh_givenAccessTokenExpired {
+  // Access token expired 10 seconds ago. ID token will expire in 10 minutes.
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:10 * 60];
+  // Creates a fake response.
+  NSString *newIdToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn];
+  OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:newIdToken
+                                                                 accessToken:kNewAccessToken
+                                                                   expiresIn:@(kAccessTokenExpiresIn)
+                                                                refreshToken:kRefreshToken
+                                                                tokenRequest:nil];
+  
+  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
+  
+  // Save the intermediate states.
+  [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user,
+                                              NSError * _Nullable error) {
+    [expectation fulfill];
+    XCTAssertNil(error);
+    XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken);
+    [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn];
+    XCTAssertEqualObjects(user.idToken.tokenString, newIdToken);
+    [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn];
+  }];
+  
+  
+  _tokenFetchHandler(fakeResponse, nil);
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testRefreshTokensIfNeededWithCompletion_refresh_givenIDTokenExpired {
+  // ID token expired 10 seconds ago. Access token will expire in 10 minutes.
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:10 * 60 idTokenExpiresIn:-10];
+  
+  // Creates a fake response.
+  NSString *newIdToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn];
+  OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:newIdToken
+                                                                 accessToken:kNewAccessToken
+                                                                   expiresIn:@(kAccessTokenExpiresIn)
+                                                                refreshToken:kRefreshToken
+                                                                tokenRequest:nil];
+  
+  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
+  
+  // Save the intermediate states.
+  [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user,
+                                              NSError * _Nullable error) {
+    [expectation fulfill];
+    XCTAssertNil(error);
+    XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken);
+    [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn];
+    
+    XCTAssertEqualObjects(user.idToken.tokenString, newIdToken);
+    [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn];
+  }];
+  
+  
+  _tokenFetchHandler(fakeResponse, nil);
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenBothTokensNotExpired {
+  // Both tokens will expire in 10 min.
+  NSTimeInterval expiresIn = 10 * 60;
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:expiresIn
+                                                idTokenExpiresIn:expiresIn];
+  
+  NSString *accessTokenStringBeforeRefresh = user.accessToken.tokenString;
+  NSString *idTokenStringBeforeRefresh = user.idToken.tokenString;
+  
+  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
+  
+  // Save the intermediate states.
+  [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user,
+                                              NSError * _Nullable error) {
+    [expectation fulfill];
+    XCTAssertNil(error);
+  }];
+  
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  
+  XCTAssertEqualObjects(user.accessToken.tokenString, accessTokenStringBeforeRefresh);
+  [self verifyUser:user accessTokenExpiresIn:expiresIn];
+  XCTAssertEqualObjects(user.idToken.tokenString, idTokenStringBeforeRefresh);
+  [self verifyUser:user idTokenExpiresIn:expiresIn];
+}
+
+- (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenRefreshErrors {
+  // Both tokens expired 10 second ago.
+  NSTimeInterval expiresIn = -10;
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:expiresIn
+                                                idTokenExpiresIn:expiresIn];
+  
+  NSString *accessTokenStringBeforeRefresh = user.accessToken.tokenString;
+  NSString *idTokenStringBeforeRefresh = user.idToken.tokenString;
+  
+  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
+  
+  // Save the intermediate states.
+  [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user,
+                                              NSError * _Nullable error) {
+    [expectation fulfill];
+    XCTAssertNotNil(error);
+    XCTAssertNil(user);
+  }];
+  
+  _tokenFetchHandler(nil, [self fakeError]);
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  
+  XCTAssertEqualObjects(user.accessToken.tokenString, accessTokenStringBeforeRefresh);
+  [self verifyUser:user accessTokenExpiresIn:expiresIn];
+  XCTAssertEqualObjects(user.idToken.tokenString, idTokenStringBeforeRefresh);
+  [self verifyUser:user idTokenExpiresIn:expiresIn];
+}
+
+- (void)testRefreshTokensIfNeededWithCompletion_handleConcurrentRefresh {
+  // Both tokens expired 10 second ago.
+  NSTimeInterval expiresIn = -10;
+  GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:expiresIn
+                                                idTokenExpiresIn:expiresIn];
+  // Creates a fake response.
+  NSString *newIdToken = [self idTokenWithExpiresIn:kNewIDTokenExpiresIn];
+  OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:newIdToken
+                                                                 accessToken:kNewAccessToken
+                                                                   expiresIn:@(kAccessTokenExpiresIn)
+                                                                refreshToken:kRefreshToken
+                                                                tokenRequest:nil];
+  
+  XCTestExpectation *firstExpectation =
+      [self expectationWithDescription:@"First callback is called"];
+  [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *user, NSError *error) {
+    [firstExpectation fulfill];
+    XCTAssertNil(error);
+    
+    XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken);
+    [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn];
+    
+    XCTAssertEqualObjects(user.idToken.tokenString, newIdToken);
+    [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn];
+  }];
+  XCTestExpectation *secondExpectation =
+      [self expectationWithDescription:@"Second callback is called"];
+  [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *user, NSError *error) {
+    [secondExpectation fulfill];
+    XCTAssertNil(error);
+    
+    XCTAssertEqualObjects(user.accessToken.tokenString, kNewAccessToken);
+    [self verifyUser:user accessTokenExpiresIn:kAccessTokenExpiresIn];
+    
+    XCTAssertEqualObjects(user.idToken.tokenString, newIdToken);
+    [self verifyUser:user idTokenExpiresIn:kNewIDTokenExpiresIn];
+  }];
+  
+  
+  _tokenFetchHandler(fakeResponse, nil);
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+# pragma mark - Test `addScopes:`
+
+- (void)testAddScopes_success {
+  id signIn = OCMClassMock([GIDSignIn class]);
+  OCMStub([signIn sharedInstance]).andReturn(signIn);
+  [[signIn expect] addScopes:OCMOCK_ANY
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+      presentingViewController:OCMOCK_ANY
+#elif TARGET_OS_OSX
+              presentingWindow:OCMOCK_ANY
+#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                    completion:OCMOCK_ANY];
+  
+  GIDGoogleUser *currentUser = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn
+                                                       idTokenExpiresIn:kIDTokenExpiresIn];
+  
+  OCMStub([signIn currentUser]).andReturn(currentUser);
+  
+  [currentUser addScopes:@[kNewScope]
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+      presentingViewController:[[UIViewController alloc] init]
+#elif TARGET_OS_OSX
+              presentingWindow:[[NSWindow alloc] init]
+#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                    completion:nil];
+  
+  [signIn verify];
+}
+
+- (void)testAddScopes_failure_addScopesToPreviousUser {
+  id signIn = OCMClassMock([GIDSignIn class]);
+  OCMStub([signIn sharedInstance]).andReturn(signIn);
+
+  GIDGoogleUser *currentUser = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn
+                                                       idTokenExpiresIn:kIDTokenExpiresIn];
+  
+  OCMStub([signIn currentUser]).andReturn(currentUser);
+  
+  GIDGoogleUser *previousUser = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn
+                                                        idTokenExpiresIn:kNewIDTokenExpiresIn];
+  
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:@"Completion is called."];
+  
+  [previousUser addScopes:@[kNewScope]
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+      presentingViewController:[[UIViewController alloc] init]
+#elif TARGET_OS_OSX
+              presentingWindow:[[NSWindow alloc] init]
+#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                    completion:^(GIDUserAuth *userAuth, NSError *error) {
+    [expectation fulfill];
+    XCTAssertNil(userAuth);
+    XCTAssertEqual(error.code, kGIDSignInErrorCodeMismatchWithCurrentUser);
+  }];
+  
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testAddScopes_failure_addScopesToPreviousUser_currentUserIsNull {
+  id signIn = OCMClassMock([GIDSignIn class]);
+  OCMStub([signIn sharedInstance]).andReturn(signIn);
+
+  GIDGoogleUser *currentUser = nil;
+  OCMStub([signIn currentUser]).andReturn(currentUser);
+  
+  GIDGoogleUser *previousUser = [self googleUserWithAccessTokenExpiresIn:kAccessTokenExpiresIn
+                                                        idTokenExpiresIn:kNewIDTokenExpiresIn];
+  
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:@"Completion is called."];
+  
+  [previousUser addScopes:@[kNewScope]
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+      presentingViewController:[[UIViewController alloc] init]
+#elif TARGET_OS_OSX
+              presentingWindow:[[NSWindow alloc] init]
+#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                    completion:^(GIDUserAuth *userAuth, NSError *error) {
+    [expectation fulfill];
+    XCTAssertNil(userAuth);
+    XCTAssertEqual(error.code, kGIDSignInErrorCodeMismatchWithCurrentUser);
+  }];
+  
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+#pragma mark - Helpers
+
+// Returns a GIDGoogleUser with different tokens expiresIn time. The token strings are constants.
+- (GIDGoogleUser *)googleUserWithAccessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn
+                                     idTokenExpiresIn:(NSTimeInterval)idTokenExpiresIn {
+  NSString *idToken = [self idTokenWithExpiresIn:idTokenExpiresIn];
+  OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:idToken
+                                                      accessToken:kAccessToken
+                                             accessTokenExpiresIn:accessTokenExpiresIn
+                                                     refreshToken:kRefreshToken];
+  
+  return [[GIDGoogleUser alloc] initWithAuthState:authState profileData:nil];
+}
+
+- (NSString *)idTokenWithExpiresIn:(NSTimeInterval)expiresIn {
+  // The expireTime should be based on 1970.
+  NSTimeInterval expireTime = [[NSDate date] timeIntervalSince1970] + expiresIn;
+  return [OIDTokenResponse idTokenWithSub:kUserID exp:@(expireTime)];
+}
+
+- (void)verifyUser:(GIDGoogleUser *)user accessTokenExpiresIn:(NSTimeInterval)expiresIn {
+  NSDate *expectedAccessTokenExpirationDate = [[NSDate date] dateByAddingTimeInterval:expiresIn];
+  XCTAssertEqualWithAccuracy([user.accessToken.expirationDate timeIntervalSince1970],
+                             [expectedAccessTokenExpirationDate timeIntervalSince1970],
+                             kTimeAccuracy);
+}
+
+- (void)verifyUser:(GIDGoogleUser *)user idTokenExpiresIn:(NSTimeInterval)expiresIn {
+  NSDate *expectedIDTokenExpirationDate = [[NSDate date] dateByAddingTimeInterval:expiresIn];
+  XCTAssertEqualWithAccuracy([user.idToken.expirationDate timeIntervalSince1970],
+                             [expectedIDTokenExpirationDate timeIntervalSince1970], kTimeAccuracy);
+}
+
+- (NSError *)fakeError {
+  return [NSError errorWithDomain:@"fake.domain" code:-1 userInfo:nil];
+}
+
 @end

+ 6 - 5
GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m

@@ -37,8 +37,9 @@
   id presentingWindow = OCMStrictClassMock([NSWindow class]);
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
   NSString *loginHint = @"login_hint";
-  GIDSignInCompletion completion = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {};
-  
+
+  void (^completion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error) =
+      ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {};
   GIDSignInInternalOptions *options =
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
@@ -63,9 +64,9 @@
 }
 
 - (void)testSilentOptions {
-  GIDSignInCompletion completion = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {};
-  GIDSignInInternalOptions *options = [GIDSignInInternalOptions
-                                       silentOptionsWithCompletion:completion];
+  void (^completion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error) =
+      ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {};
+  GIDSignInInternalOptions *options = [GIDSignInInternalOptions silentOptionsWithCompletion:completion];
   XCTAssertFalse(options.interactive);
   XCTAssertFalse(options.continuation);
   XCTAssertNil(options.extraParams);

+ 70 - 44
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -26,10 +26,10 @@
 // Test module imports
 @import GoogleSignIn;
 
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
 #import "GoogleSignIn/Sources/GIDSignIn_Private.h"
 #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
-#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
@@ -204,9 +204,6 @@ static NSString *const kNewScope = @"newScope";
   // Mock for |GIDGoogleUser|.
   id _user;
 
-  // Mock for |GIDAuthentication|.
-  id _authentication;
-
   // Mock for |OIDAuthorizationService|
   id _oidAuthorizationService;
 
@@ -238,7 +235,7 @@ static NSString *const kNewScope = @"newScope";
   NSString *_hint;
 
   // The completion to be used when testing |GIDSignIn|.
-  GIDSignInCompletion _completion;
+  GIDUserAuthCompletion _completion;
 
   // The saved authorization request.
   OIDAuthorizationRequest *_savedAuthorizationRequest;
@@ -310,7 +307,6 @@ static NSString *const kNewScope = @"newScope";
         self->_keychainRemoved = YES;
       });
   _user = OCMStrictClassMock([GIDGoogleUser class]);
-  _authentication = OCMStrictClassMock([GIDAuthentication class]);
   _oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]);
   OCMStub([_oidAuthorizationService
       presentAuthorizationRequest:SAVE_TO_ARG_BLOCK(self->_savedAuthorizationRequest)
@@ -338,10 +334,10 @@ static NSString *const kNewScope = @"newScope";
   _hint = nil;
 
   __weak GIDSignInTest *weakSelf = self;
-  _completion = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {
+  _completion = ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {
     GIDSignInTest *strongSelf = weakSelf;
-    if (!user) {
-      XCTAssertNotNil(error, @"should have an error if user is nil");
+    if (!userAuth) {
+      XCTAssertNotNil(error, @"should have an error if the userAuth is nil");
     }
     XCTAssertFalse(strongSelf->_completionCalled, @"callback already called");
     strongSelf->_completionCalled = YES;
@@ -355,7 +351,6 @@ static NSString *const kNewScope = @"newScope";
   OCMVerifyAll(_tokenRequest);
   OCMVerifyAll(_authorization);
   OCMVerifyAll(_user);
-  OCMVerifyAll(_authentication);
   OCMVerifyAll(_oidAuthorizationService);
 
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
@@ -418,24 +413,36 @@ static NSString *const kNewScope = @"newScope";
 }
 
 - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser {
-  [[[_authorization expect] andReturn:_authState] authState];
+  [[[_authorization stub] andReturn:_authState] authState];
+  [[_authorization expect] setTokenRefreshDelegate:OCMOCK_ANY];
   OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse);
-  OCMStub([_tokenResponse scope]).andReturn(nil);
-  OCMStub([_tokenResponse additionalParameters]).andReturn(nil);
-  OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken);
-  OCMStub([_tokenResponse request]).andReturn(_tokenRequest);
-  OCMStub([_tokenRequest additionalParameters]).andReturn(nil);
+  OCMStub([_authState refreshToken]).andReturn(kRefreshToken);
+  [[_authState expect] setStateChangeDelegate:OCMOCK_ANY];
 
   id idTokenDecoded = OCMClassMock([OIDIDToken class]);
   OCMStub([idTokenDecoded alloc]).andReturn(idTokenDecoded);
   OCMStub([idTokenDecoded initWithIDTokenString:OCMOCK_ANY]).andReturn(idTokenDecoded);
   OCMStub([idTokenDecoded subject]).andReturn(kFakeGaiaID);
-
+  
+  // Mock generating a GIDConfiguration when initializing GIDGoogleUser.
+  OIDAuthorizationResponse *authResponse =
+      [OIDAuthorizationResponse testInstanceWithAdditionalParameters:nil
+                                                         errorString:nil];
+  
+  OCMStub([_authState lastAuthorizationResponse]).andReturn(authResponse);
+  OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken);
+  OCMStub([_tokenResponse request]).andReturn(_tokenRequest);
+  OCMStub([_tokenRequest additionalParameters]).andReturn(nil);
+  OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken);
+  OCMStub([_tokenResponse accessTokenExpirationDate]).andReturn(nil);
+  
   [_signIn restorePreviousSignInNoRefresh];
 
   [_authorization verify];
   [_authState verify];
   [_tokenResponse verify];
+  [_tokenRequest verify];
+  [idTokenDecoded verify];
   XCTAssertEqual(_signIn.currentUser.userID, kFakeGaiaID);
 
   [idTokenDecoded stopMocking];
@@ -479,7 +486,7 @@ static NSString *const kNewScope = @"newScope";
 
   XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called."];
 
-  [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser * _Nullable user,
+  [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser *_Nullable user,
                                                  NSError * _Nullable error) {
     [expectation fulfill];
     XCTAssertNotNil(error, @"error should not have been nil");
@@ -590,13 +597,13 @@ static NSString *const kNewScope = @"newScope";
 
   id profile = OCMStrictClassMock([GIDProfileData class]);
   OCMStub([profile email]).andReturn(kUserEmail);
-
-  OCMStub([_user authentication]).andReturn(_authentication);
-  OCMStub([_authentication clientID]).andReturn(kClientId);
-  OCMStub([_user serverClientID]).andReturn(nil);
-  OCMStub([_user hostedDomain]).andReturn(nil);
-
-  OCMStub([_user openIDRealm]).andReturn(kOpenIDRealm);
+  
+  // Mock for the method `addScopes`.
+  GIDConfiguration *configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
+                                                                serverClientID:nil
+                                                                  hostedDomain:nil
+                                                                   openIDRealm:kOpenIDRealm];
+  OCMStub([_user configuration]).andReturn(configuration);
   OCMStub([_user profile]).andReturn(profile);
   OCMStub([_user grantedScopes]).andReturn(@[kGrantedScope]);
 
@@ -626,6 +633,8 @@ static NSString *const kNewScope = @"newScope";
   NSArray<NSString *> *expectedScopes = @[kNewScope, kGrantedScope];
   XCTAssertEqualObjects(grantedScopes, expectedScopes);
 
+  [_user verify];
+  [profile verify];
   [profile stopMocking];
 }
 
@@ -1075,9 +1084,9 @@ static NSString *const kNewScope = @"newScope";
   NSError *emmError = [NSError errorWithDomain:@"anydomain"
                                           code:12345
                                       userInfo:@{ OIDOAuthErrorFieldError : errorJSON }];
-  [[_authentication expect] handleTokenFetchEMMError:emmError
-                                          completion:SAVE_TO_ARG_BLOCK(completion)];
-
+  id emmSupport = OCMStrictClassMock([GIDEMMSupport class]);
+  [[emmSupport expect] handleTokenFetchEMMError:emmError
+                                     completion:SAVE_TO_ARG_BLOCK(completion)];
 
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
@@ -1091,11 +1100,12 @@ static NSString *const kNewScope = @"newScope";
   NSError *handledError = [NSError errorWithDomain:kGIDSignInErrorDomain
                                               code:kGIDSignInErrorCodeEMM
                                           userInfo:emmError.userInfo];
+  
   completion(handledError);
 
   [self waitForExpectationsWithTimeout:1 handler:nil];
 
-  [_authentication verify];
+  [emmSupport verify];
   XCTAssertFalse(_keychainSaved, @"should not save to keychain");
   XCTAssertTrue(_completionCalled, @"should call delegate");
   XCTAssertNotNil(_authError, @"should have error");
@@ -1208,6 +1218,7 @@ static NSString *const kNewScope = @"newScope";
       [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken]
                                     accessToken:restoredSignIn ? kAccessToken : nil
                                       expiresIn:oldAccessToken ? @(300) : nil
+                                   refreshToken:kRefreshToken
                                    tokenRequest:nil];
 
   OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc]
@@ -1238,10 +1249,13 @@ static NSString *const kNewScope = @"newScope";
     }
   } else {
     XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called"];
-    GIDSignInCompletion completion = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {
+    GIDUserAuthCompletion completion =
+        ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {
       [expectation fulfill];
-      if (!user) {
-        XCTAssertNotNil(error, @"should have an error if user is nil");
+      if (userAuth) {
+        XCTAssertEqualObjects(userAuth.serverAuthCode, kServerAuthCode);
+      } else {
+        XCTAssertNotNil(error, @"Should have an error if the userAuth is nil");
       }
       XCTAssertFalse(self->_completionCalled, @"callback already called");
       self->_completionCalled = YES;
@@ -1350,20 +1364,30 @@ static NSString *const kNewScope = @"newScope";
 
   // SaveAuthCallback
   __block OIDAuthState *authState;
+  __block OIDTokenResponse *updatedTokenResponse;
+  __block OIDAuthorizationResponse *updatedAuthorizationResponse;
   __block GIDProfileData *profileData;
 
   if (keychainError) {
     _saveAuthorizationReturnValue = NO;
   } else {
     if (addScopesFlow) {
-      [[_user expect] updateAuthState:SAVE_TO_ARG_BLOCK(authState)
-                          profileData:SAVE_TO_ARG_BLOCK(profileData)];
+      [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];
+      [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
+      [[_user expect] updateWithTokenResponse:SAVE_TO_ARG_BLOCK(updatedTokenResponse)
+                        authorizationResponse:SAVE_TO_ARG_BLOCK(updatedAuthorizationResponse)
+                                  profileData:SAVE_TO_ARG_BLOCK(profileData)];
     } else {
       [[[_user stub] andReturn:_user] alloc];
       (void)[[[_user expect] andReturn:_user] initWithAuthState:SAVE_TO_ARG_BLOCK(authState)
                                                     profileData:SAVE_TO_ARG_BLOCK(profileData)];
     }
   }
+  
+  // CompletionCallback - mock server auth code parsing
+  if (!keychainError) {
+    [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
+  }
 
   if (restoredSignIn && !oldAccessToken) {
     XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"];
@@ -1377,13 +1401,20 @@ static NSString *const kNewScope = @"newScope";
     _savedTokenCallback(tokenResponse, nil);
   }
 
-  [_authState verify];
   if (keychainError) {
     return;
   }
   [self waitForExpectationsWithTimeout:1 handler:nil];
+  
+  [_authState verify];
+  
   XCTAssertTrue(_keychainSaved, @"should save to keychain");
-  XCTAssertNotNil(authState);
+  if (addScopesFlow) {
+    XCTAssertNotNil(updatedTokenResponse);
+    XCTAssertNotNil(updatedAuthorizationResponse);
+  } else {
+    XCTAssertNotNil(authState);
+  }
   // Check fat ID token decoding
   XCTAssertEqualObjects(profileData.name, kFatName);
   XCTAssertEqualObjects(profileData.givenName, kFatGivenName);
@@ -1396,13 +1427,8 @@ static NSString *const kNewScope = @"newScope";
   _keychainSaved = NO;
   _authError = nil;
 
-  if (!addScopesFlow) {
-    [[[_user expect] andReturn:_authentication] authentication];
-    [[[_user expect] andReturn:_authentication] authentication];
-  }
-
-  __block GIDAuthenticationCompletion completion;
-  [[_authentication expect] doWithFreshTokens:SAVE_TO_ARG_BLOCK(completion)];
+  __block GIDGoogleUserCompletion completion;
+  [[_user expect] refreshTokensIfNeededWithCompletion:SAVE_TO_ARG_BLOCK(completion)];
 
   XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"];
 
@@ -1412,7 +1438,7 @@ static NSString *const kNewScope = @"newScope";
     XCTAssertNil(error, @"should have no error");
   }];
 
-  completion(_authentication, nil);
+  completion(_user, nil);
 
   [self waitForExpectationsWithTimeout:1 handler:nil];
   XCTAssertFalse(_keychainRemoved, @"should not remove keychain");

+ 94 - 0
GoogleSignIn/Tests/Unit/GIDTokenTest.m

@@ -0,0 +1,94 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h"
+
+#import "GoogleSignIn/Sources/GIDToken_Private.h"
+
+static NSString * const tokenString = @"tokenString";
+static NSString * const tokenString2 = @"tokenString2";
+
+@interface GIDTokenTest : XCTestCase {
+  NSDate *_date;
+}
+@end
+
+@implementation GIDTokenTest
+
+- (void)setUp {
+  [super setUp];
+  _date = [[NSDate alloc]initWithTimeIntervalSince1970:1000];
+}
+
+- (void)testInitializer {
+  GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+  XCTAssertEqualObjects(token.tokenString, tokenString);
+  XCTAssertEqualObjects(token.expirationDate, _date);
+}
+
+- (void)testTokensWithSameTokenStringAndExpirationDateAreEqual {
+  GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+  GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+  XCTAssertEqualObjects(token, token2);
+}
+
+- (void)testEqualTokensHaveTheSameHash {
+  GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+  GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+  XCTAssertEqualObjects(token, token2);
+  XCTAssertEqual(token.hash, token2.hash);
+}
+
+- (void)testTokensWithDifferentTokenStringsAreNotEqual {
+  GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+  GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString2 expirationDate:_date];
+  XCTAssertNotEqualObjects(token, token2);
+}
+
+- (void)testTokensWithSameTokenStringAndNoExpirationDateAreEqual {
+  GIDToken *refreshToken = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil];
+  GIDToken *refreshToken2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil];
+  XCTAssertEqualObjects(refreshToken, refreshToken2);
+}
+
+- (void)testTokensWithSameTokenStringAndDifferentExpirationDateAreNotEqual {
+  GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+  NSDate *date2 = [[NSDate alloc]initWithTimeIntervalSince1970:2000];
+  GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:date2];
+  XCTAssertNotEqualObjects(token, token2);
+}
+
+- (void)testTokensWithSameTokenStringAndOneHasNoExpirationDateAreNotEqual {
+  GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+  GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil];
+  XCTAssertNotEqualObjects(token, token2);
+}
+  
+- (void)testCoding {
+  if (@available(iOS 11, macOS 10.13, *)) {
+    GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date];
+    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:token requiringSecureCoding:YES error:nil];
+    GIDToken *newToken = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDToken class]
+                                                           fromData:data
+                                                              error:nil];
+    XCTAssertEqualObjects(token, newToken);
+    
+    XCTAssertTrue([GIDToken supportsSecureCoding]);
+  } else {
+    XCTSkip(@"Required API is not available for this test.");
+  }
+}
+  
+@end

+ 11 - 0
GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h

@@ -28,4 +28,15 @@
 
 + (instancetype)testInstanceWithTokenResponse:(OIDTokenResponse *)tokenResponse;
 
+/**
+ * @idToken The ID token.
+ * @accessToken The access token string.
+ * @accessTokenExipresIn The life time of the access token starting from the moment when `OIDTokenResponse` is created.
+ * @refreshToken The refresh token string.
+ */
++ (instancetype)testInstanceWithIDToken:(NSString *)idToken
+                            accessToken:(NSString *)accessToken
+                   accessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn
+                           refreshToken:(NSString *)refreshToken;
+
 @end

+ 13 - 0
GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m

@@ -33,4 +33,17 @@
                                                tokenResponse:tokenResponse];
 }
 
++ (instancetype)testInstanceWithIDToken:(NSString *)idToken
+                            accessToken:(NSString *)accessToken
+                   accessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn
+                           refreshToken:(NSString *)refreshToken {
+  OIDTokenResponse *newResponse =
+      [OIDTokenResponse testInstanceWithIDToken:idToken
+                                    accessToken:accessToken
+                                      expiresIn:@(accessTokenExpiresIn)
+                                   refreshToken:refreshToken
+                                   tokenRequest:nil];
+  return [self testInstanceWithTokenResponse:newResponse];
+}
+
 @end

+ 5 - 0
GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h

@@ -56,12 +56,17 @@ extern NSString * const kFatPictureURL;
 + (instancetype)testInstanceWithIDToken:(NSString *)idToken
                             accessToken:(NSString *)accessToken
                               expiresIn:(NSNumber *)expiresIn
+                           refreshToken:(NSString *)refreshToken
                            tokenRequest:(OIDTokenRequest *)tokenRequest;
 
 + (NSString *)idToken;
 
 + (NSString *)fatIDToken;
 
+/**
+ * @sub The subject of the ID token.
+ * @exp The interval between 00:00:00 UTC on 1 January 1970 and the expiration date of the ID token.
+ */
 + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp;
 
 + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat;

+ 4 - 2
GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m

@@ -61,19 +61,21 @@ NSString * const kFatPictureURL = @"fake_user_picture_url";
   return [OIDTokenResponse testInstanceWithIDToken:idToken
                                        accessToken:nil
                                          expiresIn:nil
+                                      refreshToken:nil
                                       tokenRequest:nil];
 }
 
 + (instancetype)testInstanceWithIDToken:(NSString *)idToken
                             accessToken:(NSString *)accessToken
                               expiresIn:(NSNumber *)expiresIn
+                           refreshToken:(NSString *)refreshToken
                            tokenRequest:(OIDTokenRequest *)tokenRequest {
   NSMutableDictionary<NSString *, NSString *> *parameters;
   parameters = [[NSMutableDictionary alloc] initWithDictionary:@{
     @"access_token" : accessToken ?: kAccessToken,
     @"expires_in" : expiresIn ?: @(kAccessTokenExpiresIn),
     @"token_type" : @"example_token_type",
-    @"refresh_token" : kRefreshToken,
+    @"refresh_token" : refreshToken ?: kRefreshToken,
     @"scope" : [OIDScopeUtilities scopesWithArray:@[ OIDAuthorizationRequestTestingScope2 ]],
     @"server_code" : kServerAuthCode,
   }];
@@ -112,7 +114,7 @@ NSString * const kFatPictureURL = @"fake_user_picture_url";
   NSMutableDictionary<NSString *, NSString *> *payloadContents =
       [NSMutableDictionary dictionaryWithDictionary:@{
     @"sub" : sub,
-    @"hd" : kHostedDomain,
+    @"hd"  : kHostedDomain,
     @"iss" : kIssuer,
     @"aud" : kAudience,
     @"exp" : exp,

+ 4 - 6
Samples/ObjC/SignInSample/Source/AuthInspectorViewController.m

@@ -36,13 +36,12 @@ static CGFloat const kTableViewCellPadding = 22.f;
   self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
   if (self) {
     _keyPaths = @[
-      @"authentication.accessToken",
-      @"authentication.accessTokenExpirationDate",
-      @"authentication.refreshToken",
-      @"authentication.idToken",
+      @"accessToken.tokenString",
+      @"accessToken.expirationDate",
+      @"refreshToken.tokenString",
+      @"idToken.tokenString",
       @"grantedScopes",
       @"userID",
-      @"serverAuthCode",
       @"profile.email",
       @"profile.name",
     ];
@@ -140,4 +139,3 @@ static CGFloat const kTableViewCellPadding = 22.f;
 }
 
 @end
-

+ 8 - 6
Samples/ObjC/SignInSample/Source/SignInViewController.m

@@ -175,7 +175,7 @@ static NSString *const kCredentialsButtonAccessibilityIdentifier = @"Credentials
 
 - (void)reportAuthStatus {
   GIDGoogleUser *googleUser = [GIDSignIn.sharedInstance currentUser];
-  if (googleUser.authentication) {
+  if (googleUser) {
     _signInAuthStatus.text = @"Status: Authenticated";
   } else {
     // To authenticate, use Google Sign-In button.
@@ -188,7 +188,7 @@ static NSString *const kCredentialsButtonAccessibilityIdentifier = @"Credentials
 // Update the interface elements containing user data to reflect the
 // currently signed in user.
 - (void)refreshUserInfo {
-  if (GIDSignIn.sharedInstance.currentUser.authentication == nil) {
+  if (!GIDSignIn.sharedInstance.currentUser) {
     self.userName.text = kPlaceholderUserName;
     self.userEmailAddress.text = kPlaceholderEmailAddress;
     self.userAvatar.image = [UIImage imageNamed:kPlaceholderAvatarImageName];
@@ -248,7 +248,7 @@ static NSString *const kCredentialsButtonAccessibilityIdentifier = @"Credentials
 
 - (IBAction)signIn:(id)sender {
   [GIDSignIn.sharedInstance signInWithPresentingViewController:self
-                                                    completion:^(GIDGoogleUser *user,
+                                                    completion:^(GIDUserAuth *userAuth,
                                                                  NSError *error) {
     if (error) {
       self->_signInAuthStatus.text =
@@ -280,9 +280,11 @@ static NSString *const kCredentialsButtonAccessibilityIdentifier = @"Credentials
 }
 
 - (IBAction)addScopes:(id)sender {
-  [GIDSignIn.sharedInstance addScopes:@[ @"https://www.googleapis.com/auth/user.birthday.read" ]
-             presentingViewController:self
-                           completion:^(GIDGoogleUser *user, NSError *error) {
+  GIDGoogleUser *currentUser = GIDSignIn.sharedInstance.currentUser;
+  [currentUser addScopes:@[ @"https://www.googleapis.com/auth/user.birthday.read" ]
+      presentingViewController:self
+                    completion:^(GIDUserAuth *_Nullable userAuth,
+                                 NSError *_Nullable error) {
     if (error) {
       self->_signInAuthStatus.text = [NSString stringWithFormat:@"Status: Failed to add scopes: %@",
                                       error];

+ 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?.refreshTokensIfNeeded { user, error in
+      guard let token = user?.accessToken.tokenString else {
         completion(.failure(.couldNotCreateURLSession(error)))
         return
       }

+ 18 - 14
Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift

@@ -36,12 +36,12 @@ final class GoogleSignInAuthenticator: ObservableObject {
       return
     }
 
-    GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { user, error in
-      guard let user = user else {
+    GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { userAuth, error in
+      guard let userAuth = userAuth else {
         print("Error! \(String(describing: error))")
         return
       }
-      self.authViewModel.state = .signedIn(user)
+      self.authViewModel.state = .signedIn(userAuth.user)
     }
 
 #elseif os(macOS)
@@ -50,12 +50,12 @@ final class GoogleSignInAuthenticator: ObservableObject {
       return
     }
 
-    GIDSignIn.sharedInstance.signIn(withPresenting: presentingWindow) { user, error in
-      guard let user = user else {
+    GIDSignIn.sharedInstance.signIn(withPresenting: presentingWindow) { userAuth, error in
+      guard let userAuth = userAuth else {
         print("Error! \(String(describing: error))")
         return
       }
-      self.authViewModel.state = .signedIn(user)
+      self.authViewModel.state = .signedIn(userAuth.user)
     }
 #endif
   }
@@ -83,20 +83,24 @@ final class GoogleSignInAuthenticator: ObservableObject {
   /// - note: Successful requests will update the `authViewModel.state` with a new current user that
   /// has the granted scope.
   func addBirthdayReadScope(completion: @escaping () -> Void) {
+    guard let currentUser = GIDSignIn.sharedInstance.currentUser else {
+      fatalError("No user signed in!")
+    }
+    
     #if os(iOS)
     guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
       fatalError("No root view controller!")
     }
 
-    GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope],
-                                       presenting: rootViewController) { user, error in
+    currentUser.addScopes([BirthdayLoader.birthdayReadScope],
+                          presenting: rootViewController) { userAuth, error in
       if let error = error {
         print("Found error while adding birthday read scope: \(error).")
         return
       }
 
-      guard let currentUser = user else { return }
-      self.authViewModel.state = .signedIn(currentUser)
+      guard let userAuth = userAuth else { return }
+      self.authViewModel.state = .signedIn(userAuth.user)
       completion()
     }
 
@@ -105,15 +109,15 @@ final class GoogleSignInAuthenticator: ObservableObject {
       fatalError("No presenting window!")
     }
 
-    GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope],
-                                       presenting: presentingWindow) { user, error in
+    currentUser.addScopes([BirthdayLoader.birthdayReadScope],
+                          presenting: presentingWindow) { userAuth, error in
       if let error = error {
         print("Found error while adding birthday read scope: \(error).")
         return
       }
 
-      guard let currentUser = user else { return }
-      self.authViewModel.state = .signedIn(currentUser)
+      guard let userAuth = userAuth else { return }
+      self.authViewModel.state = .signedIn(userAuth.user)
       completion()
     }