Переглянути джерело

Add GIDAuthorizationUtil class. (#288)

pinlu 3 роки тому
батько
коміт
2138a3a863

+ 4 - 56
GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.m

@@ -16,12 +16,8 @@
 
 #import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h"
 
-#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
-
-#import "GoogleSignIn/Sources/GIDEMMSupport.h"
-#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+#import "GoogleSignIn/Sources/GIDAuthorizationUtil.h"
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
-#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
 
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
@@ -31,20 +27,10 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-// Parameters for the auth and token exchange endpoints.
-static NSString *const kAudienceParameter = @"audience";
-
-static NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes";
-static NSString *const kLoginHintParameter = @"login_hint";
-static NSString *const kHostedDomainParameter = @"hd";
-
 @interface GIDAuthorizationFlowProcessor ()
 
 /// AppAuth external user-agent session state.
-@property(nonatomic, nullable)id<OIDExternalUserAgentSession> currentAuthorizationFlow;
-
-/// AppAuth configuration object.
-@property(nonatomic)OIDServiceConfiguration *appAuthConfiguration;
+@property(nonatomic, nullable) id<OIDExternalUserAgentSession> currentAuthorizationFlow;
 
 @end
 
@@ -60,47 +46,9 @@ static NSString *const kHostedDomainParameter = @"hd";
               emmSupport:(nullable NSString *)emmSupport
               completion:(void (^)(OIDAuthorizationResponse *_Nullable authorizationResponse,
                                    NSError *_Nullable error))completion {
-  GIDSignInCallbackSchemes *schemes =
-      [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
-  NSString *urlString = [NSString stringWithFormat:@"%@:%@",
-      [schemes clientIdentifierScheme], kBrowserCallbackPath];
-  NSURL *redirectURL = [NSURL URLWithString:urlString];
-
-  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
-  additionalParameters[kIncludeGrantedScopesParameter] = @"true";
-  if (options.configuration.serverClientID) {
-    additionalParameters[kAudienceParameter] = options.configuration.serverClientID;
-  }
-  if (options.loginHint) {
-    additionalParameters[kLoginHintParameter] = options.loginHint;
-  }
-  if (options.configuration.hostedDomain) {
-    additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain;
-  }
-  
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  [additionalParameters addEntriesFromDictionary:
-      [GIDEMMSupport parametersWithParameters:options.extraParams
-                                   emmSupport:emmSupport
-                       isPasscodeInfoRequired:NO]];
-#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
-  [additionalParameters addEntriesFromDictionary:options.extraParams];
-#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
-  additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
-  additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
-  
-  NSURL *authorizationEndpointURL = [GIDSignInPreferences authorizationEndpointURL];
-  NSURL *tokenEndpointURL = [GIDSignInPreferences tokenEndpointURL];
-  OIDServiceConfiguration *appAuthConfiguration =
-      [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpointURL
-                                                       tokenEndpoint:tokenEndpointURL];
   OIDAuthorizationRequest *request =
-      [[OIDAuthorizationRequest alloc] initWithConfiguration:appAuthConfiguration
-                                                    clientId:options.configuration.clientID
-                                                      scopes:options.scopes
-                                                 redirectURL:redirectURL
-                                                responseType:OIDResponseTypeCode
-                                        additionalParameters:additionalParameters];
+      [GIDAuthorizationUtil authorizationRequestWithOptions:options
+                                                 emmSupport:emmSupport];
   
   _currentAuthorizationFlow = [OIDAuthorizationService
       presentAuthorizationRequest:request

+ 50 - 0
GoogleSignIn/Sources/GIDAuthorizationUtil.h

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class OIDAuthorizationRequest;
+@class GIDSignInInternalOptions;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// The util class for authorization process.
+@interface GIDAuthorizationUtil : NSObject
+
+/// Creates the request to AppAuth to start the authorization flow.
+///
+/// @param options The `GIDSignInInternalOptions` object to provide serverClientID, hostedDomain,
+///     clientID, scopes, loginHint and extraParams.
+/// @param emmSupport The EMM support info string.
++ (OIDAuthorizationRequest *)
+    authorizationRequestWithOptions:(GIDSignInInternalOptions *)options
+                         emmSupport:(nullable NSString *)emmSupport;
+
+/// Unions granted scopes with new scopes or returns an error if the new scopes are the subset of
+/// the granted scopes.
+///
+/// @param scopes The existing scopes.
+/// @param newScopes The new scopes to add.
+/// @param error The reference to the error.
+/// @return The array of all scopes or nil if there is an error.
++ (nullable NSArray<NSString *> *)
+    resolvedScopesFromGrantedScoped:(NSArray<NSString *> *)scopes
+                      withNewScopes:(NSArray<NSString *> *)newScopes
+                              error:(NSError * __autoreleasing *)error;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 106 - 0
GoogleSignIn/Sources/GIDAuthorizationUtil.m

@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "GoogleSignIn/Sources/GIDAuthorizationUtil.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+
+#import "GoogleSignIn/Sources/GIDEMMSupport.h"
+#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation GIDAuthorizationUtil
+
++ (OIDAuthorizationRequest *)
+    authorizationRequestWithOptions:(GIDSignInInternalOptions *)options
+                         emmSupport:(nullable NSString *)emmSupport {
+  GIDSignInCallbackSchemes *schemes =
+      [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
+  NSString *urlString = [NSString stringWithFormat:@"%@:%@",
+      [schemes clientIdentifierScheme], kBrowserCallbackPath];
+  NSURL *redirectURL = [NSURL URLWithString:urlString];
+
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
+  additionalParameters[kIncludeGrantedScopesParameter] = @"true";
+  if (options.configuration.serverClientID) {
+    additionalParameters[kAudienceParameter] = options.configuration.serverClientID;
+  }
+  if (options.loginHint) {
+    additionalParameters[kLoginHintParameter] = options.loginHint;
+  }
+  if (options.configuration.hostedDomain) {
+    additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain;
+  }
+  
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  [additionalParameters addEntriesFromDictionary:
+      [GIDEMMSupport parametersWithParameters:options.extraParams
+                                   emmSupport:emmSupport
+                       isPasscodeInfoRequired:NO]];
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+  [additionalParameters addEntriesFromDictionary:options.extraParams];
+#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
+  additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
+  additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
+  
+  NSURL *authorizationEndpointURL = [GIDSignInPreferences authorizationEndpointURL];
+  NSURL *tokenEndpointURL = [GIDSignInPreferences tokenEndpointURL];
+  OIDServiceConfiguration *appAuthConfiguration =
+      [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpointURL
+                                                       tokenEndpoint:tokenEndpointURL];
+  OIDAuthorizationRequest *request =
+      [[OIDAuthorizationRequest alloc] initWithConfiguration:appAuthConfiguration
+                                                    clientId:options.configuration.clientID
+                                                      scopes:options.scopes
+                                                 redirectURL:redirectURL
+                                                responseType:OIDResponseTypeCode
+                                        additionalParameters:additionalParameters];
+  
+  return request;
+}
+
++ (nullable NSArray<NSString *> *)
+    resolvedScopesFromGrantedScoped:(NSArray<NSString *> *)scopes
+                      withNewScopes:(NSArray<NSString *> *)newScopes
+                              error:(NSError * __autoreleasing *)error {
+  NSMutableSet<NSString *> *grantedScopes = [NSMutableSet setWithArray:scopes];
+  NSSet<NSString *> *requestedScopes = [NSSet setWithArray:newScopes];
+  
+  if ([requestedScopes isSubsetOfSet:grantedScopes]) {
+    // All requested scopes have already been granted, generate an error.
+    *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                 code:kGIDSignInErrorCodeScopesAlreadyGranted
+                             userInfo:nil];
+    return nil;
+  }
+  
+  // Use the union of granted and requested scopes.
+  [grantedScopes unionSet:requestedScopes];
+  return [grantedScopes allObjects];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 0 - 2
GoogleSignIn/Sources/GIDGoogleUser.m

@@ -42,8 +42,6 @@ static NSString *const kHostedDomainIDTokenClaimKey = @"hd";
 static NSString *const kProfileDataKey = @"profileData";
 static NSString *const kAuthStateKey = @"authState";
 
-// Parameters for the token exchange endpoint.
-static NSString *const kAudienceParameter = @"audience";
 static NSString *const kOpenIDRealmParameter = @"openid.realm";
 
 // Additional parameter names for EMM.

+ 19 - 27
GoogleSignIn/Sources/GIDSignIn.m

@@ -23,6 +23,7 @@
 
 #import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h"
 #import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h"
+#import "GoogleSignIn/Sources/GIDAuthorizationUtil.h"
 #import "GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHTTPFetcher.h"
 #import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h"
 #import "GoogleSignIn/Sources/GIDEMMSupport.h"
@@ -75,6 +76,9 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+// The EMM support version
+NSString *const kEMMVersion = @"1";
+
 // The name of the query parameter used for logging the restart of auth from EMM callback.
 static NSString *const kEMMRestartAuthParameter = @"emmres";
 
@@ -84,9 +88,6 @@ static NSString *const kRevokeTokenURLTemplate = @"https://%@/o/oauth2/revoke";
 // Expected path for EMM callback.
 static NSString *const kEMMCallbackPath = @"/emmcallback";
 
-// The EMM support version
-static NSString *const kEMMVersion = @"1";
-
 // The error code for Google Identity.
 NSErrorDomain const kGIDSignInErrorDomain = @"com.google.GIDSignIn";
 
@@ -111,8 +112,6 @@ static NSString *const kAppHasRunBeforeKey = @"GID_AppHasRunBefore";
 // The delay before the new sign-in flow can be presented after the existing one is cancelled.
 static const NSTimeInterval kPresentationDelayAfterCancel = 1.0;
 
-// Parameters for the auth and token exchange endpoints.
-static NSString *const kAudienceParameter = @"audience";
 // See b/11669751 .
 static NSString *const kOpenIDRealmParameter = @"openid.realm";
 
@@ -260,24 +259,12 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 - (void)addScopes:(NSArray<NSString *> *)scopes
     presentingViewController:(UIViewController *)presentingViewController
                   completion:(nullable GIDSignInCompletion)completion {
-  GIDConfiguration *configuration = self.currentUser.configuration;
-  GIDSignInInternalOptions *options =
-      [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
-                                       presentingViewController:presentingViewController
-                                                      loginHint:self.currentUser.profile.email
-                                                  addScopesFlow:YES
-                                                     completion:completion];
-
-  NSSet<NSString *> *requestedScopes = [NSSet setWithArray:scopes];
-  NSMutableSet<NSString *> *grantedScopes =
-      [NSMutableSet setWithArray:self.currentUser.grantedScopes];
-
-  // Check to see if all requested scopes have already been granted.
-  if ([requestedScopes isSubsetOfSet:grantedScopes]) {
-    // All requested scopes have already been granted, notify callback of failure.
-    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
-                                         code:kGIDSignInErrorCodeScopesAlreadyGranted
-                                     userInfo:nil];
+  NSError *error;
+  NSArray<NSString *> *allScopes =
+      [GIDAuthorizationUtil resolvedScopesFromGrantedScoped:self.currentUser.grantedScopes
+                                              withNewScopes:scopes
+                                                      error:&error];
+  if (error) {
     if (completion) {
       dispatch_async(dispatch_get_main_queue(), ^{
         completion(nil, error);
@@ -285,10 +272,15 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     }
     return;
   }
-
-  // Use the union of granted and requested scopes.
-  [grantedScopes unionSet:requestedScopes];
-  options.scopes = [grantedScopes allObjects];
+  
+  GIDConfiguration *configuration = self.currentUser.configuration;
+  GIDSignInInternalOptions *options =
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
+                                       presentingViewController:presentingViewController
+                                                      loginHint:self.currentUser.profile.email
+                                                  addScopesFlow:YES
+                                                     completion:completion];
+  options.scopes = allScopes;
 
   [self signInWithOptions:options];
 }

+ 9 - 0
GoogleSignIn/Sources/GIDSignInPreferences.h

@@ -24,6 +24,15 @@ extern NSString *const kSDKVersionLoggingParameter;
 /// The name of the query parameter used for logging the Apple execution environment.
 extern NSString *const kEnvironmentLoggingParameter;
 
+/// The name of the query parameter for the token exchange endpoint.
+extern NSString *const kAudienceParameter;
+
+extern NSString *const kIncludeGrantedScopesParameter;
+
+extern NSString *const kLoginHintParameter;
+
+extern NSString *const kHostedDomainParameter;
+
 /// Expected path in the URL scheme to be handled.
 extern NSString *const kBrowserCallbackPath;
 

+ 6 - 0
GoogleSignIn/Sources/GIDSignInPreferences.m

@@ -16,8 +16,14 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+/// Parameters for the auth and token exchange endpoints.
+NSString *const kAudienceParameter = @"audience";
+NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes";
+NSString *const kLoginHintParameter = @"login_hint";
+NSString *const kHostedDomainParameter = @"hd";
 NSString *const kSDKVersionLoggingParameter = @"gpsdk";
 NSString *const kEnvironmentLoggingParameter = @"gidenv";
+
 NSString *const kBrowserCallbackPath = @"/oauth2callback";
 
 static NSString *const kLSOServer = @"accounts.google.com";

+ 5 - 2
GoogleSignIn/Sources/GIDSignIn_Private.h

@@ -24,8 +24,6 @@
 #import <AppKit/AppKit.h>
 #endif
 
-NS_ASSUME_NONNULL_BEGIN
-
 @class GIDGoogleUser;
 @class GIDSignInInternalOptions;
 
@@ -34,6 +32,11 @@ NS_ASSUME_NONNULL_BEGIN
 @protocol GIDKeychainHandler;
 @protocol GIDProfileDataFetcher;
 
+NS_ASSUME_NONNULL_BEGIN
+
+/// The EMM support version.
+extern NSString *const kEMMVersion;
+
 /// Represents a completion block that takes a `GIDSignInResult` on success or an error if the
 /// operation was unsuccessful.
 typedef void (^GIDSignInCompletion)(GIDSignInResult *_Nullable signInResult,

+ 205 - 0
GoogleSignIn/Tests/Unit/GIDAuthorizationUtilTest.m

@@ -0,0 +1,205 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <TargetConditionals.h>
+
+#import <XCTest/XCTest.h>
+
+#import "GoogleSignIn/Sources/GIDAuthorizationUtil.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+
+#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString * const kClientId = @"FakeClientID";
+static NSString * const kUserEmail = @"FakeUserEmail";
+static NSString * const kServerClientId = @"FakeServerClientID";
+
+static NSString * const kScopeBirthday = @"birthday";
+static NSString * const kScopeEmail = @"email";
+static NSString * const kScopeProfile = @"profile";
+
+@interface GIDAuthorizationUtilTest : XCTestCase
+
+@end
+
+@implementation GIDAuthorizationUtilTest {
+  GIDConfiguration *_configuration;
+}
+
+- (void)setUp {
+  [super setUp];
+  _configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
+                                               serverClientID:kServerClientId
+                                                 hostedDomain:kHostedDomain
+                                                  openIDRealm:nil];
+}
+
+- (void)testCreateAuthorizationRequest_signInFlow {
+  GIDSignInInternalOptions *options =
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                                       presentingViewController:nil
+#elif TARGET_OS_OSX
+                                               presentingWindow:nil
+#endif // TARGET_OS_OSX
+                                                      loginHint:kUserEmail
+                                                  addScopesFlow:NO
+                                                     completion:nil];
+  OIDAuthorizationRequest *request =
+      [GIDAuthorizationUtil authorizationRequestWithOptions:options
+                                                 emmSupport:nil];
+  
+  NSDictionary<NSString *, NSObject *> *params = request.additionalParameters;
+  XCTAssertEqualObjects(params[kIncludeGrantedScopesParameter], @"true");
+  XCTAssertEqualObjects(params[kSDKVersionLoggingParameter], GIDVersion());
+  XCTAssertEqualObjects(params[kEnvironmentLoggingParameter], GIDEnvironment());
+  XCTAssertEqualObjects(params[kLoginHintParameter], kUserEmail, @"login hint should match");
+  XCTAssertEqualObjects(params[kHostedDomainParameter], kHostedDomain,
+                        @"hosted domain should match");
+  XCTAssertEqualObjects(params[kAudienceParameter], kServerClientId, @"client ID should match");
+  
+  NSArray<NSString *> *defaultScopes = @[kScopeEmail, kScopeProfile];
+  NSString *expectedScopeString = [defaultScopes componentsJoinedByString:@" "];
+  XCTAssertEqualObjects(request.scope, expectedScopeString);
+}
+
+- (void)testCreateAuthorizationRequest_additionalScopes {
+  NSArray<NSString *> *addtionalScopes = @[kScopeBirthday];
+  GIDSignInInternalOptions *options =
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                                       presentingViewController:nil
+#elif TARGET_OS_OSX
+                                               presentingWindow:nil
+#endif // TARGET_OS_OSX
+                                                      loginHint:kUserEmail
+                                                  addScopesFlow:NO
+                                                         scopes:addtionalScopes
+                                                     completion:nil];
+  OIDAuthorizationRequest *request =
+      [GIDAuthorizationUtil authorizationRequestWithOptions:options
+                                                 emmSupport:nil];
+  
+  NSArray<NSString *> *expectedScopes = @[kScopeBirthday, kScopeEmail, kScopeProfile];
+  NSString *expectedScopeString = [expectedScopes componentsJoinedByString:@" "];
+  XCTAssertEqualObjects(request.scope, expectedScopeString);
+}
+
+- (void)testCreateAuthrizationRequest_addScopes {
+  NSArray<NSString *> *scopes = @[kScopeEmail, kScopeProfile, kScopeBirthday];
+  GIDSignInInternalOptions *options =
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                                       presentingViewController:nil
+#elif TARGET_OS_OSX
+                                               presentingWindow:nil
+#endif // TARGET_OS_OSX
+                                                      loginHint:kUserEmail
+                                                  addScopesFlow:YES
+                                                         scopes:scopes
+                                                     completion:nil];
+  
+  OIDAuthorizationRequest *request =
+      [GIDAuthorizationUtil authorizationRequestWithOptions:options
+                                                 emmSupport:nil];
+  
+  NSString *expectedScopeString = [scopes componentsJoinedByString:@" "];
+  XCTAssertEqualObjects(request.scope, expectedScopeString);
+}
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+- (void)testCreateAuthorizationRequest_signInFlow_EMM {
+  GIDSignInInternalOptions *options =
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                                       presentingViewController:nil
+#elif TARGET_OS_OSX
+                                               presentingWindow:nil
+#endif // TARGET_OS_OSX
+                                                      loginHint:kUserEmail
+                                                  addScopesFlow:NO
+                                                     completion:nil];
+  OIDAuthorizationRequest *request =
+      [GIDAuthorizationUtil authorizationRequestWithOptions:options
+                                                 emmSupport:kEMMVersion];
+  
+  NSString *systemName = [UIDevice currentDevice].systemName;
+  if ([systemName isEqualToString:@"iPhone OS"]) {
+    systemName = @"iOS";
+  }
+  NSString *expectedOSVersion = [NSString stringWithFormat:@"%@ %@",
+      systemName, [UIDevice currentDevice].systemVersion];
+  NSDictionary<NSString *, NSObject *> *authParams = request.additionalParameters;
+  
+  BOOL isEligibleForEMM = [UIDevice currentDevice].systemVersion.integerValue >= 9;
+  if (isEligibleForEMM) {
+    XCTAssertEqualObjects(authParams[@"emm_support"], kEMMVersion,
+                          @"EMM support should match in auth request");
+    XCTAssertEqualObjects(authParams[@"device_os"], expectedOSVersion,
+                          @"OS version should match in auth request");
+  } else {
+    XCTAssertNil(authParams[@"emm_support"],
+                 @"EMM support should not be in auth request for unsupported OS");
+    XCTAssertNil(authParams[@"device_os"],
+                 @"OS version should not be in auth request for unsupported OS");
+  }
+}
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+- (void)testUnionScopes_success {
+  NSArray<NSString *> *scopes = @[kScopeEmail, kScopeProfile];
+  NSArray<NSString *> *newScopes = @[kScopeBirthday];
+  
+  NSError *error;
+  NSArray<NSString *> *allScopes =
+      [GIDAuthorizationUtil resolvedScopesFromGrantedScoped:scopes
+                                              withNewScopes:newScopes
+                                                      error:&error];
+  
+  NSArray<NSString *> *expectedScopes = @[kScopeEmail, kScopeProfile, kScopeBirthday];
+  XCTAssertEqualObjects(allScopes, expectedScopes);
+  XCTAssertNil(error);
+}
+
+- (void)testUnionScopes_addExistingScopes_error {
+  NSArray<NSString *> *scopes = @[kScopeEmail, kScopeProfile, kScopeBirthday];
+  NSArray<NSString *> *newScopes = @[kScopeBirthday];
+  
+  NSError *error;
+  NSArray<NSString *> *allScopes =
+      [GIDAuthorizationUtil resolvedScopesFromGrantedScoped:scopes
+                                              withNewScopes:newScopes
+                                                      error:&error];
+  
+  XCTAssertNil(allScopes);
+  XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain);
+  XCTAssertEqual(error.code, kGIDSignInErrorCodeScopesAlreadyGranted);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 11 - 210
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -93,8 +93,6 @@ static NSString * const kDotReversedClientId = @"FakeClientID";
 static NSString * const kClientId2 = @"FakeClientID2";
 static NSString * const kServerClientId = @"FakeServerClientID";
 static NSString * const kLanguage = @"FakeLanguage";
-static NSString * const kScope = @"FakeScope";
-static NSString * const kScope2 = @"FakeScope2";
 static NSString * const kAuthCode = @"FakeAuthCode";
 static NSString * const kKeychainName = @"auth";
 static NSString * const kUserEmail = @"FakeUserEmail";
@@ -135,13 +133,6 @@ static NSString * const kAppName = @"UnitTests";
 static NSString * const kUserIDKey = @"userID";
 static NSString * const kHostedDomainKey = @"hostedDomain";
 static NSString * const kIDTokenExpirationKey = @"idTokenExp";
-static NSString * const kScopeKey = @"scope";
-
-// Basic profile (Fat ID Token / userinfo endpoint) keys
-static NSString *const kBasicProfilePictureKey = @"picture";
-static NSString *const kBasicProfileNameKey = @"name";
-static NSString *const kBasicProfileGivenNameKey = @"given_name";
-static NSString *const kBasicProfileFamilyNameKey = @"family_name";
 
 static NSString * const kCustomKeychainName = @"CUSTOM_KEYCHAIN_NAME";
 static NSString * const kAddActivity = @"http://schemas.google.com/AddActivity";
@@ -154,9 +145,6 @@ static NSString *const kTokenURL = @"https://oauth2.googleapis.com/token";
 
 static NSString *const kFakeURL = @"http://foo.com";
 
-static NSString *const kEMMSupport = @"1";
-
-static NSString *const kGrantedScope = @"grantedScope";
 static NSString *const kNewScope = @"newScope";
 
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
@@ -243,9 +231,6 @@ static NSString *const kNewScope = @"newScope";
   // The completion to be used when testing `GIDSignIn`.
   GIDSignInCompletion _completion;
 
-  // The saved authorization request.
-  OIDAuthorizationRequest *_savedAuthorizationRequest;
-
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   // The saved presentingViewController from the authorization request.
   UIViewController *_savedPresentingViewController;
@@ -295,11 +280,11 @@ static NSString *const kNewScope = @"newScope";
   _user = OCMStrictClassMock([GIDGoogleUser class]);
   _oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]);
   OCMStub([_oidAuthorizationService
-      presentAuthorizationRequest:SAVE_TO_ARG_BLOCK(self->_savedAuthorizationRequest)
+      presentAuthorizationRequest:OCMOCK_ANY
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
-           presentingViewController:SAVE_TO_ARG_BLOCK(self->_savedPresentingViewController)
+         presentingViewController:SAVE_TO_ARG_BLOCK(self->_savedPresentingViewController)
 #elif TARGET_OS_OSX
-           presentingWindow:SAVE_TO_ARG_BLOCK(self->_savedPresentingWindow)
+                 presentingWindow:SAVE_TO_ARG_BLOCK(self->_savedPresentingWindow)
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
                          callback:COPY_TO_ARG_BLOCK(self->_savedAuthorizationCallback)]);
   OCMStub([self->_oidAuthorizationService
@@ -527,108 +512,6 @@ static NSString *const kNewScope = @"newScope";
                         modalCancel:NO];
 }
 
