Skip to content

Commit ae6faa9

Browse files
authored
Throw an exception if verified age scope is passed in sign-in request through the add scopes flow. (#473)
1 parent c54b610 commit ae6faa9

5 files changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <TargetConditionals.h>
18+
19+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
20+
21+
#import <Foundation/Foundation.h>
22+
23+
/// A registry to manage restricted scopes and their associated handling classes to track scopes
24+
/// that require separate flows within an application.
25+
@interface GIDRestrictedScopesRegistry : NSObject
26+
27+
/// A set of strings representing the restricted scopes.
28+
@property (nonatomic, strong, readonly) NSSet<NSString *> *restrictedScopes;
29+
30+
/// A dictionary mapping restricted scopes to their corresponding handling classes.
31+
@property (nonatomic, strong, readonly) NSDictionary<NSString *, Class> *scopeToClassMapping;
32+
33+
/// This designated initializer sets up the initial restricted scopes and their corresponding handling classes.
34+
///
35+
/// @return An initialized `GIDRestrictedScopesRegistry` instance
36+
- (instancetype)init;
37+
38+
/// Checks if a given scope is restricted.
39+
///
40+
/// @param scope The scope to check.
41+
/// @return YES if the scope is restricted; otherwise, NO.
42+
- (BOOL)isScopeRestricted:(NSString *)scope;
43+
44+
/// Retrieves a dictionary mapping restricted scopes to their handling classes within a given set of scopes.
45+
///
46+
/// @param scopes A set of scopes to lookup their handling class.
47+
/// @return A dictionary where restricted scopes found in the input set are mapped to their corresponding handling classes.
48+
/// If no restricted scopes are found, an empty dictionary is returned.
49+
- (NSDictionary<NSString *, Class> *)restrictedScopesToClassMappingInSet:(NSSet<NSString *> *)scopes;
50+
51+
@end
52+
53+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#import "GoogleSignIn/Sources/GIDRestrictedScopesRegistry.h"
16+
17+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h"
18+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
19+
20+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
21+
22+
@implementation GIDRestrictedScopesRegistry
23+
24+
- (instancetype)init {
25+
self = [super init];
26+
if (self) {
27+
_restrictedScopes = [NSSet setWithObjects:kAccountDetailTypeAgeOver18Scope, nil];
28+
_scopeToClassMapping = @{
29+
kAccountDetailTypeAgeOver18Scope: [GIDVerifyAccountDetail class],
30+
};
31+
}
32+
return self;
33+
}
34+
35+
- (BOOL)isScopeRestricted:(NSString *)scope {
36+
return [self.restrictedScopes containsObject:scope];
37+
}
38+
39+
- (NSDictionary<NSString *, Class> *)restrictedScopesToClassMappingInSet:(NSSet<NSString *> *)scopes {
40+
NSMutableDictionary<NSString *, Class> *mapping = [NSMutableDictionary dictionary];
41+
for (NSString *scope in scopes) {
42+
if ([self isScopeRestricted:scope]) {
43+
Class handlingClass = self.scopeToClassMapping[scope];
44+
mapping[scope] = handlingClass;
45+
}
46+
}
47+
return [mapping copy];
48+
}
49+
50+
@end
51+
52+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

GoogleSignIn/Sources/GIDSignIn.m

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
2121
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
2222
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h"
23+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h"
24+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
2325

2426
#import "GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h"
2527
#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h"
2628

2729
#import "GoogleSignIn/Sources/GIDAuthFlow.h"
2830
#import "GoogleSignIn/Sources/GIDEMMSupport.h"
31+
#import "GoogleSignIn/Sources/GIDRestrictedScopesRegistry.h"
2932
#import "GoogleSignIn/Sources/GIDSignInConstants.h"
3033
#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
3134
#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
@@ -142,6 +145,8 @@ @implementation GIDSignIn {
142145
GIDTimedLoader *_timedLoader;
143146
// Flag indicating developer's intent to use App Check.
144147
BOOL _configureAppCheckCalled;
148+
// The class used to manage restricted scopes and their associated handling classes.
149+
GIDRestrictedScopesRegistry *_registry;
145150
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
146151
}
147152

@@ -256,6 +261,10 @@ - (void)addScopes:(NSArray<NSString *> *)scopes
256261
loginHint:self.currentUser.profile.email
257262
addScopesFlow:YES
258263
completion:completion];
264+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
265+
// Explicitly throw an exception for invalid or restricted scopes in the request.
266+
[self assertValidScopes:scopes];
267+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
259268

