Ver código fonte

Process authorization response and token fetch. (#414)

Brianna Morales 1 ano atrás
pai
commit
95e7561d35
20 arquivos alterados com 1090 adições e 148 exclusões
  1. 61 0
      GoogleSignIn/Sources/GIDAuthFlow.h
  2. 41 0
      GoogleSignIn/Sources/GIDAuthFlow.m
  3. 62 0
      GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h
  4. 44 0
      GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h
  5. 76 0
      GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.m
  6. 53 0
      GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h
  7. 66 0
      GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.m
  8. 27 0
      GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h
  9. 268 0
      GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.m
  10. 35 139
      GoogleSignIn/Sources/GIDSignIn.m
  11. 18 0
      GoogleSignIn/Sources/GIDSignInConstants.h
  12. 9 0
      GoogleSignIn/Sources/GIDSignInConstants.m
  13. 48 9
      GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m
  14. 11 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h
  15. 54 0
      GoogleSignIn/Tests/Unit/GIDAuthFlowTest.m
  16. 185 0
      GoogleSignIn/Tests/Unit/GIDAuthorizationResponseHelperTest.m
  17. 4 0
      GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h
  18. 18 0
      GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m
  19. 2 0
      GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h
  20. 8 0
      GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m

+ 61 - 0
GoogleSignIn/Sources/GIDAuthFlow.h

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 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/GIDCallbackQueue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class OIDAuthState;
+@class GIDProfileData;
+
+/// The callback queue used for authentication flow.
+@interface GIDAuthFlow : GIDCallbackQueue
+
+/// A representation of the state of the OAuth session for this instance.
+@property(nonatomic, strong, nullable) OIDAuthState *authState;
+
+/// The error thrown from the OAuth session encountered for this instance.
+@property(nonatomic, strong, nullable) NSError *error;
+
+/// The EMM support version.
+@property(nonatomic, copy, nullable) NSString *emmSupport;
+
+/// The profile data extracted from the ID token.
+@property(nonatomic, nullable) GIDProfileData *profileData;
+
+/// Initialize a `GIDAuthFlow` object by specifying all available properties.
+///
+/// @param authState The configuration to be used.
+/// @param error The configuration to be used.
+/// @param emmSupport The configuration to be used.
+/// @param profileData The configuration to be used.
+/// @return An initialized `GIDAuthFlow` instance.
+- (instancetype)initWithAuthState:(nullable OIDAuthState *)authState
+                            error:(nullable NSError *)error
+                       emmSupport:(nullable NSString *)emmSupport
+                      profileData:(nullable GIDProfileData *)profileData
+    NS_DESIGNATED_INITIALIZER;
+
+/// Initialize a `GIDAuthFlow` object by calling the designated initializer with `nil` values.
+///
+/// @return An initialized `GIDAuthFlow` instance.
+- (instancetype)init;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 41 - 0
GoogleSignIn/Sources/GIDAuthFlow.m

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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/GIDAuthFlow.h"
+
+@implementation GIDAuthFlow
+
+- (instancetype)initWithAuthState:(nullable OIDAuthState *)authState
+                            error:(nullable NSError *)error
+                       emmSupport:(nullable NSString *)emmSupport
+                      profileData:(nullable GIDProfileData *)profileData {
+  self = [super init];
+  if (self) {
+    _authState = authState;
+    _error = error;
+    _emmSupport = emmSupport;
+    _profileData = profileData;
+  }
+  return self;
+}
+
+- (instancetype)init {
+  return [self initWithAuthState:nil
+                           error:nil
+                      emmSupport:nil
+                     profileData:nil];
+}
+
+@end

+ 62 - 0
GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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 GIDAuthFlow;
+@class GIDConfiguration;
+@class OIDAuthorizationResponse;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A list of potential current flow names.
+typedef NS_ENUM(NSInteger, GIDFlowName) {
+  /// The Sign In flow.
+  GIDFlowNameSignIn = 0,
+  /// The Verify flow.
+  GIDFlowNameVerifyAccountDetail = 1,
+};
+
+@protocol GIDAuthorizationResponseHandling
+
+/// Initializes a new instance of the `GIDAuthorizationResponseHandling` class with the provided fields.
+///
+/// @param authorizationResponse The authorization response to be processed.
+/// @param emmSupport The EMM support version.
+/// @param flowName The name of the current flow.
+/// @param configuration The configuration.
+/// @param error The error thrown if there's no authorization response.
+/// @return A new initialized instance of the `GIDAuthorizationResponseHandling` class.
+- (instancetype)
+    initWithAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse
+                       emmSupport:(nullable NSString *)emmSupport
+                         flowName:(enum GIDFlowName)flowName
+                    configuration:(nullable GIDConfiguration *)configuration
+                            error:(nullable NSError *)error;
+
+/// Fetches the access token if necessary as part of the auth flow.
+///
+/// @param authFlow The auth flow to either fetch tokens or error.
+- (void)maybeFetchToken:(GIDAuthFlow *)authFlow;
+
+/// Processes the authorization response and returns an auth flow.
+///
+/// @return An instance of `GIDAuthFlow`.
+- (GIDAuthFlow *)generateAuthFlowFromAuthorizationResponse;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 44 - 0
GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 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/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class OIDAuthState;
+
+/// A fake implementation of `GIDAuthorizationResponseHandling` for testing purposes.
+@interface GIDAuthorizationResponseHandlingFake : NSObject <GIDAuthorizationResponseHandling>
+
+/// The auth state to be used to fetch tokens.
+@property (nonatomic, nullable) OIDAuthState *authState;
+
+/// The error to be passed into the completion.
+@property (nonatomic, nullable) NSError *error;
+
+/// Creates an instance conforming to `GIDAuthorizationResponseHandling` with the provided
+/// auth state and error.
+///
+/// @param authState The `OIDAuthState` instance to access tokens.
+/// @param error The `NSError` to pass into the completion.
+- (instancetype)initWithAuthState:(nullable OIDAuthState *)authState
+                            error:(nullable NSError *)error;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 76 - 0
GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.m

@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 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/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h"
+
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h"
+
+#import "GoogleSignIn/Sources/GIDAuthFlow.h"
+#import "GoogleSignIn/Sources/GIDSignInConstants.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/OIDAuthState.h>
+#import <AppAuth/OIDTokenResponse.h>
+#endif
+
+@implementation GIDAuthorizationResponseHandlingFake
+
+- (instancetype)initWithAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse 
+                                   emmSupport:(NSString *)emmSupport
+                                     flowName:(enum GIDFlowName)flowName
+                                configuration:(GIDConfiguration *)configuration
+                                        error:(NSError *)error {
+  self = [super init];
+  if (self) {
+    NSAssert(false, @"This class is only to be used in testing. Do not use.");
+  }
+  return self;
+}
+
+- (instancetype)initWithAuthState:(nullable OIDAuthState *)authState
+                            error:(nullable NSError *)error {
+  self = [super init];
+  if (self) {
+    _authState = authState;
+    _error = error;
+  }
+  return self;
+}
+
+- (GIDAuthFlow *)generateAuthFlowFromAuthorizationResponse {
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:self.authState
+                                                           error:self.error
+                                                      emmSupport:nil
+                                                     profileData:nil];
+  return authFlow;
+}
+
+- (void)maybeFetchToken:(GIDAuthFlow *)authFlow {
+  if (authFlow.error ||
+      (_authState.lastTokenResponse.accessToken &&
+       [_authState.lastTokenResponse.accessTokenExpirationDate timeIntervalSinceNow] >
+       kMinimumRestoredAccessTokenTimeToExpire)) {
+    return;
+  }
+
+  authFlow.authState = self.authState;
+  authFlow.error = self.error;
+}
+
+@end
+

