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

Merge branch 'refactorGSI' into pin-GIDProfileDataFetcher-fake

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

+ 58 - 0
GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 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 GIDSignInInternalOptions;
+@class OIDAuthorizationResponse;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// The protocol to control the authorization flow.
+@protocol GIDAuthorizationFlowProcessor <NSObject>
+
+/// The state of the authorization flow.
+@property(nonatomic, readonly, getter=isStarted) BOOL start;
+
+/// Starts the authorization flow.
+///
+/// This method sends authorization request to AppAuth `OIDAuthorizationService` and gets back the
+/// response or an error.
+///
+/// @param options The `GIDSignInInternalOptions` object to provide serverClientID, hostedDomain,
+///     clientID, scopes, loginHint and extraParams.
+/// @param emmSupport The EMM support info string.
+/// @param completion The block that is called on completion asynchronously.
+///      authorizationResponse The response from `OIDAuthorizationService`.
+///      error The error from `OIDAuthorizationService`.
+- (void)startWithOptions:(GIDSignInInternalOptions *)options
+              emmSupport:(nullable NSString *)emmSupport
+              completion:(void (^)(OIDAuthorizationResponse *_Nullable authorizationResponse,
+                                   NSError *_Nullable error))completion;
+
+/// Handles the custom URL scheme opened by SFSafariViewController and returns control to the
+/// client on iOS 10.
+///
+/// @param url The redirect URL invoked by the server.
+/// @return YES if the passed URL matches the expected redirect URL and was consumed, NO otherwise.
+- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)url;
+
+/// Cancels the authorization flow.
+- (void)cancelAuthenticationFlow;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 31 - 0
GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 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>
+
+#import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h"
+
+@class OIDServiceConfiguration;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Concrete implementation of the protocol `GIDAuthorizationFlowProcessor`.
+@interface GIDAuthorizationFlowProcessor : NSObject <GIDAuthorizationFlowProcessor>
+
+@end
+
+NS_ASSUME_NONNULL_END
+

+ 134 - 0
GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.m

