Jelajahi Sumber

Throw an exception if verified age scope is passed in sign-in request through the add scopes flow. (#473)

Brianna Morales 1 tahun lalu
induk
melakukan
ae6faa956d

+ 53 - 0
GoogleSignIn/Sources/GIDRestrictedScopesRegistry.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 <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <Foundation/Foundation.h>
+
+/// A registry to manage restricted scopes and their associated handling classes to track scopes
+/// that require separate flows within an application.
+@interface GIDRestrictedScopesRegistry : NSObject
+
+/// A set of strings representing the restricted scopes.
+@property (nonatomic, strong, readonly) NSSet<NSString *> *restrictedScopes;
+
+/// A dictionary mapping restricted scopes to their corresponding handling classes.
+@property (nonatomic, strong, readonly) NSDictionary<NSString *, Class> *scopeToClassMapping;
+
+/// This designated initializer sets up the initial restricted scopes and their corresponding handling classes.
+///
+/// @return An initialized `GIDRestrictedScopesRegistry` instance
+- (instancetype)init;
+
+/// Checks if a given scope is restricted.
+///
+/// @param scope The scope to check.
+/// @return YES if the scope is restricted; otherwise, NO.
+- (BOOL)isScopeRestricted:(NSString *)scope;
+
+/// Retrieves a dictionary mapping restricted scopes to their handling classes within a given set of scopes.
+///
+/// @param scopes A set of scopes to lookup their handling class.
+/// @return A dictionary where restricted scopes found in the input set are mapped to their corresponding handling classes.
+///     If no restricted scopes are found, an empty dictionary is returned.
+- (NSDictionary<NSString *, Class> *)restrictedScopesToClassMappingInSet:(NSSet<NSString *> *)scopes;
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 52 - 0
GoogleSignIn/Sources/GIDRestrictedScopesRegistry.m

@@ -0,0 +1,52 @@
+// 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/GIDRestrictedScopesRegistry.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+@implementation GIDRestrictedScopesRegistry
+
+- (instancetype)init {
+  self = [super init];
+  if (self) {
+    _restrictedScopes = [NSSet setWithObjects:kAccountDetailTypeAgeOver18Scope, nil];
+    _scopeToClassMapping = @{
+      kAccountDetailTypeAgeOver18Scope: [GIDVerifyAccountDetail class],
+    };
+  }
+  return self;
+}
+
+- (BOOL)isScopeRestricted:(NSString *)scope {
+  return [self.restrictedScopes containsObject:scope];
+}
+
+- (NSDictionary<NSString *, Class> *)restrictedScopesToClassMappingInSet:(NSSet<NSString *> *)scopes {
+  NSMutableDictionary<NSString *, Class> *mapping = [NSMutableDictionary dictionary];
+  for (NSString *scope in scopes) {
+    if ([self isScopeRestricted:scope]) {
+      Class handlingClass = self.scopeToClassMapping[scope];
+      mapping[scope] = handlingClass;
+    }
+  }
+  return [mapping copy];
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 31 - 0
GoogleSignIn/Sources/GIDSignIn.m

@@ -20,12 +20,15 @@
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.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/GIDRestrictedScopesRegistry.h"
 #import "GoogleSignIn/Sources/GIDSignInConstants.h"
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
 #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
@@ -142,6 +145,8 @@ static NSString *const kClientAssertionTypeParameterValue =
   GIDTimedLoader *_timedLoader;
   // Flag indicating developer's intent to use App Check.
   BOOL _configureAppCheckCalled;
+  // The class used to manage restricted scopes and their associated handling classes.
+  GIDRestrictedScopesRegistry *_registry;
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 }
 
@@ -256,6 +261,10 @@ static NSString *const kClientAssertionTypeParameterValue =
                                                       loginHint:self.currentUser.profile.email
                                                   addScopesFlow:YES
                                                      completion:completion];
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  // Explicitly throw an exception for invalid or restricted scopes in the request.
+  [self assertValidScopes:scopes];
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
   NSSet<NSString *> *requestedScopes = [NSSet setWithArray:scopes];
   NSMutableSet<NSString *> *grantedScopes =