+ 53 - 0
GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 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/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h"
+
+@class GIDAuthFlow;
+@class GIDConfiguration;
+@class OIDAuthorizationResponse;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A helper class to process the authorization response.
+@interface GIDAuthorizationResponseHelper : NSObject
+
+/// The response handler used to process the authorization response.
+@property (nonatomic, readonly) id<GIDAuthorizationResponseHandling> responseHandler;
+
+/// Initializes a new instance of the `GIDAuthorizationResponseHelper` class with the provided field.
+///
+/// @param responseHandler The response handler with the authorization response to process.
+/// @return A new initialized instance of the `GIDAuthorizationResponseHelper` class.
+- (instancetype)
+initWithAuthorizationResponseHandler:(id<GIDAuthorizationResponseHandling>)responseHandler;
+
+/// Fetches the access token if necessary using the response handler as part of the auth flow.
+///
+/// @param authFlow The auth flow to either fetch tokens or error.
+- (void)fetchTokenWithAuthFlow:(GIDAuthFlow *)authFlow;
+
+
+/// Processes the initialized authorization response and returns a filled `GIDAuthFlow` instance.
+///
+/// @return An instance of `GIDAuthFlow`  to either fetch tokens or error.
+- (GIDAuthFlow *)fetchAuthFlowFromProcessedResponse;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 66 - 0
GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.m

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 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/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h"
+
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
+
+#import "GoogleSignIn/Sources/GIDAuthFlow.h"
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
+#import "GoogleSignIn/Sources/GIDSignInConstants.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/OIDAuthState.h>
+#import <AppAuth/OIDAuthorizationResponse.h>
+#import <AppAuth/OIDAuthorizationService.h>
+#import <AppAuth/OIDError.h>
+#import <AppAuth/OIDTokenRequest.h>
+#import <AppAuth/OIDTokenResponse.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation GIDAuthorizationResponseHelper
+
+- (instancetype)
+initWithAuthorizationResponseHandler:(id<GIDAuthorizationResponseHandling>)responseHandler {
+  self = [super init];
+  if (self) {
+    _responseHandler = responseHandler;
+  }
+  return self;
+}
+
+- (void)fetchTokenWithAuthFlow:(GIDAuthFlow *)authFlow {
+  [self.responseHandler maybeFetchToken:authFlow];
+}
+
+- (GIDAuthFlow *)fetchAuthFlowFromProcessedResponse {
+  GIDAuthFlow *authFlow = [self.responseHandler generateAuthFlowFromAuthorizationResponse];
+
+  return authFlow;
+}
+@end
+
+NS_ASSUME_NONNULL_END

+ 27 - 0
GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A class that handles the authorization response and fetches tokens.
+@interface GIDAuthorizationResponseHandler : NSObject <GIDAuthorizationResponseHandling>
+@end
+
+NS_ASSUME_NONNULL_END

+ 268 - 0
GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.m