-- (void)testOAuthLogin_AdditionalScopes {
-  NSString *expectedScopeString;
-  
-  [self OAuthLoginWithAddScopesFlow:NO
-                          authError:nil
-                         tokenError:nil
-            emmPasscodeInfoRequired:NO
-                      keychainError:NO
-                     restoredSignIn:NO
-                     oldAccessToken:NO
-                        modalCancel:NO
-                useAdditionalScopes:YES
-                   additionalScopes:nil];
-
-  expectedScopeString = [@[ @"email", @"profile" ] componentsJoinedByString:@" "];
-  XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
-
-  [self OAuthLoginWithAddScopesFlow:NO
-                          authError:nil
-                         tokenError:nil
-            emmPasscodeInfoRequired:NO
-                      keychainError:NO
-                     restoredSignIn:NO
-                     oldAccessToken:NO
-                        modalCancel:NO
-                useAdditionalScopes:YES
-                   additionalScopes:@[ kScope ]];
-
-  expectedScopeString = [@[ kScope, @"email", @"profile" ] componentsJoinedByString:@" "];
-  XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
-
-  [self OAuthLoginWithAddScopesFlow:NO
-                          authError:nil
-                         tokenError:nil
-            emmPasscodeInfoRequired:NO
-                      keychainError:NO
-                     restoredSignIn:NO
-                     oldAccessToken:NO
-                        modalCancel:NO
-                useAdditionalScopes:YES
-                   additionalScopes:@[ kScope, kScope2 ]];
-
-  expectedScopeString = [@[ kScope, kScope2, @"email", @"profile" ] componentsJoinedByString:@" "];
-  XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
-}
-
-- (void)testAddScopes {
-  // Restore the previous sign-in account. This is the preparation for adding scopes.
-  [self OAuthLoginWithAddScopesFlow:NO
-                          authError:nil
-                         tokenError:nil
-            emmPasscodeInfoRequired:NO
-                      keychainError:NO
-                     restoredSignIn:YES
-                     oldAccessToken:NO
-                        modalCancel:NO];
-
-  XCTAssertNotNil(_signIn.currentUser);
-
-  id profile = OCMStrictClassMock([GIDProfileData class]);
-  OCMStub([profile email]).andReturn(kUserEmail);
-  
-  // Mock for the method `addScopes`.
-  GIDConfiguration *configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
-                                                                serverClientID:nil
-                                                                  hostedDomain:nil
-                                                                   openIDRealm:kOpenIDRealm];
-  OCMStub([_user configuration]).andReturn(configuration);
-  OCMStub([_user profile]).andReturn(profile);
-  OCMStub([_user grantedScopes]).andReturn(@[kGrantedScope]);
-
-  [self OAuthLoginWithAddScopesFlow:YES
-                          authError:nil
-                         tokenError:nil
-            emmPasscodeInfoRequired:NO
-                      keychainError:NO
-                     restoredSignIn:NO
-                     oldAccessToken:NO
-                        modalCancel:NO];
-
-  NSArray<NSString *> *grantedScopes;
-  NSString *grantedScopeString = _savedAuthorizationRequest.scope;
-
-  if (grantedScopeString) {
-    grantedScopeString = [grantedScopeString stringByTrimmingCharactersInSet:
-        [NSCharacterSet whitespaceCharacterSet]];
-    // Tokenize with space as a delimiter.
-    NSMutableArray<NSString *> *parsedScopes =
-        [[grantedScopeString componentsSeparatedByString:@" "] mutableCopy];
-    // Remove empty strings.
-    [parsedScopes removeObject:@""];
-    grantedScopes = [parsedScopes copy];
-  }
-  
-  NSArray<NSString *> *expectedScopes = @[kNewScope, kGrantedScope];
-  XCTAssertEqualObjects(grantedScopes, expectedScopes);
-
-  [_user verify];
-  [profile verify];
-  [profile stopMocking];
-}
-
 - (void)testOpenIDRealm {
   _signIn.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
                                                       serverClientID:nil
@@ -648,41 +531,6 @@ static NSString *const kNewScope = @"newScope";
   XCTAssertEqual(params[kOpenIDRealmKey], kOpenIDRealm, @"OpenID Realm should match.");
 }
 