@@ -0,0 +1,134 @@
+/*
+ * Copyright 2023 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/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
+#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Parameters for the auth and token exchange endpoints.
+static NSString *const kAudienceParameter = @"audience";
+
+static NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes";
+static NSString *const kLoginHintParameter = @"login_hint";
+static NSString *const kHostedDomainParameter = @"hd";
+
+@interface GIDAuthorizationFlowProcessor ()
+
+/// AppAuth external user-agent session state.
+@property(nonatomic, nullable)id<OIDExternalUserAgentSession> currentAuthorizationFlow;
+
+/// AppAuth configuration object.
+@property(nonatomic)OIDServiceConfiguration *appAuthConfiguration;
+
+@end
+
+@implementation GIDAuthorizationFlowProcessor
+
+# pragma mark - Public API
+
+- (BOOL)isStarted {
+  return self.currentAuthorizationFlow != nil;
+}
+
+- (void)startWithOptions:(GIDSignInInternalOptions *)options
+              emmSupport:(nullable NSString *)emmSupport
+              completion:(void (^)(OIDAuthorizationResponse *_Nullable authorizationResponse,
+                                   NSError *_Nullable error))completion {
+  GIDSignInCallbackSchemes *schemes =
+      [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
+  NSString *urlString = [NSString stringWithFormat:@"%@:%@",
+      [schemes clientIdentifierScheme], kBrowserCallbackPath];
+  NSURL *redirectURL = [NSURL URLWithString:urlString];
+
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
+  additionalParameters[kIncludeGrantedScopesParameter] = @"true";
+  if (options.configuration.serverClientID) {
+    additionalParameters[kAudienceParameter] = options.configuration.serverClientID;
+  }
+  if (options.loginHint) {
+    additionalParameters[kLoginHintParameter] = options.loginHint;
+  }
+  if (options.configuration.hostedDomain) {
+    additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain;
+  }
+  
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  [additionalParameters addEntriesFromDictionary:
+      [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
+  additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
+  additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
+  
+  NSURL *authorizationEndpointURL = [GIDSignInPreferences authorizationEndpointURL];
+  NSURL *tokenEndpointURL = [GIDSignInPreferences tokenEndpointURL];
+  OIDServiceConfiguration *appAuthConfiguration =
+      [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpointURL
+                                                       tokenEndpoint:tokenEndpointURL];
+  OIDAuthorizationRequest *request =
+      [[OIDAuthorizationRequest alloc] initWithConfiguration:appAuthConfiguration
+                                                    clientId:options.configuration.clientID
+                                                      scopes:options.scopes
+                                                 redirectURL:redirectURL
+                                                responseType:OIDResponseTypeCode
+                                        additionalParameters:additionalParameters];
+  
+  _currentAuthorizationFlow = [OIDAuthorizationService
+      presentAuthorizationRequest:request
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+         presentingViewController:options.presentingViewController
+#elif TARGET_OS_OSX
+                 presentingWindow:options.presentingWindow
+#endif // TARGET_OS_OSX
+                         callback:^(OIDAuthorizationResponse *authorizationResponse,
+                                    NSError *error) {
+    completion(authorizationResponse, error);
+  }];
+}
+
+- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)url {
+  if ([self.currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) {
+    self.currentAuthorizationFlow = nil;
+    return YES;
+  } else {
+    return NO;
+  }
+}
+
+- (void)cancelAuthenticationFlow {
+  [self.currentAuthorizationFlow cancel];
+  self.currentAuthorizationFlow = nil;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 2 - 1
GoogleSignIn/Sources/GIDProfileData.m

@@ -128,7 +128,8 @@ static NSString *const kOldImageURLStringKey = @"picture";
   }
   
   return [self initWithEmail:email
-                        name:name givenName:givenName
+                        name:name
+                   givenName:givenName
                   familyName:familyName
                     imageURL:imageURL];
 }

+ 7 - 4
GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h

@@ -26,8 +26,8 @@ NS_ASSUME_NONNULL_BEGIN
 
 /// Fetches the latest `GIDProfileData` object.
 ///
-/// "This method either extracts profile data from `OIDIDToken` in `OIDAuthState` or fetches it
-/// from the UserInfo endpoint."
+/// This method either extracts profile data from `OIDIDToken` in `OIDAuthState` or fetches it
+/// from the UserInfo endpoint.
 ///
 /// @param authState The state of the current OAuth session.
 /// @param completion The block that is called on completion asynchronously.
@@ -35,10 +35,13 @@ NS_ASSUME_NONNULL_BEGIN
                            completion:(void (^)(GIDProfileData *_Nullable profileData,
                                                 NSError *_Nullable error))completion;
 
-/// Fetches the latest `GIDProfileData` object.
+/// Fetches a `GIDProfileData` object from an ID token.
+///
+/// This method returns  a `GIDProfileData` object if the ID token is a fat one. Otherwise, returns
+/// nil.
 ///
 /// @param idToken The ID token.
-- (nullable GIDProfileData*)fetchProfileDataWithIDToken:(OIDIDToken *)idToken;
+- (nullable GIDProfileData *)fetchProfileDataWithIDToken:(OIDIDToken *)idToken;
 
 @end
 

+ 5 - 3
GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m

@@ -63,11 +63,13 @@ static NSString *const kBasicProfileFamilyNameKey = @"family_name";
                                                 NSError *_Nullable error))completion {
   OIDIDToken *idToken =
       [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken];
-  // If profile data is present in the ID token, use it.
   if (idToken) {
+    // If we have an ID token, try to extract profile data from it.
     GIDProfileData *profileData = [self fetchProfileDataWithIDToken:idToken];
-    completion(profileData, nil);
-    return;
+    if (profileData) {
+      completion(profileData, nil);
+      return;
+    }
   }
   
   // If we can't retrieve profile data from the ID token, make a UserInfo endpoint request to

+ 32 - 80
GoogleSignIn/Sources/GIDSignIn.m

@@ -21,6 +21,8 @@
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h"
 
+#import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h"
+#import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h"
 #import "GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHTTPFetcher.h"
 #import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h"
 #import "GoogleSignIn/Sources/GIDEMMSupport.h"
@@ -79,9 +81,6 @@ static NSString *const kEMMRestartAuthParameter = @"emmres";
 // The URL template for the URL to revoke the token.
 static NSString *const kRevokeTokenURLTemplate = @"https://%@/o/oauth2/revoke";
 
-// Expected path in the URL scheme to be handled.
-static NSString *const kBrowserCallbackPath = @"/oauth2callback";
-
 // Expected path for EMM callback.
 static NSString *const kEMMCallbackPath = @"/emmcallback";
 
@@ -116,9 +115,6 @@ static const NSTimeInterval kPresentationDelayAfterCancel = 1.0;
 static NSString *const kAudienceParameter = @"audience";
 // See b/11669751 .
 static NSString *const kOpenIDRealmParameter = @"openid.realm";
-static NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes";
-static NSString *const kLoginHintParameter = @"login_hint";
-static NSString *const kHostedDomainParameter = @"hd";
 
 // Minimum time to expiration for a restored access token.
 static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
@@ -148,8 +144,6 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   // represent a sign in continuation.
   GIDSignInInternalOptions *_currentOptions;
   
-  // AppAuth external user-agent session state.
-  id<OIDExternalUserAgentSession> _currentAuthorizationFlow;
   // Flag to indicate that the auth flow is restarting.
   BOOL _restarting;
   
@@ -161,6 +155,9 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   
   // The class to fetches data from a url end point.
   id<GIDHTTPFetcher> _httpFetcher;
+  
+  // The class to control the authorization flow.
+  id<GIDAuthorizationFlowProcessor> _authorizationFlowProcessor;
 }
 
 #pragma mark - Public methods
@@ -172,8 +169,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 - (BOOL)handleURL:(NSURL *)url {
   // Check if the callback path matches the expected one for a URL from Safari/Chrome/SafariVC.
   if ([url.path isEqual:kBrowserCallbackPath]) {
-    if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) {
-      _currentAuthorizationFlow = nil;
+    if ([_authorizationFlowProcessor resumeExternalUserAgentFlowWithURL:url]) {
       return YES;
     }
     return NO;
@@ -245,12 +241,12 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                           additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
                                 completion:(nullable GIDSignInCompletion)completion {
   GIDSignInInternalOptions *options =
-    [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
-                                     presentingViewController:presentingViewController
-                                                    loginHint:hint
-                                                addScopesFlow:NO
-                                                       scopes:additionalScopes
-                                                   completion:completion];
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
+                                       presentingViewController:presentingViewController
+                                                      loginHint:hint
+                                                  addScopesFlow:NO
+                                                         scopes:additionalScopes
+                                                     completion:completion];
   [self signInWithOptions:options];
 }
 
@@ -323,12 +319,12 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                   additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
                         completion:(nullable GIDSignInCompletion)completion {
   GIDSignInInternalOptions *options =
-    [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
-                                             presentingWindow:presentingWindow
-                                                    loginHint:hint
-                                                addScopesFlow:NO
-                                                       scopes:additionalScopes
-                                                   completion:completion];
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
+                                               presentingWindow:presentingWindow
+                                                      loginHint:hint
+                                                  addScopesFlow:NO
+                                                         scopes:additionalScopes
+                                                     completion:completion];
   [self signInWithOptions:options];
 }
 
@@ -451,15 +447,20 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 - (id)initPrivate {
   id<GIDKeychainHandler> keychainHandler = [[GIDKeychainHandler alloc] init];
   id<GIDHTTPFetcher> httpFetcher = [[GIDHTTPFetcher alloc] init];
+  id<GIDAuthorizationFlowProcessor> authorizationFlowProcessor =
+      [[GIDAuthorizationFlowProcessor alloc] init];
   id<GIDProfileDataFetcher> profileDataFetcher = [[GIDProfileDataFetcher alloc] init];
   return [self initWithKeychainHandler:keychainHandler
                            httpFetcher:httpFetcher
-                    profileDataFetcher:profileDataFetcher];
+                    profileDataFetcher:profileDataFetcher
+            authorizationFlowProcessor:authorizationFlowProcessor];
 }
 
 - (instancetype)initWithKeychainHandler:(id<GIDKeychainHandler>)keychainHandler
                             httpFetcher:(id<GIDHTTPFetcher>)httpFetcher
-                     profileDataFetcher:(id<GIDProfileDataFetcher>)profileDataFetcher {
+                     profileDataFetcher:(id<GIDProfileDataFetcher>)profileDataFetcher
+             authorizationFlowProcessor:
+    (id<GIDAuthorizationFlowProcessor>)authorizationFlowProcessor {
   self = [super init];
   if (self) {
     // Get the bundle of the current executable.
@@ -489,6 +490,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     
     _keychainHandler = keychainHandler;
     _httpFetcher = httpFetcher;
+    _authorizationFlowProcessor = authorizationFlowProcessor;
     _profileDataFetcher = profileDataFetcher;
   }
   return self;
@@ -515,7 +517,6 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     // Explicitly throw exception for missing client ID here. This must come before
     // scheme check because schemes rely on reverse client IDs.
     [self assertValidParameters];
-
     [self assertValidPresentingViewController];
 
     // If the application does not support the required URL schemes tell the developer so.
@@ -554,64 +555,17 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 #pragma mark - Authentication flow
 
 - (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options {
-  GIDSignInCallbackSchemes *schemes =
-      [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
-  NSURL *redirectURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@",
-                                             [schemes clientIdentifierScheme],
-                                             kBrowserCallbackPath]];
   NSString *emmSupport;
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
   emmSupport = [[self class] isOperatingSystemAtLeast9] ? kEMMVersion : nil;
 #elif TARGET_OS_MACCATALYST || TARGET_OS_OSX
   emmSupport = nil;
 #endif // TARGET_OS_MACCATALYST || TARGET_OS_OSX
-
-  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
-  additionalParameters[kIncludeGrantedScopesParameter] = @"true";
-  if (options.configuration.serverClientID) {
-    additionalParameters[kAudienceParameter] = options.configuration.serverClientID;
-  }
-  if (options.loginHint) {
-    additionalParameters[kLoginHintParameter] = options.loginHint;
-  }
-  if (options.configuration.hostedDomain) {
-    additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain;
-  }
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  [additionalParameters addEntriesFromDictionary:
-      [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
-  additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
-  additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
   
-  NSURL *authorizationEndpointURL = [GIDSignInPreferences authorizationEndpointURL];
-  NSURL *tokenEndpointURL = [GIDSignInPreferences tokenEndpointURL];
-  OIDServiceConfiguration *appAuthConfiguration =
-      [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpointURL
-                                                       tokenEndpoint:tokenEndpointURL];
-
-  OIDAuthorizationRequest *request =
-      [[OIDAuthorizationRequest alloc] initWithConfiguration:appAuthConfiguration
-                                                    clientId:options.configuration.clientID
-                                                      scopes:options.scopes
-                                                 redirectURL:redirectURL
-                                                responseType:OIDResponseTypeCode
-                                        additionalParameters:additionalParameters];
-
-  _currentAuthorizationFlow = [OIDAuthorizationService
-      presentAuthorizationRequest:request
-#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
-         presentingViewController:options.presentingViewController
-#elif TARGET_OS_OSX
-                 presentingWindow:options.presentingWindow
-#endif // TARGET_OS_OSX
-                        callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
-                                   NSError *_Nullable error) {
+  [_authorizationFlowProcessor startWithOptions:options
+                                     emmSupport:(NSString *)emmSupport
+                                     completion:^(OIDAuthorizationResponse *authorizationResponse,
+                                                  NSError *error) {
     [self processAuthorizationResponse:authorizationResponse
                                  error:error
                             emmSupport:emmSupport];
@@ -680,7 +634,6 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 // Perform authentication with the provided options.
 - (void)authenticateWithOptions:(GIDSignInInternalOptions *)options {
-
   // If this is an interactive flow, we're not going to try to restore any saved auth state.
   if (options.interactive) {
     [self authenticateInteractivelyWithOptions:options];
@@ -873,12 +826,11 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     return NO;
   }
 #endif // TARGET_OS_OSX
-  if (!_currentAuthorizationFlow) {
+  if (!_authorizationFlowProcessor.isStarted) {
     return NO;
   }
   _restarting = YES;
-  [_currentAuthorizationFlow cancel];
-  _currentAuthorizationFlow = nil;
+  [_authorizationFlowProcessor cancelAuthenticationFlow];
   _restarting = NO;
   NSDictionary<NSString *, NSString *> *extraParameters = @{ kEMMRestartAuthParameter : @"1" };
   // In iOS 13 the presentation of ASWebAuthenticationSession needs an anchor window,

+ 6 - 0
GoogleSignIn/Sources/GIDSignInPreferences.h

@@ -18,9 +18,15 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+/// The name of the query parameter used for logging the SDK version.
 extern NSString *const kSDKVersionLoggingParameter;
+
+/// The name of the query parameter used for logging the Apple execution environment.
 extern NSString *const kEnvironmentLoggingParameter;
 
+/// Expected path in the URL scheme to be handled.
+extern NSString *const kBrowserCallbackPath;
+
 NSString* GIDVersion(void);
 
 NSString* GIDEnvironment(void);

+ 4 - 6
GoogleSignIn/Sources/GIDSignInPreferences.m

@@ -16,16 +16,14 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+NSString *const kSDKVersionLoggingParameter = @"gpsdk";
+NSString *const kEnvironmentLoggingParameter = @"gidenv";
+NSString *const kBrowserCallbackPath = @"/oauth2callback";
+
 static NSString *const kLSOServer = @"accounts.google.com";
 static NSString *const kTokenServer = @"oauth2.googleapis.com";
 static NSString *const kUserInfoServer = @"www.googleapis.com";
 
-// The name of the query parameter used for logging the SDK version.
-NSString *const kSDKVersionLoggingParameter = @"gpsdk";
-
-// The name of the query parameter used for logging the Apple execution environment.
-NSString *const kEnvironmentLoggingParameter = @"gidenv";
-
 // Supported Apple execution environments
 static NSString *const kAppleEnvironmentUnknown = @"unknown";
 static NSString *const kAppleEnvironmentIOS = @"ios";

+ 3 - 0
GoogleSignIn/Sources/GIDSignIn_Private.h

@@ -29,6 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
 @class GIDGoogleUser;
 @class GIDSignInInternalOptions;
 
+@protocol GIDAuthorizationFlowProcessor;
 @protocol GIDHTTPFetcher;
 @protocol GIDKeychainHandler;
 @protocol GIDProfileDataFetcher;
@@ -54,6 +55,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 - (instancetype)initWithKeychainHandler:(id<GIDKeychainHandler>)keychainHandler
                             httpFetcher:(id<GIDHTTPFetcher>)HTTPFetcher
                      profileDataFetcher:(id<GIDProfileDataFetcher>)profileDataFetcher
+             authorizationFlowProcessor:
+    (id<GIDAuthorizationFlowProcessor>)authorizationFlowProcessor
     NS_DESIGNATED_INITIALIZER;
 
 /// Authenticates with extra options.

+ 4 - 4
GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h

@@ -36,16 +36,16 @@ NS_ASSUME_NONNULL_BEGIN
 /// Whether or not the user has profile image.
 @property(nonatomic, readonly) BOOL hasImage;
 
++ (instancetype)new NS_UNAVAILABLE;
+
+- (instancetype)init NS_UNAVAILABLE;
+
 /// Gets the user's profile image URL for the given dimension in pixels for each side of the square.
 ///
 /// @param dimension The desired height (and width) of the profile image.
 /// @return The URL of the user's profile image.
 - (nullable NSURL *)imageURLWithDimension:(NSUInteger)dimension;
 
-- (instancetype)init NS_UNAVAILABLE;
-
-+ (instancetype)new NS_UNAVAILABLE;
-
 @end
 
 NS_ASSUME_NONNULL_END

+ 139 - 0
GoogleSignIn/Tests/Unit/GIDAuthorizationFlowProcessorTest.m

@@ -0,0 +1,139 @@
+// Copyright 2023 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/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h"
+
+#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.h"
+
+#import <XCTest/XCTest.h>
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+@import OCMock;
+#else
+#import <AppAuth/AppAuth.h>
+#import <OCMock/OCMock.h>
+#endif
+
+static NSString *const kFakeURL = @"www.fakeURL.com";
+static NSString *const kErrorDomain = @"ERROR_DOMAIN";
+static NSInteger const kErrorCode = 400;
+static NSInteger const kTimeout = 1;
+
+@interface GIDAuthorizationFlowProcessorTest : XCTestCase {
+  GIDAuthorizationFlowProcessor *_authorizationFlowProcessor;
+  OIDFakeExternalUserAgentSession *_fakeExternalUserAgentSession;
+  id _authorizationServiceMock;
+  OIDAuthorizationResponse *_fakeResponse;
+}
+
+@end
+
+@implementation GIDAuthorizationFlowProcessorTest
+
+- (void)setUp {
+  [super setUp];
+  
+  _authorizationFlowProcessor = [[GIDAuthorizationFlowProcessor alloc] init];
+  _fakeExternalUserAgentSession= [[OIDFakeExternalUserAgentSession alloc] init];
+
+  _authorizationServiceMock = OCMClassMock([OIDAuthorizationService class]);
+  _fakeResponse = [OIDAuthorizationResponse testInstance];
+  NSError *error = [self error];
+  OCMStub([_authorizationServiceMock
+      presentAuthorizationRequest:[OCMArg any]
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+         presentingViewController:[OCMArg any]
+#elif TARGET_OS_OSX
+                 presentingWindow:[OCMArg any]
+#endif // TARGET_OS_OSX
+                         callback:([OCMArg invokeBlockWithArgs:_fakeResponse, error, nil])
+          ]).andReturn(_fakeExternalUserAgentSession);
+}
+
+- (void)testStartAndCancelAuthorizationFlow_success {
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion is invoked."];
+  GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
+  [_authorizationFlowProcessor startWithOptions:options
+                                     emmSupport:nil
+                                     completion:^(OIDAuthorizationResponse *authorizationResponse,
+                                                  NSError *error) {
+    XCTAssertEqualObjects(authorizationResponse.accessToken,
+                          self->_fakeResponse.accessToken);
+    XCTAssertEqualObjects(authorizationResponse.authorizationCode,
+                          self->_fakeResponse.authorizationCode);
+    [expectation fulfill];
+  }];
+  [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+  XCTAssertTrue(_authorizationFlowProcessor.isStarted);
+  
+  [_authorizationFlowProcessor cancelAuthenticationFlow];
+  XCTAssertFalse(_authorizationFlowProcessor.isStarted);
+}
+
+- (void)testStartAndResumeAuthorizationFlow_success {
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion is invoked."];
+  GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
+  [_authorizationFlowProcessor startWithOptions:options
+                                     emmSupport:nil
+                                     completion:^(OIDAuthorizationResponse *authorizationResponse,
+                                                  NSError *error) {
+    XCTAssertEqualObjects(authorizationResponse.accessToken,
+                          self->_fakeResponse.accessToken);
+    XCTAssertEqualObjects(authorizationResponse.authorizationCode,
+                          self->_fakeResponse.authorizationCode);
+    [expectation fulfill];
+  }];
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  XCTAssertTrue(_authorizationFlowProcessor.isStarted);
+  
+  _fakeExternalUserAgentSession.resumeExternalUserAgentFlow = YES;
+  
+  NSURL *url = [[NSURL alloc] initWithString:kFakeURL];
+  [_authorizationFlowProcessor resumeExternalUserAgentFlowWithURL:url];
+  XCTAssertFalse(_authorizationFlowProcessor.isStarted);
+}
+
+- (void)testStartAndFailToResumeAuthorizationFlow {
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion is invoked."];
+  GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
+  [_authorizationFlowProcessor startWithOptions:options
+                                     emmSupport:nil
+                                     completion:^(OIDAuthorizationResponse *authorizationResponse,
+                                                  NSError *error) {
+    XCTAssertEqualObjects(authorizationResponse.accessToken,
+                          self->_fakeResponse.accessToken);
+    XCTAssertEqualObjects(authorizationResponse.authorizationCode,
+                          self->_fakeResponse.authorizationCode);
+    [expectation fulfill];
+  }];
+  [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+  XCTAssertTrue(_authorizationFlowProcessor.isStarted);
+ 
+  _fakeExternalUserAgentSession.resumeExternalUserAgentFlow = NO;
+  NSURL *url = [[NSURL alloc] initWithString:kFakeURL];
+  [_authorizationFlowProcessor resumeExternalUserAgentFlowWithURL:url];
+  XCTAssertTrue(_authorizationFlowProcessor.isStarted);
+}
+
+#pragma mark - Helpers
+
+- (NSError *)error {
+  return [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:nil];
+}
+
+@end

+ 57 - 24
GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m

@@ -1,4 +1,4 @@
-// Copyright 2022 Google LLC
+// Copyright 2023 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -39,9 +39,11 @@ static NSInteger const kErrorCode = 400;
   _profileDataFetcher = [[GIDProfileDataFetcher alloc] initWithHTTPFetcher:_httpFetcher];
 }
 
-- (void)testFetchProfileData_outOfAuthState_success {
+// Extracts the `GIDProfileData` out of a fat ID token.
+- (void)testFetchProfileData_fatIDToken_success {
+  NSString *fatIDToken = [OIDTokenResponse fatIDToken];
   OIDTokenResponse *tokenResponse =
-      [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken]
+      [OIDTokenResponse testInstanceWithIDToken:fatIDToken
                                     accessToken:kAccessToken
                                       expiresIn:@(300)
                                    refreshToken:kRefreshToken
@@ -71,7 +73,39 @@ static NSInteger const kErrorCode = 400;
   [self waitForExpectationsWithTimeout:1 handler:nil];
 }
 
-- (void)testFetchProfileData_fromInfoURL_fetchingError {
+// Can not extracts the `GIDProfileData` out of a non-fat ID token. Try to fetch it from the
+// UserInfo endpoint and get an error.
+- (void)testFetchProfileData_nonFatIDToken_fetchingError {
+  NSString *nonFatIDToken = [OIDTokenResponse idToken];
+  OIDTokenResponse *tokenResponse =
+      [OIDTokenResponse testInstanceWithIDToken:nonFatIDToken
+                                    accessToken:kAccessToken
+                                      expiresIn:@(300)
+                                   refreshToken:kRefreshToken
+                                   tokenRequest:nil];
+  OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse];
+  
+  NSError *fetchingError = [self error];
+  [self setGIDHTTPFetcherTestBlockWithData:nil error:fetchingError];
+  
+  XCTestExpectation *completionExpectation =
+      [self expectationWithDescription:@"completion is invoked"];
+  
+  [_profileDataFetcher
+      fetchProfileDataWithAuthState:authState
+                         completion:^(GIDProfileData *profileData, NSError *error) {
+    XCTAssertNil(profileData);
+    XCTAssertNotNil(error);
+    XCTAssertEqualObjects(error.domain, kErrorDomain);
+    XCTAssertEqual(error.code, kErrorCode);
+    [completionExpectation fulfill];
+  }];
+  
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+// There is no ID token. Try to fetch it from the UserInfo endpoint and get an error.
+- (void)testFetchProfileData_noIDToken_fetchingError {
   OIDTokenResponse *tokenResponse =
       [OIDTokenResponse testInstanceWithIDToken:nil
                                     accessToken:kAccessToken
@@ -80,15 +114,8 @@ static NSInteger const kErrorCode = 400;
                                    tokenRequest:nil];
   OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse];
   
-  XCTestExpectation *fetcherExpectation =
-      [self expectationWithDescription:@"_httpFetcher is invoked"];
-  GIDHTTPFetcherTestBlock testBlock =
-      ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) {
-        NSError *fetchingError = [self error];
-        responseProvider(nil, fetchingError);
-        [fetcherExpectation fulfill];
-      };
-  [_httpFetcher setTestBlock:testBlock];
+  NSError *fetchingError = [self error];
+  [self setGIDHTTPFetcherTestBlockWithData:nil error:fetchingError];
   
   XCTestExpectation *completionExpectation =
       [self expectationWithDescription:@"completion is invoked"];
@@ -106,8 +133,8 @@ static NSInteger const kErrorCode = 400;
   [self waitForExpectationsWithTimeout:1 handler:nil];
 }
 
-// Fetch the NSData successfully but can not parse it.
-- (void)testFetchProfileData_fromInfoURL_parsingError {
+// Fetch the NSData from the UserInfo endpoint successfully but can not parse it.
+- (void)testFetchProfileData_noIDToken_parsingError {
   OIDTokenResponse *tokenResponse =
       [OIDTokenResponse testInstanceWithIDToken:nil
                                     accessToken:kAccessToken
@@ -116,15 +143,8 @@ static NSInteger const kErrorCode = 400;
                                    tokenRequest:nil];
   OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse];
   
-  XCTestExpectation *fetcherExpectation =
-      [self expectationWithDescription:@"_httpFetcher is invoked"];
-  GIDHTTPFetcherTestBlock testBlock =
-      ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) {
-        NSData *data = [[NSData alloc] init];
-        responseProvider(data, nil);
-        [fetcherExpectation fulfill];
-      };
-  [_httpFetcher setTestBlock:testBlock];
+  NSData *data = [[NSData alloc] init];
+  [self setGIDHTTPFetcherTestBlockWithData:data error:nil];
   
   XCTestExpectation *completionExpectation =
       [self expectationWithDescription:@"completion is invoked"];
@@ -147,4 +167,17 @@ static NSInteger const kErrorCode = 400;
   return [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:nil];
 }
 
+/// Set the return value from the UserInfo endpoint.
+- (void)setGIDHTTPFetcherTestBlockWithData:(NSData *)data
+                                     error:(NSError *)error {
+  XCTestExpectation *fetcherExpectation =
+      [self expectationWithDescription:@"_httpFetcher is invoked"];
+  GIDHTTPFetcherTestBlock testBlock =
+      ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) {
+        responseProvider(data, error);
+        [fetcherExpectation fulfill];
+      };
+  [_httpFetcher setTestBlock:testBlock];
+}
+
 @end

+ 8 - 1
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -26,6 +26,7 @@
 // Test module imports
 @import GoogleSignIn;
 
+#import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h"
 #import "GoogleSignIn/Sources/GIDEMMSupport.h"
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
 #import "GoogleSignIn/Sources/GIDSignIn_Private.h"
@@ -35,6 +36,7 @@
 #import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h"
 #import "GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/Fakes/GIDFakeProfileDataFetcher.h"
 
+
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
@@ -318,9 +320,14 @@ static NSString *const kNewScope = @"newScope";
   
   _profileDataFetcher = [[GIDFakeProfileDataFetcher alloc] init];
   
+  GIDAuthorizationFlowProcessor * authorizationFlowProcessor =
+      [[GIDAuthorizationFlowProcessor alloc] init];
+  
   _signIn = [[GIDSignIn alloc] initWithKeychainHandler:_keychainHandler
                                            httpFetcher:_httpFetcher
-                                    profileDataFetcher:_profileDataFetcher];
+                                    profileDataFetcher:_profileDataFetcher
+                            authorizationFlowProcessor:authorizationFlowProcessor];
+
   _hint = nil;
 
   __weak GIDSignInTest *weakSelf = self;

+ 32 - 0
GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.h

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 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>
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#endif
+
+/// The fake OIDExternalUserAgentSession.
+@interface OIDFakeExternalUserAgentSession : NSObject <OIDExternalUserAgentSession>
+
+/// Set the return value for the method `resumeExternalUserAgentFlowWithURL:`.
+/// The defualt value is YES.
+@property(nonatomic) BOOL resumeExternalUserAgentFlow;
+
+@end

+ 44 - 0
GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.m

@@ -0,0 +1,44 @@
+// Copyright 2023 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/OIDFakeExternalUserAgentSession.h"
+
+@implementation OIDFakeExternalUserAgentSession
+
+- (instancetype)init {
+  self = [super init];
+  if (self) {
+    self.resumeExternalUserAgentFlow = YES;
+  }
+  return self;
+}
+
+- (void)cancel {
+  // no op.
+}
+
+- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL {
+  return self.resumeExternalUserAgentFlow;
+}
+
+- (void)cancelWithCompletion:(nullable void (^)(void))completion {
+  NSAssert(NO, @"Not implemented.");
+}
+
+
+- (void)failExternalUserAgentFlowWithError:(nonnull NSError *)error {
+  NSAssert(NO, @"Not implemented.");
+}
+
+@end