Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions GoogleSignIn/Sources/GIDRestrictedScopesRegistry.h
Original file line number Diff line number Diff line change
@@ -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 changes: 52 additions & 0 deletions GoogleSignIn/Sources/GIDRestrictedScopesRegistry.m
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions GoogleSignIn/Sources/GIDSignIn.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -142,6 +145,8 @@ @implementation GIDSignIn {
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
}

Expand Down Expand Up @@ -256,6 +261,10 @@ - (void)addScopes:(NSArray<NSString *> *)scopes
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 =
Expand Down Expand Up @@ -499,6 +508,7 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
callbackPath:kBrowserCallbackPath
keychainName:kGTMAppAuthKeychainName
isFreshInstall:isFreshInstall];
_registry = [[GIDRestrictedScopesRegistry alloc] init];
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
}
return self;
Expand Down Expand Up @@ -989,6 +999,27 @@ - (void)assertValidPresentingViewController {
}
}

#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];
Expand Down
46 changes: 46 additions & 0 deletions GoogleSignIn/Tests/Unit/GIDRestrictedScopesRegistryTest.m
Original file line number Diff line number Diff line change
@@ -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 changes: 23 additions & 0 deletions GoogleSignIn/Tests/Unit/GIDSignInTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,29 @@ - (void)testTokenEndpointEMMError {
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];
Expand Down