-- (void)testOAuthLogin_LoginHint {
-  _hint = kUserEmail;
-
-  [self OAuthLoginWithAddScopesFlow:NO
-                          authError:nil
-                         tokenError:nil
-            emmPasscodeInfoRequired:NO
-                      keychainError:NO
-                     restoredSignIn:NO
-                     oldAccessToken:NO
-                        modalCancel:NO];
-
-  NSDictionary<NSString *, NSObject *> *params = _savedAuthorizationRequest.additionalParameters;
-  XCTAssertEqualObjects(params[@"login_hint"], kUserEmail, @"login hint should match");
-}
-
-- (void)testOAuthLogin_HostedDomain {
-  _signIn.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
-                                                      serverClientID:nil
-                                                        hostedDomain:kHostedDomain
-                                                         openIDRealm:nil];
-
-  [self OAuthLoginWithAddScopesFlow:NO
-                          authError:nil
-                         tokenError:nil
-            emmPasscodeInfoRequired:NO
-                      keychainError:NO
-                     restoredSignIn:NO
-                     oldAccessToken:NO
-                        modalCancel:NO];
-
-  NSDictionary<NSString *, NSObject *> *params = _savedAuthorizationRequest.additionalParameters;
-  XCTAssertEqualObjects(params[@"hd"], kHostedDomain, @"hosted domain should match");
-}
-
 - (void)testOAuthLogin_ConsentCanceled {
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:@"access_denied"
@@ -1011,15 +859,10 @@ static NSString *const kNewScope = @"newScope";
   }
   NSString *expectedOSVersion = [NSString stringWithFormat:@"%@ %@",
       systemName, [UIDevice currentDevice].systemVersion];
