Преглед изворни кода

Decompose GIDSignIn into interfaces and constituent parts

The changes here are rough, but are intended to show a potential direction for breaking up GIDSignIn and to make it easier to test.
Matt Mathias пре 1 година
родитељ
комит
e22cdc5e58

+ 367 - 0
GoogleSignIn/Sources/GIDAuthorization.m

@@ -0,0 +1,367 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthorization.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
+
+#import "GoogleSignIn/Sources/GIDAuthorizationFlow/GIDAuthorizationFlow.h"
+#import "GoogleSignIn/Sources/GIDAuthorization_Private.h"
+#import "GoogleSignIn/Sources/GIDConfiguration_Private.h"
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
+#import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
+#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+#import "GoogleSignIn/Sources/GIDSignInResult_Private.h"
+
+@import GTMAppAuth;
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/OIDAuthState.h>
+#endif
+
+// TODO: Put these constants in a constants file/class
+static NSString *const kGSIServiceName = @"gsi_auth";
+/// The EMM support version
+static NSString *const kEMMVersion = @"1";
+
+// Parameters for the auth and token exchange endpoints.
+static NSString *const kAudienceParameter = @"audience";
+static NSString *const kOpenIDRealmParameter = @"openid.realm";
+static NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes";
+static NSString *const kLoginHintParameter = @"login_hint";
+static NSString *const kHostedDomainParameter = @"hd";
+
+/// Expected path in the URL scheme to be handled.
+static NSString *const kBrowserCallbackPath = @"/oauth2callback";
+/// The URL template for the authorization endpoint.
+static NSString *const kAuthorizationURLTemplate = @"https://%@/o/oauth2/v2/auth";
+/// The URL template for the token endpoint.
+static NSString *const kTokenURLTemplate = @"https://%@/token";
+
+@interface GIDAuthorization ()
+
+@property(nonatomic, readonly) OIDServiceConfiguration *appAuthConfiguration;
+
+@end
+
+@implementation GIDAuthorization
+
+#pragma mark - Initialization
+
+- (instancetype)init {
+  NSBundle *mainBundle = [NSBundle mainBundle];
+  GIDConfiguration *defaultConfiguration = [GIDConfiguration configurationFromBundle:mainBundle];
+  return [self initWithConfiguration:defaultConfiguration];
+}
+
+- (instancetype)initWithConfiguration:(GIDConfiguration *)configuration {
+  GTMKeychainStore *keychainStore = [[GTMKeychainStore alloc] initWithItemName:kGSIServiceName];
+  return [self initWithKeychainStore:keychainStore configuration:configuration];
+}
+
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
+  return [self initWithKeychainStore:keychainStore configuration:nil];
+}
+
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
+                        configuration:(nullable GIDConfiguration *)configuration {
+  return [self initWithKeychainStore:keychainStore
+                       configuration:configuration
+        authorizationFlowCoordinator:nil];
+}
+
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
+                        configuration:(nullable GIDConfiguration *)configuration
+         authorizationFlowCoordinator:(nullable id<GIDAuthorizationFlowCoordinator>)authFlow {
+  self = [super init];
+  if (self) {
+    _keychainStore = keychainStore;
+    NSBundle *mainBundle = [NSBundle mainBundle];
+    GIDConfiguration *defaultConfiguration = [GIDConfiguration configurationFromBundle:mainBundle];
+    _currentConfiguration = configuration ?: defaultConfiguration;
+    _authFlow = authFlow;
+    
+    NSString *authorizationEnpointURL = [NSString stringWithFormat:kAuthorizationURLTemplate,
+                                         [GIDSignInPreferences googleAuthorizationServer]];
+    NSString *tokenEndpointURL = [NSString stringWithFormat:kTokenURLTemplate,
+                                  [GIDSignInPreferences googleTokenServer]];
+    _appAuthConfiguration = [[OIDServiceConfiguration alloc]
+                              initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEnpointURL]
+                                              tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]];
+  }
+  return self;
+}
+
+#pragma mark - Restoring Previous Sign Ins
+
+- (BOOL)hasPreviousSignIn {
+  if (self.currentUser) {
+    return self.currentUser.authState.isAuthorized;
+  }
+  OIDAuthState *authState = [self loadAuthState];
+  return authState.isAuthorized;
+}
+
+#pragma mark - Load Previous Authorization State
+
+- (nullable OIDAuthState *)loadAuthState {
+  GTMAuthSession *authSession = [self.keychainStore retrieveAuthSessionWithError:nil];
+  return authSession.authState;
+}
+
+#pragma mark - Signing In
+
+- (void)signInWithOptions:(GIDSignInInternalOptions *)options {
+  // Options for continuation are not the options we want to cache. The purpose of caching the
+  // options in the first place is to provide continuation flows with a starting place from which to
+  // derive suitable options for the continuation!
+  if (!options.continuation) {
+    self.currentOptions = options;
+  }
+  
+  if (options.interactive) {
+    // Ensure that a configuration has been provided.
+    if (!self.currentConfiguration) {
+      // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+      [NSException raise:NSInvalidArgumentException
+                  format:@"No active configuration. Make sure GIDClientID is set in Info.plist."];
+      return;
+    }
+    
+    // Explicitly throw exception for missing client ID here. This must come before
+    // scheme check because schemes rely on reverse client IDs.
+    [self assertValidParameters];
+    
+    [self assertValidPresentingController];
+    
+    // If the application does not support the required URL schemes tell the developer so.
+    GIDSignInCallbackSchemes *schemes =
+      [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
+    NSArray<NSString *> *unsupportedSchemes = [schemes unsupportedSchemes];
+    if (unsupportedSchemes.count != 0) {
+      // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+      [NSException raise:NSInvalidArgumentException
+                  format:@"Your app is missing support for the following URL schemes: %@",
+       [unsupportedSchemes componentsJoinedByString:@", "]];
+    }
+  }
+  
+  // If this is a non-interactive flow, use cached authentication if possible.
+  if (!options.interactive && self.currentUser) {
+    [self.currentUser refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *unused, NSError *error) {
+      if (error) {
+        [self authenticateWithOptions:options];
+      } else {
+        if (options.completion) {
+          self->_currentOptions = nil;
+          dispatch_async(dispatch_get_main_queue(), ^{
+            GIDSignInResult *signInResult =
+            [[GIDSignInResult alloc] initWithGoogleUser:self->_currentUser serverAuthCode:nil];
+            options.completion(signInResult, nil);
+          });
+        }
+      }
+    }];
+  } else {
+    [self authenticateWithOptions:options];
+  }
+}
+
+#pragma mark - Authorization Flow
+
+- (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];
+    return;
+  }
+
+  // Try retrieving an authorization object from the keychain.
+  OIDAuthState *authState = [self loadAuthState];
+
+  if (![authState isAuthorized]) {
+    // No valid auth in keychain, per documentation/spec, notify callback of failure.
+    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                         code:kGIDSignInErrorCodeHasNoAuthInKeychain
+                                     userInfo:nil];
+    if (options.completion) {
+      _currentOptions = nil;
+      dispatch_async(dispatch_get_main_queue(), ^{
+        options.completion(nil, error);
+      });
+    }
+    return;
+  }
+
+  self.authFlow = [[GIDAuthorizationFlow alloc] initWithSignInOptions:self.currentOptions
+                                                            authState:authState
+                                                          profileData:nil
+                                                           googleUser:self.currentUser
+                                             externalUserAgentSession:nil
+                                                           emmSupport:nil
+                                                                error:nil];
+  [self.authFlow maybeFetchToken];
+  [self.authFlow addDecodeIdTokenCallback];
+  [self.authFlow addSaveAuthCallback];
+  [self.authFlow addCompletionCallback];
+}
+
+- (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options {
+  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
+
+  [self authorizationRequestWithOptions:options
+                             completion:^(OIDAuthorizationRequest * _Nullable request,
+                                          NSError * _Nullable error) {
+    self->_authFlow.currentUserAgentSession =
+      [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) {
+        [self->_authFlow processAuthorizationResponse:authorizationResponse
+                                                error:error
+                                           emmSupport:emmSupport];
+    }];
+  }];
+}
+
+#pragma mark - Authorization Request
+
+- (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options
+                             completion:
+(void (^)(OIDAuthorizationRequest *_Nullable request, NSError *_Nullable error))completion {
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters =
+      [self additionalParametersFromOptions:options];
+  OIDAuthorizationRequest *request = [self authorizationRequestWithOptions:options
+                                                      additionalParameters:additionalParameters];
+  // TODO: Add app check steps as well
+  completion(request, nil);
+}
+
+- (OIDAuthorizationRequest *)
+authorizationRequestWithOptions:(GIDSignInInternalOptions *)options
+           additionalParameters:(NSDictionary<NSString *, NSString *> *)additionalParameters {
+  OIDAuthorizationRequest *request;
+  if (options.nonce) {
+    request = [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
+                                                            clientId:options.configuration.clientID
+                                                              scopes:options.scopes
+                                                         redirectURL:[self redirectURLWithOptions:options]
+                                                        responseType:OIDResponseTypeCode
+                                                               nonce:options.nonce
+                                                additionalParameters:additionalParameters];
+  } else {
+    request = [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
+                                                            clientId:options.configuration.clientID
+                                                              scopes:options.scopes
+                                                         redirectURL:[self redirectURLWithOptions:options]
+                                                        responseType:OIDResponseTypeCode
+                                                additionalParameters:additionalParameters];
+  }
+  return request;
+}
+
+- (NSMutableDictionary<NSString *, NSString *> *)
+    additionalParametersFromOptions:(GIDSignInInternalOptions *)options {
+  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 =
+      [[NSMutableDictionary alloc] init];
+  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();
+
+  return additionalParameters;
+}
+
+
+#pragma mark - Validity Assertions
+
+- (void)assertValidParameters {
+  if (![_currentOptions.configuration.clientID length]) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"You must specify `clientID` in `GIDConfiguration`"];
+  }
+}
+
+- (void)assertValidPresentingController {
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+  if (!self.currentOptions.presentingViewController)
+#elif TARGET_OS_OSX
+  if (!self.currentOptions.presentingWindow)
+#endif // TARGET_OS_OSX
+  {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"`presentingViewController` must be set."];
+  }
+}
+
+#pragma mark - Utilities
+
++ (BOOL)isOperatingSystemAtLeast9 {
+  NSProcessInfo *processInfo = [NSProcessInfo processInfo];
+  return [processInfo respondsToSelector:@selector(isOperatingSystemAtLeastVersion:)] &&
+      [processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 9}];
+}
+
+- (NSURL *)redirectURLWithOptions:(GIDSignInInternalOptions *)options {
+  GIDSignInCallbackSchemes *schemes =
+      [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
+  NSURL *redirectURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@",
+                                             [schemes clientIdentifierScheme],
+                                             kBrowserCallbackPath]];
+  return redirectURL;
+}
+
+@end