260269
NSSet<NSString *> *requestedScopes = [NSSet setWithArray:scopes];
261270
NSMutableSet<NSString *> *grantedScopes =
@@ -499,6 +508,7 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
499508
callbackPath:kBrowserCallbackPath
500509
keychainName:kGTMAppAuthKeychainName
501510
isFreshInstall:isFreshInstall];
511+
_registry = [[GIDRestrictedScopesRegistry alloc] init];
502512
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
503513
}
504514
return self;
@@ -989,6 +999,27 @@ - (void)assertValidPresentingViewController {
989999
}
9901000
}
9911001

1002+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
1003+
- (void)assertValidScopes:(NSArray<NSString *> *)scopes {
1004+
NSDictionary<NSString *, Class> *restrictedScopesMapping =
1005+
[_registry restrictedScopesToClassMappingInSet:[NSSet setWithArray:scopes]];
1006+
1007+
if (restrictedScopesMapping.count > 0) {
1008+
NSMutableString *errorMessage =
1009+
[NSMutableString stringWithString:@"The following scopes are not supported in the 'addScopes' flow. "
1010+
"Please use the appropriate classes to handle these:\n"];
1011+
[restrictedScopesMapping enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull restrictedScope,
1012+
Class _Nonnull handlingClass,
1013+
BOOL * _Nonnull stop) {
1014+
[errorMessage appendFormat:@"%@ -> %@\n", restrictedScope, NSStringFromClass(handlingClass)];
1015+
}];
1016+
// NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
1017+
[NSException raise:NSInvalidArgumentException
1018+
format:@"%@", errorMessage];
1019+
}
1020+
}
1021+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
1022+
9921023
// Checks whether or not this is the first time the app runs.
9931024
- (BOOL)isFreshInstall {
9941025
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#import <XCTest/XCTest.h>
16+
17+
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
18+
#import "GoogleSignIn/Sources/GIDRestrictedScopesRegistry.h"
19+
20+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h"
21+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
22+
23+
@interface GIDRestrictedScopesRegistryTest : XCTestCase
24+
@end
25+
26+
@implementation GIDRestrictedScopesRegistryTest
27+
28+
- (void)testIsScopeRestricted {
29+
GIDRestrictedScopesRegistry *registry = [[GIDRestrictedScopesRegistry alloc] init];
30+
BOOL isRestricted = [registry isScopeRestricted:kAccountDetailTypeAgeOver18Scope];
31+
XCTAssertTrue(isRestricted);
32+
}
33+
34+
- (void)testRestrictedScopesToClassMappingInSet {
35+
GIDRestrictedScopesRegistry *registry = [[GIDRestrictedScopesRegistry alloc] init];
36+
NSSet<NSString *> *scopes = [NSSet setWithObjects:kAccountDetailTypeAgeOver18Scope, @"some_other_scope", nil];
37+
NSDictionary<NSString *, Class> *mapping = [registry restrictedScopesToClassMappingInSet:scopes];
38+
39+
XCTAssertEqual(mapping.count, 1);
40+
XCTAssertEqualObjects(mapping[kAccountDetailTypeAgeOver18Scope], [GIDVerifyAccountDetail class]);
41+
XCTAssertNil(mapping[@"some_other_scope"]);
42+
}
43+
44+
@end
45+
46+
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

GoogleSignIn/Tests/Unit/GIDSignInTest.m

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,29 @@ - (void)testTokenEndpointEMMError {
13001300
XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain);
13011301
XCTAssertEqual(_authError.code, kGIDSignInErrorCodeEMM);
13021302
XCTAssertNil(_signIn.currentUser, @"should not have current user");
1303+
}
1304+
1305+
- (void)testValidScopesException {
1306+
NSString *requestedScope = @"https://www.googleapis.com/auth/verified.age.over18.standard";
1307+
NSString *expectedException =
1308+
[NSString stringWithFormat:@"The following scopes are not supported in the 'addScopes' flow. "
1309+
"Please use the appropriate classes to handle these:\n%@ -> %@\n",
1310+
requestedScope, NSStringFromClass([GIDVerifyAccountDetail class])];
1311+
BOOL threw = NO;
1312+
@try {
1313+
[_signIn addScopes:@[requestedScope]
1314+
#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
1315+
presentingViewController:_presentingViewController
1316+
#elif TARGET_OS_OSX
1317+
presentingWindow:_presentingWindow
1318+
#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
1319+
completion:_completion];
1320+
} @catch (NSException *exception) {
1321+
threw = YES;
1322+
XCTAssertEqualObjects(exception.description, expectedException);
1323+
} @finally {
1324+
}
1325+
XCTAssert(threw);
13031326

13041327
// TODO: Keep mocks from carrying forward to subsequent tests. (#410)
13051328
[_authState stopMocking];

0 commit comments

Comments
 (0)