@@ -0,0 +1,268 @@
+/*
+ * Copyright 2024 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/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
+
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h"
+
+#import "GoogleSignIn/Sources/GIDAuthFlow.h"
+#import "GoogleSignIn/Sources/GIDSignInConstants.h"
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/OIDAuthState.h>
+#import <AppAuth/OIDAuthorizationResponse.h>
+#import <AppAuth/OIDAuthorizationService.h>
+#import <AppAuth/OIDError.h>
+#import <AppAuth/OIDTokenRequest.h>
+#import <AppAuth/OIDTokenResponse.h>
+#endif
+
+@interface GIDAuthorizationResponseHandler ()
+
+/// The authorization response to process.
+@property(nonatomic, nullable) OIDAuthorizationResponse *authorizationResponse;
+
+/// The EMM support version.
+@property(nonatomic, nullable) NSString *emmSupport;
+
+/// The name of the current flow.
+@property(nonatomic) GIDFlowName flowName;
+
+/// The configuration for the current flow.
+@property(nonatomic, nullable) GIDConfiguration *configuration;
+
+/// The configuration for the current flow.
+@property(nonatomic, nullable) NSError *error;
+
+@end
+
+@implementation GIDAuthorizationResponseHandler
+
+- (instancetype)
+    initWithAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse
+                       emmSupport:(nullable NSString *)emmSupport
+                         flowName:(GIDFlowName)flowName
+                    configuration:(nullable GIDConfiguration *)configuration
+                            error:(nullable NSError *)error {
+  self = [super init];
+  if (self) {
+    _authorizationResponse = authorizationResponse;
+    _emmSupport = emmSupport;
+    _flowName = flowName;
+    _configuration = configuration;
+    _error = error;
+  }
+  return self;
+}
+
+- (GIDAuthFlow *)generateAuthFlowFromAuthorizationResponse {
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:nil
+                                                           error:nil
+                                                      emmSupport:_emmSupport
+                                                     profileData:nil];
+
+  if (_authorizationResponse) {
+    if (_authorizationResponse.authorizationCode.length) {
+      authFlow.authState =
+          [[OIDAuthState alloc] initWithAuthorizationResponse:_authorizationResponse];
+      [self maybeFetchToken:authFlow];
+    } else {
+      [self authorizationCodeErrorToAuthFlow:authFlow];
+    }
+  } else {
+    [self authorizationResponseErrorToAuthFlow:authFlow error:_error];
+  }
+  return authFlow;
+}
+
+- (void)maybeFetchToken:(GIDAuthFlow *)authFlow {
+  OIDAuthState *authState = authFlow.authState;
+  // Do nothing if we have an auth flow error or a restored access token that isn't near expiration.
+  if (authFlow.error ||
+      (authState.lastTokenResponse.accessToken &&
+       [authState.lastTokenResponse.accessTokenExpirationDate timeIntervalSinceNow] >
+       kMinimumRestoredAccessTokenTimeToExpire)) {
+    return;
+  }
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
+  if (_configuration.serverClientID) {
+    additionalParameters[kAudienceParameter] = _configuration.serverClientID;
+  }
+  if (_configuration.openIDRealm) {
+    additionalParameters[kOpenIDRealmParameter] = _configuration.openIDRealm;
+  }
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  if (_flowName == GIDFlowNameSignIn) {
+    NSDictionary<NSString *, NSObject *> *params =
+        authState.lastAuthorizationResponse.additionalParameters;
+    NSString *passcodeInfoRequired = (NSString *)params[kEMMPasscodeInfoRequiredKeyName];
+    [additionalParameters addEntriesFromDictionary:
+        [GIDEMMSupport parametersWithParameters:@{}
+                                     emmSupport:authFlow.emmSupport
+                         isPasscodeInfoRequired:passcodeInfoRequired.length > 0]];
+  }
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
+  additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
+
+  OIDTokenRequest *tokenRequest;
+  if (!authState.lastTokenResponse.accessToken &&
+      authState.lastAuthorizationResponse.authorizationCode) {
+    tokenRequest = [authState.lastAuthorizationResponse
+        tokenExchangeRequestWithAdditionalParameters:additionalParameters];
+  } else {
+    [additionalParameters
+        addEntriesFromDictionary:authState.lastTokenResponse.request.additionalParameters];
+    tokenRequest = [authState tokenRefreshRequestWithAdditionalParameters:additionalParameters];
+  }
+
+  // TODO: Clean up callback flow (#427).
+  [authFlow wait];
+  [OIDAuthorizationService
+      performTokenRequest:tokenRequest
+                 callback:^(OIDTokenResponse *_Nullable tokenResponse,
+                            NSError *_Nullable error) {
+    [authState updateWithTokenResponse:tokenResponse error:error];
+    authFlow.error = error;
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+    if (authFlow.emmSupport) {
+      [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *error) {
+        authFlow.error = error;
+        [authFlow next];
+      }];
+    } else {
+      [authFlow next];
+    }
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+    [authFlow next];
+#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
+  }];
+}
+
+- (void)authorizationCodeErrorToAuthFlow:(GIDAuthFlow *)authFlow {
+  NSDictionary<NSString *, NSObject *> *params = _authorizationResponse.additionalParameters;
+  NSString *errorString = (NSString *)params[kOAuth2ErrorKeyName];
+
+  switch (_flowName) {
+    case GIDFlowNameSignIn: {
+      GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+      if (authFlow.emmSupport) {
+        [authFlow wait];
+        BOOL isEMMError = [[GIDEMMErrorHandler sharedInstance]
+                           handleErrorFromResponse:params
+                                        completion:^{
+                                          [authFlow next];
+                                        }];
+        if (isEMMError) {
+          errorCode = kGIDSignInErrorCodeEMM;
+        }
+      }
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+      if ([errorString isEqualToString:kOAuth2AccessDenied]) {
+        errorCode = kGIDSignInErrorCodeCanceled;
+      }
+
+      authFlow.error = [self errorWithString:errorString code:errorCode];
+    }
+      break;
+    case GIDFlowNameVerifyAccountDetail: {
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+      GIDVerifyErrorCode errorCode = GIDVerifyErrorCodeUnknown;
+      if ([errorString isEqualToString:kOAuth2AccessDenied]) {
+        errorCode = GIDVerifyErrorCodeCanceled;
+      }
+
+      authFlow.error = [self errorWithString:errorString code:errorCode];
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+      break;
+    }
+  }
+}
+
+- (void)authorizationResponseErrorToAuthFlow:(GIDAuthFlow *)authFlow
+                                       error:(NSError *)error {
+  NSString *errorString = [error localizedDescription];
+  switch (_flowName) {
+    case GIDFlowNameSignIn: {
+      GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
+      if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) {
+        // The user has canceled the flow at the iOS modal dialog.
+        errorString = kUserCanceledSignInError;
+        errorCode = kGIDSignInErrorCodeCanceled;
+      }
+      authFlow.error = [self errorWithString:errorString code:errorCode];
+      break;
+    }
+    case GIDFlowNameVerifyAccountDetail: {
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+      GIDVerifyErrorCode errorCode = GIDVerifyErrorCodeUnknown;
+      if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) {
+        errorString = kUserCanceledVerifyError;
+        errorCode = GIDVerifyErrorCodeCanceled;
+      }
+      authFlow.error = [self errorWithString:errorString code:errorCode];
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+      break;
+    }
+  }
+}
+
+- (NSError *)errorWithString:(nullable NSString *)errorString code:(NSInteger)code {
+  if (errorString == nil) {
+    errorString = @"Unknown error";
+  }
+
+  if (!_flowName) {
+    errorString = @"No specified flow";
+  }
+
+  NSDictionary<NSString *, NSString *> *errorDict = @{ NSLocalizedDescriptionKey : errorString };
+  switch (_flowName) {
+    case GIDFlowNameSignIn:
+      return [NSError errorWithDomain:kGIDSignInErrorDomain
+                                 code:code
+                             userInfo:errorDict];
+      break;
+    case GIDFlowNameVerifyAccountDetail:
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+      return [NSError errorWithDomain:kGIDVerifyErrorDomain
+                                 code:code
+                             userInfo:errorDict];
+      break;
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+    default:
+      break;
+  }
+  return [NSError errorWithDomain:kGIDSignInErrorDomain
+                             code:code
+                         userInfo:errorDict];
+}
+
+
+@end

+ 35 - 139
GoogleSignIn/Sources/GIDSignIn.m

@@ -21,11 +21,14 @@
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h"
 
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h"
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h"
+
+#import "GoogleSignIn/Sources/GIDAuthFlow.h"
 #import "GoogleSignIn/Sources/GIDEMMSupport.h"
 #import "GoogleSignIn/Sources/GIDSignInConstants.h"
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
 #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
-#import "GoogleSignIn/Sources/GIDCallbackQueue.h"
 #import "GoogleSignIn/Sources/GIDScopes.h"
 #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
@@ -52,8 +55,6 @@
 #import <AppAuth/OIDIDToken.h>
 #import <AppAuth/OIDResponseTypes.h>
 #import <AppAuth/OIDServiceConfiguration.h>
-#import <AppAuth/OIDTokenRequest.h>
-#import <AppAuth/OIDTokenResponse.h>
 #import <AppAuth/OIDURLQueryComponent.h>
 #import <GTMSessionFetcher/GTMSessionFetcher.h>
 
@@ -97,16 +98,10 @@ static NSString *const kBasicProfileFamilyNameKey = @"family_name";
 
 // Parameters in the callback URL coming back from browser.
 static NSString *const kAuthorizationCodeKeyName = @"code";
-static NSString *const kOAuth2ErrorKeyName = @"error";
-static NSString *const kOAuth2AccessDenied = @"access_denied";
-static NSString *const kEMMPasscodeInfoRequiredKeyName = @"emm_passcode_info_required";
 
 // Error string for unavailable keychain.
 static NSString *const kKeychainError = @"keychain error";
 
-// Error string for user cancelations.
-static NSString *const kUserCanceledError = @"The user canceled the sign-in flow.";
-
 // User preference key to detect fresh install of the app.
 static NSString *const kAppHasRunBeforeKey = @"GID_AppHasRunBefore";
 
@@ -116,22 +111,6 @@ static const NSTimeInterval kFetcherMaxRetryInterval = 15.0;
 // The delay before the new sign-in flow can be presented after the existing one is cancelled.
 static const NSTimeInterval kPresentationDelayAfterCancel = 1.0;
 
-// Minimum time to expiration for a restored access token.
-static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
-
-// The callback queue used for authentication flow.
-@interface GIDAuthFlow : GIDCallbackQueue
-
-@property(nonatomic, strong, nullable) OIDAuthState *authState;
-@property(nonatomic, strong, nullable) NSError *error;
-@property(nonatomic, copy, nullable) NSString *emmSupport;
-@property(nonatomic, nullable) GIDProfileData *profileData;
-
-@end
-
-@implementation GIDAuthFlow
-@end
-
 @implementation GIDSignIn {
   // This value is used when sign-in flows are resumed via the handling of a URL. Its value is
   // set when a sign-in flow is begun via |signInWithOptions:| when the options passed don't
@@ -593,8 +572,8 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   }];
 }
 
-- (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse
-                               error:(NSError *)error
+- (void)processAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse
+                               error:(nullable NSError *)error
                           emmSupport:(NSString *)emmSupport{
   if (_restarting) {
     // The auth flow is restarting, so the work here would be performed in the next round.
@@ -602,55 +581,23 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
     return;
   }
 
-  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init];
-  authFlow.emmSupport = emmSupport;
+  GIDAuthorizationResponseHandler *responseHandler =
+      [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:authorizationResponse
+                                                                  emmSupport:emmSupport
+                                                                    flowName:GIDFlowNameSignIn
+                                                               configuration:
+                                                                 _currentOptions.configuration
+                                                                       error:error];
+  GIDAuthorizationResponseHelper *responseHelper =
+      [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler];
 
-  if (authorizationResponse) {
-    if (authorizationResponse.authorizationCode.length) {
-      authFlow.authState = [[OIDAuthState alloc]
-          initWithAuthorizationResponse:authorizationResponse];
-      // perform auth code exchange
-      [self maybeFetchToken:authFlow];
-    } else {
-      // There was a failure, convert to appropriate error code.
-      NSString *errorString;
-      GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
-      NSDictionary<NSString *, NSObject *> *params = authorizationResponse.additionalParameters;
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-      if (authFlow.emmSupport) {
-        [authFlow wait];
-        BOOL isEMMError = [[GIDEMMErrorHandler sharedInstance]
-            handleErrorFromResponse:params
-                         completion:^{
-                           [authFlow next];
-                         }];
-        if (isEMMError) {
-          errorCode = kGIDSignInErrorCodeEMM;
-        }
-      }
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-      errorString = (NSString *)params[kOAuth2ErrorKeyName];
-      if ([errorString isEqualToString:kOAuth2AccessDenied]) {
-        errorCode = kGIDSignInErrorCodeCanceled;
-      }
+  GIDAuthFlow *authFlow = [responseHelper fetchAuthFlowFromProcessedResponse];
 
-      authFlow.error = [self errorWithString:errorString code:errorCode];
-    }
-  } else {
-    NSString *errorString = [error localizedDescription];
-    GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
-    if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) {
-      // The user has canceled the flow at the iOS modal dialog.
-      errorString = kUserCanceledError;
-      errorCode = kGIDSignInErrorCodeCanceled;
-    }
-    authFlow.error = [self errorWithString:errorString code:errorCode];
+  if (authFlow) {
+    [self addDecodeIdTokenCallback:authFlow];
+    [self addSaveAuthCallback:authFlow];
+    [self addCompletionCallback:authFlow];
   }
-
-  [self addDecodeIdTokenCallback:authFlow];
-  [self addSaveAuthCallback:authFlow];
-  [self addCompletionCallback:authFlow];
 }
 
 // Perform authentication with the provided options.
@@ -680,77 +627,26 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   }
 
   // Complete the auth flow using saved auth in keychain.
-  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init];
-  authFlow.authState = authState;
-  [self maybeFetchToken:authFlow];
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:authState
+                                                           error:nil
+                                                      emmSupport:nil
+                                                     profileData:nil];
+  GIDAuthorizationResponseHandler *responseHandler =
+      [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:nil
+                                                                  emmSupport:nil
+                                                                    flowName:GIDFlowNameSignIn
+                                                               configuration:
+                                                                 _currentOptions.configuration
+                                                                       error:nil];
+  GIDAuthorizationResponseHelper *responseHelper =
+      [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler];
+
+  [responseHelper fetchTokenWithAuthFlow:authFlow];
   [self addDecodeIdTokenCallback:authFlow];
   [self addSaveAuthCallback:authFlow];
   [self addCompletionCallback:authFlow];
 }
 
-// Fetches the access token if necessary as part of the auth flow.
-- (void)maybeFetchToken:(GIDAuthFlow *)authFlow {
-  OIDAuthState *authState = authFlow.authState;
-  // Do nothing if we have an auth flow error or a restored access token that isn't near expiration.
-  if (authFlow.error ||
-      (authState.lastTokenResponse.accessToken &&
-        [authState.lastTokenResponse.accessTokenExpirationDate timeIntervalSinceNow] >
-        kMinimumRestoredAccessTokenTimeToExpire)) {
-    return;
-  }
-  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
-  if (_currentOptions.configuration.serverClientID) {
-    additionalParameters[kAudienceParameter] = _currentOptions.configuration.serverClientID;
-  }
-  if (_currentOptions.configuration.openIDRealm) {
-    additionalParameters[kOpenIDRealmParameter] = _currentOptions.configuration.openIDRealm;
-  }
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  NSDictionary<NSString *, NSObject *> *params =
-      authState.lastAuthorizationResponse.additionalParameters;
-  NSString *passcodeInfoRequired = (NSString *)params[kEMMPasscodeInfoRequiredKeyName];
-  [additionalParameters addEntriesFromDictionary:
-      [GIDEMMSupport parametersWithParameters:@{}
-                                   emmSupport:authFlow.emmSupport
-                       isPasscodeInfoRequired:passcodeInfoRequired.length > 0]];
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
-  additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
-
-  OIDTokenRequest *tokenRequest;
-  if (!authState.lastTokenResponse.accessToken &&
-      authState.lastAuthorizationResponse.authorizationCode) {
-    tokenRequest = [authState.lastAuthorizationResponse
-        tokenExchangeRequestWithAdditionalParameters:additionalParameters];
-  } else {
-    [additionalParameters
-        addEntriesFromDictionary:authState.lastTokenResponse.request.additionalParameters];
-    tokenRequest = [authState tokenRefreshRequestWithAdditionalParameters:additionalParameters];
-  }
-
-  [authFlow wait];
-  [OIDAuthorizationService
-      performTokenRequest:tokenRequest
-                 callback:^(OIDTokenResponse *_Nullable tokenResponse,
-                            NSError *_Nullable error) {
-    [authState updateWithTokenResponse:tokenResponse error:error];
-    authFlow.error = error;
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-    if (authFlow.emmSupport) {
-      [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *error) {
-        authFlow.error = error;
-        [authFlow next];
-      }];
-    } else {
-      [authFlow next];
-    }
-#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
-    [authFlow next];
-#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
-  }];
-}
-
 // Adds a callback to the auth flow to save the auth object to |self| and the keychain as well.
 - (void)addSaveAuthCallback:(GIDAuthFlow *)authFlow {
   __weak GIDAuthFlow *weakAuthFlow = authFlow;

+ 18 - 0
GoogleSignIn/Sources/GIDSignInConstants.h

@@ -39,3 +39,21 @@ extern NSString *const kLoginHintParameter;
 
 /// The name of the hosted domain parameter for the auth and token exchange endpoints.
 extern NSString *const kHostedDomainParameter;
+
+/// The name of the error key that occurred during the authorization or token exchange process.
+extern NSString *const kOAuth2ErrorKeyName;
+
+/// The value of the error key when the user cancels the authorization or token exchange process.
+extern NSString *const kOAuth2AccessDenied;
+
+/// The name of the key that indicates whether a passocde is required for EMM.
+extern NSString *const kEMMPasscodeInfoRequiredKeyName;
+
+/// The error string for user cancelation in the sign-in flow.
+extern NSString *const kUserCanceledSignInError;
+
+/// The error string for user cancelation in the verify flow.
+extern NSString *const kUserCanceledVerifyError;
+
+/// Minimum time to expiration for a restored access token.
+extern const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire;

+ 9 - 0
GoogleSignIn/Sources/GIDSignInConstants.m

@@ -27,3 +27,12 @@ NSString *const kOpenIDRealmParameter = @"openid.realm";
 NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes";
 NSString *const kLoginHintParameter = @"login_hint";
 NSString *const kHostedDomainParameter = @"hd";
+
+NSString *const kOAuth2ErrorKeyName = @"error";
+NSString *const kOAuth2AccessDenied = @"access_denied";
+NSString *const kEMMPasscodeInfoRequiredKeyName = @"emm_passcode_info_required";
+
+NSString *const kUserCanceledSignInError = @"The user canceled the sign-in flow.";
+NSString *const kUserCanceledVerifyError = @"The user canceled the verification flow.";
+
+const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;

+ 48 - 9
GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m

@@ -20,27 +20,41 @@
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiedAccountDetailResult.h"
 
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h"
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h"
+
+#import "GoogleSignIn/Sources/GIDAuthFlow.h"
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
 #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
 #import "GoogleSignIn/Sources/GIDSignInConstants.h"
 #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
 
-@import GTMAppAuth;
-
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
-@import GTMSessionFetcherCore;
 #else
 #import <AppAuth/OIDAuthorizationRequest.h>
+#import <AppAuth/OIDAuthorizationResponse.h>
+#import <AppAuth/OIDAuthorizationService.h>
+#import <AppAuth/OIDExternalUserAgentSession.h>
 #import <AppAuth/OIDResponseTypes.h>
 #import <AppAuth/OIDServiceConfiguration.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+#import <AppAuth/OIDAuthorizationService+IOS.h>
+#endif
+
 #endif
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
+// TODO: Unify error domain across sign-in and verify flow (#425).
+NSErrorDomain const kGIDVerifyErrorDomain = @"com.google.GIDVerifyAccountDetail";
+
 @implementation GIDVerifyAccountDetail {
   /// AppAuth configuration object.
   OIDServiceConfiguration *_appAuthConfiguration;
+  /// AppAuth external user-agent session state.
+  id<OIDExternalUserAgentSession> _currentAuthorizationFlow;
 }
 
 - (instancetype)initWithConfig:(GIDConfiguration *)configuration {
@@ -48,10 +62,11 @@
   if (self) {
     _configuration = configuration;
 
-    NSString *authorizationEndpointURL = [NSString stringWithFormat:kAuthorizationURLTemplate,
-        [GIDSignInPreferences googleAuthorizationServer]];
-    NSString *tokenEndpointURL = [NSString stringWithFormat:kTokenURLTemplate,
-        [GIDSignInPreferences googleTokenServer]];
+    NSString *authorizationEndpointURL = 
+        [NSString stringWithFormat:kAuthorizationURLTemplate,
+         [GIDSignInPreferences googleAuthorizationServer]];
+    NSString *tokenEndpointURL =
+        [NSString stringWithFormat:kTokenURLTemplate, [GIDSignInPreferences googleTokenServer]];
     _appAuthConfiguration = [[OIDServiceConfiguration alloc]
         initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEndpointURL]
                         tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]];
@@ -148,6 +163,7 @@
   if (options.configuration.hostedDomain) {
     additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain;
   }
+
   additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
   additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
 
@@ -159,14 +175,37 @@
     }
   }
 
-  // TODO(#405): Use request variable to present request and process response.
-  __unused OIDAuthorizationRequest *request =
+  OIDAuthorizationRequest *request =
       [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
                                                     clientId:options.configuration.clientID
                                                       scopes:scopes
                                                  redirectURL:redirectURL
                                                 responseType:OIDResponseTypeCode
                                         additionalParameters:additionalParameters];
+
+   _currentAuthorizationFlow = [OIDAuthorizationService
+       presentAuthorizationRequest:request
+          presentingViewController:options.presentingViewController
+                          callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
+                                     NSError *_Nullable error) {
+     [self processAuthorizationResponse:authorizationResponse
+                                  error:error];
+  }];
+}
+
+- (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse
+                               error:(NSError *)error {
+  GIDAuthorizationResponseHandler *responseHandler =
+      [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:authorizationResponse
+                                                                  emmSupport:nil
+                                                                    flowName:GIDFlowNameVerifyAccountDetail
+                                                               configuration:_configuration
+                                                                       error:error];
+  GIDAuthorizationResponseHelper *responseHelper =
+      [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler];
+
+  // TODO: Add completion callback method (#413).
+  __unused GIDAuthFlow *authFlow = [responseHelper fetchAuthFlowFromProcessedResponse];
 }
 
 #pragma mark - Helpers

+ 11 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h

@@ -32,6 +32,17 @@ NS_ASSUME_NONNULL_BEGIN
 @class GIDVerifiableAccountDetail;
 @class GIDVerifiedAccountDetailResult;
 
+/// The error domain for `NSError`s returned by the Google Sign-In SDK.
+extern NSErrorDomain const kGIDVerifyErrorDomain;
+
+/// A list of potential error codes returned from the Google Sign-In SDK.
+typedef NS_ERROR_ENUM(kGIDVerifyErrorDomain, GIDVerifyErrorCode) {
+  /// Indicates an unknown error has occurred.
+  GIDVerifyErrorCodeUnknown = 0,
+  /// Indicates the user canceled the verification request.
+  GIDVerifyErrorCodeCanceled = 1,
+};
+
 /// Represents a completion block that takes a `GIDVerifiedAccountDetailResult` on success or an
 /// error if the operation was unsuccessful.
 typedef void (^GIDVerifyCompletion)(GIDVerifiedAccountDetailResult *_Nullable verifiedResult,

+ 54 - 0
GoogleSignIn/Tests/Unit/GIDAuthFlowTest.m

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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/GIDAuthFlow.h"
+
+#import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
+
+@interface GIDAuthFlowTest : XCTestCase
+@end
+
+@implementation GIDAuthFlowTest
+
+- (void)testInitWithAuthState {
+  OIDAuthState *authState = [OIDAuthState testInstance];
+  GIDProfileData *profileData = [GIDProfileData testInstance];
+
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:authState
+                                                           error:nil
+                                                      emmSupport:nil
+                                                     profileData:profileData];
+
+  XCTAssertNotNil(authFlow);
+  XCTAssertEqual(authFlow.authState, authState);
+  XCTAssertNil(authFlow.error);
+  XCTAssertNil(authFlow.emmSupport);
+  XCTAssertEqual(authFlow.profileData, profileData);
+}
+
+- (void)testInit {
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init];
+
+  XCTAssertNotNil(authFlow);
+  XCTAssertNil(authFlow.authState);
+  XCTAssertNil(authFlow.error);
+  XCTAssertNil(authFlow.emmSupport);
+  XCTAssertNil(authFlow.profileData);
+}
+
+@end

+ 185 - 0
GoogleSignIn/Tests/Unit/GIDAuthorizationResponseHelperTest.m

@@ -0,0 +1,185 @@
+/*
+ * Copyright 2024 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/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h"
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h"
+#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
+#import "GoogleSignIn/Sources/GIDAuthFlow.h"
+
+#import "GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
+
+/// The EMM support version
+static NSString *const kEMMVersion = @"1";
+
+/// Time interval to use for an expiring access token.
+static NSTimeInterval kAccessTokenExpiration = 20;
+
+@interface GIDAuthorizationResponseHelperTest : XCTestCase
+@end
+
+@implementation GIDAuthorizationResponseHelperTest
+
+- (void)testInitWithAuthorizationResponseHandler {
+  GIDAuthorizationResponseHandler *responseHandler =
+      [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:nil
+                                                                  emmSupport:nil
+                                                                    flowName:GIDFlowNameVerifyAccountDetail
+                                                               configuration:nil
+                                                                       error:nil];
+  GIDAuthorizationResponseHelper *responseHelper = 
+      [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler];
+
+  XCTAssertNotNil(responseHelper);
+  XCTAssertNotNil(responseHelper.responseHandler);
+}
+
+- (void)testFetchTokenWithAuthFlow {
+  OIDTokenResponse *tokenResponse = 
+      [OIDTokenResponse testInstanceWithAccessTokenExpiration:@(kAccessTokenExpiration)];
+  OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse];
+
+  GIDAuthorizationResponseHandlingFake *responseHandler =
+      [[GIDAuthorizationResponseHandlingFake alloc] initWithAuthState:authState error:nil];
+  GIDAuthorizationResponseHelper *responseHelper =
+      [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler];
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:authState 
+                                                           error:nil
+                                                      emmSupport:nil
+                                                     profileData:nil];
+
+  [responseHelper fetchTokenWithAuthFlow:authFlow];
+  XCTAssertNotNil(authFlow);
+  XCTAssertEqual(authFlow.authState, authState);
+  XCTAssertNil(authFlow.error);
+}
+
+- (void)testSuccessfulGenerateAuthFlowFromAuthorizationResponse {
+  OIDTokenResponse *tokenResponse = 
+      [OIDTokenResponse testInstanceWithAccessTokenExpiration:@(kAccessTokenExpiration)];
+  OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse];
+
+  GIDAuthorizationResponseHandlingFake *responseHandler =
+      [[GIDAuthorizationResponseHandlingFake alloc] initWithAuthState:authState error:nil];
+
+  GIDAuthFlow *authFlow = [responseHandler generateAuthFlowFromAuthorizationResponse];
+  XCTAssertNotNil(authFlow);
+  XCTAssertEqual(authFlow.authState, authState);
+  XCTAssertNil(authFlow.error);
+}
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+- (void)testGenerateAuthFlowFromAuthorizationResponse_noCode {
+  NSDictionary<NSString *, NSString *> *errorDict =
+      @{ NSLocalizedDescriptionKey : @"Unknown error" };
+  NSError *expectedError = [NSError errorWithDomain:kGIDVerifyErrorDomain
+                                               code:GIDVerifyErrorCodeUnknown
+                                           userInfo:errorDict];
+
+  OIDAuthorizationResponse *authorizationResponse = 
+      [OIDAuthorizationResponse testInstanceNoAuthCodeWithAdditionalParameters:nil 
+                                                                   errorString:nil];
+  GIDAuthorizationResponseHandler *responseHandler =
+      [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:authorizationResponse
+                                                                  emmSupport:nil
+                                                                    flowName:GIDFlowNameVerifyAccountDetail
+                                                               configuration:nil
+                                                                       error:nil];
+
+  GIDAuthFlow *authFlow = [responseHandler generateAuthFlowFromAuthorizationResponse];
+  XCTAssertNotNil(authFlow);
+  XCTAssertNil(authFlow.authState);
+  XCTAssertNotNil(authFlow.error);
+  XCTAssertEqualObjects(authFlow.error, expectedError);
+}
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+- (void)testGenerateAuthFlowWithMissingAuthorizationResponse {
+  NSError *error = [NSError errorWithDomain:kGIDVerifyErrorDomain
+                                       code:GIDVerifyErrorCodeUnknown
+                                   userInfo:nil];
+
+  NSString *errorString = [error localizedDescription];
+  NSDictionary<NSString *, NSString *> *errorDict = @{ NSLocalizedDescriptionKey : errorString };
+  NSError *expectedError = [NSError errorWithDomain:kGIDVerifyErrorDomain
+                                               code:GIDVerifyErrorCodeUnknown
+                                           userInfo:errorDict];
+
+  GIDAuthorizationResponseHandler *responseHandler =
+      [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:nil
+                                                                  emmSupport:nil
+                                                                    flowName:GIDFlowNameVerifyAccountDetail
+                                                               configuration:nil
+                                                                       error:error];
+
+  GIDAuthFlow *authFlow = [responseHandler generateAuthFlowFromAuthorizationResponse];
+  XCTAssertNotNil(authFlow);
+  XCTAssertNil(authFlow.authState);
+  XCTAssertNotNil(authFlow.error);
+  XCTAssertEqualObjects(authFlow.error, expectedError);
+}
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+- (void)testMaybeFetchToken_authFlowError {
+  NSError *expectedError = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                               code:kGIDSignInErrorCodeUnknown
+                                           userInfo:nil];
+
+  OIDTokenResponse *tokenResponse = 
+      [OIDTokenResponse testInstanceWithAccessTokenExpiration:@(kAccessTokenExpiration)];
+  OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse];
+
+  GIDAuthorizationResponseHandlingFake *responseHandler = 
+      [[GIDAuthorizationResponseHandlingFake alloc] initWithAuthState:authState error:nil];
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:nil
+                                                           error:expectedError
+                                                      emmSupport:nil
+                                                     profileData:nil];
+
+  [responseHandler maybeFetchToken:authFlow];
+  XCTAssertNil(authFlow.authState);
+  XCTAssertNotNil(authFlow.error);
+  XCTAssertEqualObjects(authFlow.error, expectedError);
+}
+
+- (void)testMaybeFetchToken_noRefresh {
+  NSError *expectedError = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                               code:kGIDSignInErrorCodeUnknown
+                                           userInfo:nil];
+
+  OIDAuthState *authState = [OIDAuthState testInstance];
+  GIDAuthorizationResponseHandlingFake *responseHandler = 
+      [[GIDAuthorizationResponseHandlingFake alloc] initWithAuthState:authState error:nil];
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:authState
+                                                           error:expectedError
+                                                      emmSupport:nil
+                                                     profileData:nil];
+
+  [responseHandler maybeFetchToken:authFlow];
+  XCTAssertNotNil(authFlow.authState);
+  XCTAssertEqualObjects(authFlow.authState, authState);
+  XCTAssertNotNil(authFlow.error);
+  XCTAssertEqualObjects(authFlow.error, expectedError);
+}
+
+@end

+ 4 - 0
GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h

@@ -28,4 +28,8 @@
     (NSDictionary<NSString *, NSString *> *)additionalParameters
                                          errorString:(NSString *)errorString;
 
++ (instancetype)testInstanceNoAuthCodeWithAdditionalParameters:
+    (NSDictionary<NSString *, NSString *> *)additionalParameters
+                                                   errorString:(NSString *)errorString;
+
 @end

+ 18 - 0
GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m

@@ -49,4 +49,22 @@
                                                 parameters:parameters];
 }
 
++ (instancetype)testInstanceNoAuthCodeWithAdditionalParameters:
+    (NSDictionary<NSString *, NSString *> *)additionalParameters
+                                                   errorString:(NSString *)errorString {
+  NSMutableDictionary<NSString *, NSString *> *parameters;
+  if (errorString) {
+    parameters = [NSMutableDictionary dictionaryWithDictionary:@{ @"error" : errorString }];
+  } else {
+    parameters = [NSMutableDictionary dictionaryWithDictionary:@{
+      @"code" : @"",
+    }];
+    if (additionalParameters) {
+      [parameters addEntriesFromDictionary:additionalParameters];
+    }
+  }
+  return [[OIDAuthorizationResponse alloc] initWithRequest:[OIDAuthorizationRequest testInstance]
+                                                parameters:parameters];
+}
+
 @end

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

@@ -53,6 +53,8 @@ extern NSString * const kFatPictureURL;
 
 + (instancetype)testInstanceWithIDToken:(NSString *)idToken;
 
++ (instancetype)testInstanceWithAccessTokenExpiration:(NSNumber *)expiration;
+
 + (instancetype)testInstanceWithIDToken:(NSString *)idToken
                             accessToken:(NSString *)accessToken
                               expiresIn:(NSNumber *)expiresIn

+ 8 - 0
GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m

@@ -65,6 +65,14 @@ NSString * const kFatPictureURL = @"fake_user_picture_url";
                                       tokenRequest:nil];
 }
 
++ (instancetype)testInstanceWithAccessTokenExpiration:(NSNumber *)expiration {
+  return [OIDTokenResponse testInstanceWithIDToken:[self idToken]
+                                       accessToken:nil
+                                         expiresIn:expiration
+                                      refreshToken:nil
+                                      tokenRequest:nil];
+}
+
 + (instancetype)testInstanceWithIDToken:(NSString *)idToken
                             accessToken:(NSString *)accessToken
                               expiresIn:(NSNumber *)expiresIn