+ 49 - 0
GoogleSignIn/Sources/GIDAuthorizationFlow/GIDAuthorizationFlow.h

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2025 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"
+#import "GoogleSignIn/Sources/GIDAuthorizationFlow/GIDAuthorizationFlowCoordinator.h"
+
+@class GIDSignInInternalOptions;
+@class OIDAuthState;
+@class GIDProfileData;
+@class GIDGoogleUser;
+@protocol OIDExternalUserAgentSession;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDAuthorizationFlow : GIDCallbackQueue <GIDAuthorizationFlowCoordinator>
+
+@property(nonatomic, strong, nullable) OIDAuthState *authState;
+@property(nonatomic, strong, nullable) GIDProfileData *profileData;
+@property(nonatomic, strong, nullable) GIDSignInInternalOptions *options;
+@property(nonatomic, strong, nullable) GIDGoogleUser *googleUser;
+@property(nonatomic, strong, nullable) id<OIDExternalUserAgentSession> currentUserAgentSession;
+@property(nonatomic, copy, nullable) NSString *emmSupport;
+@property(nonatomic, strong, nullable) NSError *error;
+
+- (instancetype)initWithSignInOptions:(nullable GIDSignInInternalOptions *)options
+                            authState:(nullable OIDAuthState *)authState
+                          profileData:(nullable GIDProfileData *)profileData
+                           googleUser:(nullable GIDGoogleUser *)googleUser
+             externalUserAgentSession:(nullable id<OIDExternalUserAgentSession>)userAgentSession
+                           emmSupport:(nullable NSString *)emmSupport
+                                error:(nullable NSError *)error;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 162 - 0
GoogleSignIn/Sources/GIDAuthorizationFlow/GIDAuthorizationFlow.m