-  NSDictionary<NSString *, NSObject *> *authParams =
-      _savedAuthorizationRequest.additionalParameters;
+
   NSDictionary<NSString *, NSString *> *tokenParams = _savedTokenRequest.additionalParameters;
   if (_isEligibleForEMM) {
-    XCTAssertEqualObjects(authParams[@"emm_support"], kEMMSupport,
-                          @"EMM support should match in auth request");
-    XCTAssertEqualObjects(authParams[@"device_os"], expectedOSVersion,
-                          @"OS version should match in auth request");
-    XCTAssertEqualObjects(tokenParams[@"emm_support"], kEMMSupport,
+    XCTAssertEqualObjects(tokenParams[@"emm_support"], kEMMVersion,
                           @"EMM support should match in token request");
     XCTAssertEqualObjects(tokenParams[@"device_os"],
                           expectedOSVersion,
@@ -1027,10 +870,6 @@ static NSString *const kNewScope = @"newScope";
     XCTAssertNil(tokenParams[@"emm_passcode_info"],
                  @"no passcode info should be in token request");
   } else {
-    XCTAssertNil(authParams[@"emm_support"],
-                 @"EMM support should not be in auth request for unsupported OS");
-    XCTAssertNil(authParams[@"device_os"],
-                 @"OS version should not be in auth request for unsupported OS");
     XCTAssertNil(tokenParams[@"emm_support"],
                  @"EMM support should not be in token request for unsupported OS");
     XCTAssertNil(tokenParams[@"device_os"],
@@ -1158,26 +997,6 @@ static NSString *const kNewScope = @"newScope";
   XCTAssertEqualObjects(strings[1], token);
 }
 
-- (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
-                          authError:(NSString *)authError
-                         tokenError:(NSError *)tokenError
-            emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired
-                      keychainError:(BOOL)keychainError
-                     restoredSignIn:(BOOL)restoredSignIn
-                     oldAccessToken:(BOOL)oldAccessToken
-                        modalCancel:(BOOL)modalCancel {
-  [self OAuthLoginWithAddScopesFlow:addScopesFlow
-                          authError:authError
-                         tokenError:tokenError
-            emmPasscodeInfoRequired:emmPasscodeInfoRequired
-                      keychainError:keychainError
-                     restoredSignIn:restoredSignIn
-                     oldAccessToken:oldAccessToken
-                        modalCancel:modalCancel
-                useAdditionalScopes:NO
-                   additionalScopes:nil];
-}
-
 // The authorization flow with parameters to control which branches to take.
 - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
                           authError:(NSString *)authError
@@ -1186,9 +1005,7 @@ static NSString *const kNewScope = @"newScope";
                       keychainError:(BOOL)keychainError
                      restoredSignIn:(BOOL)restoredSignIn
                      oldAccessToken:(BOOL)oldAccessToken
-                        modalCancel:(BOOL)modalCancel
-                useAdditionalScopes:(BOOL)useAdditionalScopes
-                   additionalScopes:(NSArray *)additionalScopes {
+                        modalCancel:(BOOL)modalCancel {
   if (restoredSignIn) {
     // clearAndAuthenticateWithOptions
     [_keychainHandler saveAuthState:_authState];
@@ -1267,33 +1084,17 @@ static NSString *const kNewScope = @"newScope";
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
               completion:completion];
     } else {
-      if (useAdditionalScopes) {
-#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
-        [_signIn signInWithPresentingViewController:_presentingViewController
-#elif TARGET_OS_OSX
-        [_signIn signInWithPresentingWindow:_presentingWindow
-#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
-                                       hint:_hint
-                           additionalScopes:additionalScopes
-                                 completion:completion];
-      } else {
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
-        [_signIn signInWithPresentingViewController:_presentingViewController
+      [_signIn signInWithPresentingViewController:_presentingViewController
 #elif TARGET_OS_OSX
-        [_signIn signInWithPresentingWindow:_presentingWindow
+      [_signIn signInWithPresentingWindow:_presentingWindow
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
-                                       hint:_hint
-                                 completion:completion];
-      }
+                                     hint:_hint
+                               completion:completion];
     }
 
     [_authState verify];
-
-    XCTAssertNotNil(_savedAuthorizationRequest);
-    NSDictionary<NSString *, NSObject *> *params = _savedAuthorizationRequest.additionalParameters;
-    XCTAssertEqualObjects(params[@"include_granted_scopes"], @"true");
-    XCTAssertEqualObjects(params[kSDKVersionLoggingParameter], GIDVersion());
-    XCTAssertEqualObjects(params[kEnvironmentLoggingParameter], GIDEnvironment());
+    
     XCTAssertNotNil(_savedAuthorizationCallback);
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
     XCTAssertEqual(_savedPresentingViewController, _presentingViewController);