Skip to content

Commit 1b43e18

Browse files
committed
Added GIDTokenClaimsInternalOptions Implementation + Unit Tests
1 parent 7f75975 commit 1b43e18

File tree

4 files changed

+218
-0
lines changed

4 files changed

+218
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 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 <Foundation/Foundation.h>
18+
19+
@class GIDTokenClaim;
20+
21+
NS_ASSUME_NONNULL_BEGIN
22+
23+
extern NSString *const kTokenClaimErrorDescription;
24+
25+
extern NSString *const kTokenClaimEssentialPropertyKeyName;
26+
extern NSString *const kTokenClaimKeyName;
27+
28+
/**
29+
* An internal utility class for processing and serializing the NSSet of GIDTokenClaim objects
30+
* into the JSON format required for an OIDAuthorizationRequest.
31+
*/
32+
@interface GIDTokenClaimsInternalOptions : NSObject
33+
34+
/**
35+
* Processes the NSSet of GIDTokenClaim objects, handling ambiguous claims, and returns a JSON string.
36+
*
37+
* @param claims The NSSet of GIDTokenClaim objects provided by the developer.
38+
* @param error A pointer to an NSError object to be populated if an error occurs (e.g., if a
39+
* claim is requested as both essential and non-essential).
40+
* @return A JSON string representing the claims request, or nil if the input is empty or an
41+
* error occurs.
42+
*/
43+
+ (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet<GIDTokenClaim *> *)claims
44+
error:(NSError **)error;
45+
46+
@end
47+
48+
NS_ASSUME_NONNULL_END
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2025 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 "GIDTokenClaimsInternalOptions.h"
18+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h"
19+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
20+
21+
NSString * const kTokenClaimErrorDescription = @"The claim was requested as both essential and non-essential. Please provide only one version.";
22+
23+
NSString * const kTokenClaimEssentialPropertyKey = @"essential";
24+
NSString * const kTokenClaimKeyName = @"id_token";
25+
26+
@implementation GIDTokenClaimsInternalOptions
27+
28+
+ (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet<GIDTokenClaim *> *)claims
29+
error:(NSError **)error {
30+
if (!claims || claims.count == 0) {
31+
return nil;
32+
}
33+
34+
// === Step 1: Check for claims with ambiguous essential property. ===
35+
NSMutableDictionary<NSString *, GIDTokenClaim *> *validTokenClaims =
36+
[[NSMutableDictionary alloc] init];
37+
38+
for (GIDTokenClaim *currentClaim in claims) {
39+
GIDTokenClaim *existingClaim = validTokenClaims[currentClaim.name];
40+
41+
// Check for a conflict: a claim with the same name but different essentiality.
42+
if (existingClaim && existingClaim.isEssential != currentClaim.isEssential) {
43+
if (error) {
44+
*error = [NSError errorWithDomain:kGIDSignInErrorDomain
45+
code:kGIDSignInErrorCodeAmbiguousClaims
46+
userInfo:@{NSLocalizedDescriptionKey: kTokenClaimErrorDescription}];
47+
}
48+
return nil; // Validation failed
49+
}
50+
validTokenClaims[currentClaim.name] = currentClaim;
51+
}
52+
53+
// === Step 2: Build the dictionary structure required for OIDC JSON ===
54+
NSMutableDictionary<NSString *, id> *tokenClaimsDictionary = [[NSMutableDictionary alloc] init];
55+
for (GIDTokenClaim *claim in validTokenClaims.allValues) {
56+
if (claim.isEssential) {
57+
tokenClaimsDictionary[claim.name] = @{ kTokenClaimEssentialPropertyKey: @YES };
58+
} else {
59+
// Per OIDC spec, non-essential claims can be represented by null.
60+
tokenClaimsDictionary[claim.name] = [NSNull null];
61+
}
62+
}
63+
NSDictionary<NSString *, id> *finalRequestDictionary = @{ kTokenClaimKeyName: tokenClaimsDictionary };
64+
65+
// === Step 3: Serialize the final dictionary into a JSON string ===
66+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:finalRequestDictionary
67+
options:0
68+
error:error];
69+
if (!jsonData) {
70+
return nil;
71+
}
72+
73+
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
74+
}
75+
76+
@end

GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) {
4545
kGIDSignInErrorCodeCanceled = -5,
4646
/// Indicates an Enterprise Mobility Management related error has occurred.
4747
kGIDSignInErrorCodeEMM = -6,
48+
/// Indicates a claim was requested as both essential and non-essential .
49+
kGIDSignInErrorCodeAmbiguousClaims = -7,
4850
/// Indicates the requested scopes have already been granted to the `currentUser`.
4951
kGIDSignInErrorCodeScopesAlreadyGranted = -8,
5052
/// Indicates there is an operation on a previous user.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//
2+
// GIDTokenClaimsInternalOptionsTest.h
3+
// GoogleSignIn
4+
//
5+
// Created by Akshat Gandhi on 9/5/25.
6+
//
7+
8+
9+
#import <XCTest/XCTest.h>
10+
#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h"
11+
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h"
12+
13+
@import OCMock;
14+
15+
static NSString *const kEssentialAuthTimeExpectedJSON = @"{\"id_token\":{\"auth_time\":{\"essential\":true}}}";
16+
static NSString *const kNonEssentialAuthTimeExpectedJSON = @"{\"id_token\":{\"auth_time\":null}}";
17+
18+
19+
@interface GIDTokenClaimsInternalOptionsTest : XCTestCase
20+
@end
21+
22+
@implementation GIDTokenClaimsInternalOptionsTest
23+
24+
#pragma mark - Input Validation Tests
25+
26+
- (void)testValidatedJSONStringForClaims_WithNilInput_ShouldReturnNil {
27+
XCTAssertNil([GIDTokenClaimsInternalOptions validatedJSONStringForClaims:nil error:nil]);
28+
}
29+
30+
- (void)testValidatedJSONStringForClaims_WithEmptyInput_ShouldReturnNil {
31+
XCTAssertNil([GIDTokenClaimsInternalOptions validatedJSONStringForClaims:[NSSet set] error:nil]);
32+
}
33+
34+
#pragma mark - Correct Formatting Tests
35+
36+
- (void)testValidatedJSONStringForClaims_WithNonEssentialClaim_IsCorrectlyFormatted {
37+
NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]];
38+
39+
NSError *error = nil;
40+
NSString *result = [GIDTokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
41+
42+
XCTAssertNil(error);
43+
XCTAssertEqualObjects(result, kNonEssentialAuthTimeExpectedJSON);
44+
}
45+
46+
- (void)testValidatedJSONStringForClaims_WithEssentialClaim_IsCorrectlyFormatted {
47+
NSSet *claims = [NSSet setWithObject:[GIDTokenClaim essentialAuthTimeClaim]];
48+
49+
NSError *error = nil;
50+
NSString *result = [GIDTokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
51+
52+
XCTAssertNil(error);
53+
XCTAssertEqualObjects(result, kEssentialAuthTimeExpectedJSON);
54+
}
55+
56+
#pragma mark - Client Error Handling Tests
57+
58+
- (void)testValidatedJSONStringForClaims_WithConflictingClaims_ReturnsNilAndPopulatesError {
59+
NSSet *claims = [NSSet setWithObjects:[GIDTokenClaim authTimeClaim],
60+
[GIDTokenClaim essentialAuthTimeClaim],
61+
nil];
62+
NSError *error = nil;
63+
64+
NSString *result = [GIDTokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
65+
66+
XCTAssertNil(result, @"Method should return nil for conflicting claims.");
67+
XCTAssertNotNil(error, @"An error object should be populated.");
68+
XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain, @"Error domain should be correct.");
69+
XCTAssertEqual(error.code, kGIDSignInErrorCodeAmbiguousClaims, @"Error code should be for ambiguous claims.");
70+
}
71+
72+
- (void)testValidatedJSONStringForClaims_WhenSerializationFails_ReturnsNilAndError {
73+
NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]];
74+
NSError *fakeJSONError = [NSError errorWithDomain:@"com.fake.json" code:-999 userInfo:nil];
75+
id mockSerialization = OCMClassMock([NSJSONSerialization class]);
76+
77+
OCMStub([mockSerialization dataWithJSONObject:OCMOCK_ANY
78+
options:0
79+
error:[OCMArg setTo:fakeJSONError]]).andReturn(nil);
80+
81+
NSError *actualError = nil;
82+
NSString *result =
83+
[GIDTokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&actualError];
84+
85+
XCTAssertNil(result, @"The result should be nil when JSON serialization fails.");
86+
XCTAssertEqualObjects(actualError, fakeJSONError,
87+
@"The error from serialization should be passed back to the caller.");
88+
89+
[mockSerialization stopMocking];
90+
}
91+
92+
@end

0 commit comments

Comments
 (0)