@@ -0,0 +1,162 @@
+/*
+ * Copyright 2025 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 "GIDAuthorizationFlow.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/OIDAuthState.h>
+#import <AppAuth/OIDAuthorizationResponse.h>
+#endif
+
+// TODO: Move these constants to their own spot
+#pragma mark - 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 user cancelations.
+static NSString *const kUserCanceledError = @"The user canceled the sign-in flow.";
+
+@interface GIDAuthorizationFlow ()
+
+@property(nonatomic) BOOL restarting;
+
+@end
+
+@implementation GIDAuthorizationFlow
+
+- (instancetype)initWithSignInOptions:(GIDSignInInternalOptions *)options
+                            authState:(OIDAuthState *)authState
+                          profileData:(nullable GIDProfileData *)profileData
+                           googleUser:(nullable GIDGoogleUser *)googleUser
+             externalUserAgentSession:(nullable id<OIDExternalUserAgentSession>)userAgentSession
+                           emmSupport:(nullable NSString *)emmSupport
+                                error:(nullable NSError *)error {
+  self = [super init];
+  if (self) {
+    _options = options;
+    _authState = authState;
+    _profileData = profileData;
+    _googleUser = googleUser;
+    _currentUserAgentSession = userAgentSession;
+    _error = error;
+    _emmSupport = emmSupport;
+  }
+  return self;
+}
+
+#pragma mark - Token Fetching
+
+- (void)maybeFetchToken {
+  
+}
+
+#pragma mark - Decode ID Token
+
+- (void)addDecodeIdTokenCallback {
+  
+}
+
+#pragma mark - Saving Authorization
+
+- (void)addSaveAuthCallback {
+  
+}
+
+#pragma mark - Completion Callback
+
+- (void)addCompletionCallback {
+  
+}
+
+#pragma mark - Authorization Response
+
+- (void)processAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse
+                               error:(nullable NSError *)error
+                          emmSupport:(nullable NSString *)emmSupport {
+  if (self.restarting) {
+    // The auth flow is restarting, so the work here would be performed in the next round.
+    self.restarting = NO;
+    return;
+  }
+
+  self.emmSupport = emmSupport;
+
+  if (authorizationResponse) {
+    if (authorizationResponse.authorizationCode.length) {
+      self.authState = [[OIDAuthState alloc] initWithAuthorizationResponse:authorizationResponse];
+      [self maybeFetchToken];
+    } 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 (self.emmSupport) {
+        [self wait];
+        BOOL isEMMError = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:params
+                                                                            completion:^{
+          [self next];
+        }];
+        if (isEMMError) {
+          errorCode = kGIDSignInErrorCodeEMM;
+        }
+      }
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+      errorString = (NSString *)params[kOAuth2ErrorKeyName];
+      if ([errorString isEqualToString:kOAuth2AccessDenied]) {
+        errorCode = kGIDSignInErrorCodeCanceled;
+      }
+
+      self.error = [self errorWithString:errorString code:errorCode];
+    }
+  } else {
+    NSString *errorString = [error localizedDescription];
+    GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
+    if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow ||
+        error.code == OIDErrorCodeProgramCanceledAuthorizationFlow) {
+      // The user has canceled the flow at the iOS modal dialog.
+      errorString = kUserCanceledError;
+      errorCode = kGIDSignInErrorCodeCanceled;
+    }
+    self.error = [self errorWithString:errorString code:errorCode];
+  }
+
+  [self addDecodeIdTokenCallback];
+  [self addSaveAuthCallback];
+  [self addCompletionCallback];
+}
+
+#pragma mark - Errors
+
+// TODO: Move this to its own type
+- (NSError *)errorWithString:(NSString *)errorString code:(GIDSignInErrorCode)code {
+  if (errorString == nil) {
+    errorString = @"Unknown error";
+  }
+  NSDictionary<NSString *, NSString *> *errorDict = @{ NSLocalizedDescriptionKey : errorString };
+  return [NSError errorWithDomain:kGIDSignInErrorDomain
+                             code:code
+                         userInfo:errorDict];
+}
+
+@end

+ 47 - 0
GoogleSignIn/Sources/GIDAuthorizationFlow/GIDAuthorizationFlowCoordinator.h

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025 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.
+ */
+
+@class OIDAuthState;
+@class GIDProfileData;
+@class GIDSignInInternalOptions;
+@class GIDGoogleUser;
+@class OIDAuthorizationResponse;
+
+@protocol OIDExternalUserAgentSession;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol GIDAuthorizationFlowCoordinator <NSObject>
+
+@property(nonatomic, strong, nullable) OIDAuthState *authState;
+@property(nonatomic, strong, nullable) GIDProfileData *profileData;
+@property(nonatomic, strong, nullable) GIDSignInInternalOptions *options;
+@property(nonatomic, strong, nullable) GIDGoogleUser *googleUser;
+@property(nonatomic, strong, nullable) id<OIDExternalUserAgentSession> currentUserAgentSession;
+@property(nonatomic, copy, nullable) NSString *emmSupport;
+@property(nonatomic, strong, nullable) NSError *error;
+
+- (void)maybeFetchToken;
+- (void)addDecodeIdTokenCallback;
+- (void)addSaveAuthCallback;
+- (void)addCompletionCallback;
+- (void)processAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse
+                               error:(nullable NSError *)error
+                          emmSupport:(nullable NSString *)emmSupport;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 73 - 0
GoogleSignIn/Sources/GIDAuthorization_Private.h

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 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 <TargetConditionals.h>
+
+@class GIDConfiguration;
+@class GTMKeychainStore;
+@class GIDSignInInternalOptions;
+@class GIDAuthorizationFlow;
+@protocol GIDAuthorizationFlowCoordinator;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDAuthorization ()
+
+/// Private initializer taking a `GTMKeychainStore`.
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore;
+
+/// Private initializer taking a `GTMKeychainStore` and a `GIDConfiguration`.
+///
+/// If `configuration` is nil, then a default configuration is generated from values in the `Info.plist`.
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
+                        configuration:(nullable GIDConfiguration *)configuration;
+
+/// Private initializer taking a `GTMKeychainStore`, `GIDConfiguration` and a `GIDAuthorizationFlowCoordinator`.
+///
+/// If `configuration` is nil, then a default configuration is generated from values in the `Info.plist`. If a nil
+/// `GIDAuthorizationFlowCoordinator` conforming instance is provided, then one will be created during the authorization flow.
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
+                        configuration:(nullable GIDConfiguration *)configuration
+         authorizationFlowCoordinator:(nullable id<GIDAuthorizationFlowCoordinator>)authFlow;
+
+//#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+// /// Private initializer taking a `GTMKeychainStore` and `GIDAppCheckProvider`.
+//- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
+//                             appCheck:(GIDAppCheck *)appCheck
+//API_AVAILABLE(ios(14));
+//#endif // TARGET_OS_IOS || !TARGET_OS_MACCATALYST
+
+/// Authenticates in with the provided options.
+- (void)signInWithOptions:(GIDSignInInternalOptions *)options;
+
+/// The current configuration used for authorization.
+@property(nonatomic, nullable) GIDConfiguration *currentConfiguration;
+
+/// Keychain manager for GTMAppAuth
+@property(nonatomic, readwrite) GTMKeychainStore *keychainStore;
+
+/// Options used when sign-in flows are resumed via the handling of a URL.
+///
+/// Options are set when a sign-in flow is begun via |signInWithOptions:| when the options passed don't represent a sign in continuation.
+@property(nonatomic, nullable) GIDSignInInternalOptions *currentOptions;
+
+/// The `GIDAuthorizationFlowCoordinator` conforming type managing the authorization flow.
+@property(nonatomic, readwrite) id<GIDAuthorizationFlowCoordinator> authFlow;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 36 - 0
GoogleSignIn/Sources/GIDConfiguration.m