@@ -499,6 +508,7 @@ static NSString *const kClientAssertionTypeParameterValue =
                               callbackPath:kBrowserCallbackPath
                               keychainName:kGTMAppAuthKeychainName
                             isFreshInstall:isFreshInstall];
+    _registry = [[GIDRestrictedScopesRegistry alloc] init];
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
   }
   return self;
@@ -989,6 +999,27 @@ static NSString *const kClientAssertionTypeParameterValue =
   }
 }
 
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+- (void)assertValidScopes:(NSArray<NSString *> *)scopes {
+  NSDictionary<NSString *, Class> *restrictedScopesMapping =
+      [_registry restrictedScopesToClassMappingInSet:[NSSet setWithArray:scopes]];
+
+  if (restrictedScopesMapping.count > 0) {
+    NSMutableString *errorMessage =
+    [NSMutableString stringWithString:@"The following scopes are not supported in the 'addScopes' flow. "
+                                       "Please use the appropriate classes to handle these:\n"];
+    [restrictedScopesMapping enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull restrictedScope,
+                                                                 Class  _Nonnull handlingClass,
+                                                                 BOOL * _Nonnull stop) {
+      [errorMessage appendFormat:@"%@ -> %@\n", restrictedScope, NSStringFromClass(handlingClass)];
+    }];
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"%@", errorMessage];
+  }
+}
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
 // Checks whether or not this is the first time the app runs.
 - (BOOL)isFreshInstall {
   NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];

+ 46 - 0
GoogleSignIn/Tests/Unit/GIDRestrictedScopesRegistryTest.m

@@ -0,0 +1,46 @@
+// 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>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+#import "GoogleSignIn/Sources/GIDRestrictedScopesRegistry.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
+
+@interface GIDRestrictedScopesRegistryTest : XCTestCase
+@end
+
+@implementation GIDRestrictedScopesRegistryTest
+
+- (void)testIsScopeRestricted {
+  GIDRestrictedScopesRegistry *registry = [[GIDRestrictedScopesRegistry alloc] init];
+  BOOL isRestricted = [registry isScopeRestricted:kAccountDetailTypeAgeOver18Scope];
+  XCTAssertTrue(isRestricted);
+}
+
+- (void)testRestrictedScopesToClassMappingInSet {
+  GIDRestrictedScopesRegistry *registry = [[GIDRestrictedScopesRegistry alloc] init];
+  NSSet<NSString *> *scopes = [NSSet setWithObjects:kAccountDetailTypeAgeOver18Scope, @"some_other_scope", nil];
+  NSDictionary<NSString *, Class> *mapping = [registry restrictedScopesToClassMappingInSet:scopes];
+  
+  XCTAssertEqual(mapping.count, 1);
+  XCTAssertEqualObjects(mapping[kAccountDetailTypeAgeOver18Scope], [GIDVerifyAccountDetail class]);
+  XCTAssertNil(mapping[@"some_other_scope"]);
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 23 - 0
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -1300,6 +1300,29 @@ static NSString *const kNewScope = @"newScope";
   XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain);
   XCTAssertEqual(_authError.code, kGIDSignInErrorCodeEMM);
   XCTAssertNil(_signIn.currentUser, @"should not have current user");
+}
+
+- (void)testValidScopesException {
+  NSString *requestedScope = @"https://www.googleapis.com/auth/verified.age.over18.standard";
+  NSString *expectedException = 
+    [NSString stringWithFormat:@"The following scopes are not supported in the 'addScopes' flow. "
+                                "Please use the appropriate classes to handle these:\n%@ -> %@\n",
+                                requestedScope, NSStringFromClass([GIDVerifyAccountDetail class])];
+  BOOL threw = NO;
+  @try {
+    [_signIn addScopes:@[requestedScope]
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+      presentingViewController:_presentingViewController
+#elif TARGET_OS_OSX
+      presentingWindow:_presentingWindow
+#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
+            completion:_completion];
+  } @catch (NSException *exception) {
+    threw = YES;
+    XCTAssertEqualObjects(exception.description, expectedException);
+  } @finally {
+  }
+  XCTAssert(threw);
 
   // TODO: Keep mocks from carrying forward to subsequent tests. (#410)
   [_authState stopMocking];