@@ -26,6 +26,12 @@ static NSString *const kHostedDomainKey = @"hostedDomain";
 // The key for the openIDRealm property to be used with NSSecureCoding.
 static NSString *const kOpenIDRealmKey = @"openIDRealm";
 
+// Info.plist config keys
+static NSString *const kConfigClientIDKey = @"GIDClientID";
+static NSString *const kConfigServerClientIDKey = @"GIDServerClientID";
+static NSString *const kConfigHostedDomainKey = @"GIDHostedDomain";
+static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
+
 NS_ASSUME_NONNULL_BEGIN
 
 @implementation GIDConfiguration
@@ -108,6 +114,36 @@ NS_ASSUME_NONNULL_BEGIN
   [coder encodeObject:_openIDRealm forKey:kOpenIDRealmKey];
 }
 
++ (nullable NSString *)configValueFromBundle:(NSBundle *)bundle forKey:(NSString *)key {
+  NSString *value;
+  id configValue = [bundle objectForInfoDictionaryKey:key];
+  if ([configValue isKindOfClass:[NSString class]]) {
+    value = configValue;
+  }
+  return value;
+}
+
++ (nullable GIDConfiguration *)configurationFromBundle:(NSBundle *)bundle {
+  GIDConfiguration *configuration;
+
+  // Retrieve any valid config parameters from the bundle's Info.plist.
+  NSString *clientID = [self.class configValueFromBundle:bundle forKey:kConfigClientIDKey];
+  NSString *serverClientID = [self.class configValueFromBundle:bundle
+                                                       forKey:kConfigServerClientIDKey];
+  NSString *hostedDomain = [self.class configValueFromBundle:bundle forKey:kConfigHostedDomainKey];
+  NSString *openIDRealm = [self.class configValueFromBundle:bundle forKey:kConfigOpenIDRealmKey];
+    
+  // If we have at least a client ID, try to construct a configuration.
+  if (clientID) {
+    configuration = [[GIDConfiguration alloc] initWithClientID:clientID
+                                                 serverClientID:serverClientID
+                                                   hostedDomain:hostedDomain
+                                                    openIDRealm:openIDRealm];
+  }
+  
+  return configuration;
+}
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 31 - 0
GoogleSignIn/Sources/GIDConfiguration_Private.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDConfiguration ()
+
+/// Retrieves a configuration value from an `NSBundle`'s Info.plist for a given key.
++ (nullable NSString *)configValueFromBundle:(NSBundle *)bundle forKey:(NSString *)key;
+
+/// Generates a `GIDConfiguration` from an `NSBundle``s Info.plist.
++ (nullable GIDConfiguration *)configurationFromBundle:(NSBundle *)bundle;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 48 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthorization.h

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2025 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 GIDConfiguration;
+@class GIDGoogleUser;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDAuthorization : NSObject
+
+/// Initializer used to create an instance of `GIDAuthorization`.
+///
+/// This initializer will generate a default `GIDConfiguration` using values from your `Info.plist`.
+- (instancetype)init;
+
+/// Initializer used to create an instance of `GIDAuthorization`.
+///
+/// This initializer will generate a default `GIDConfiguration` using values from your `Info.plist`.
+- (instancetype)initWithConfiguration:(GIDConfiguration *)configuration;
+
+/// Checks if there is a current user or a previous sign in.
+///
+/// - Returns: A `BOOL` if there was a previous sign in.
+- (BOOL)hasPreviousSignIn;
+
+/// The current user managed by Google Sign-In.
+///
+/// - Note: The current user will be nil if there is no current user.
+@property(nonatomic, readonly, nullable) GIDGoogleUser *currentUser;
+
+@end
+
+NS_ASSUME_NONNULL_END

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

@@ -18,6 +18,7 @@
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 #import "GIDAppCheckError.h"
 #endif
+#import "GIDAuthorization.h"
 #import "GIDConfiguration.h"
 #import "GIDGoogleUser.h"
 #import "GIDProfileData.h"