From 7f75975324a044528a31245fa31746e430458e25 Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:27:23 -0700 Subject: [PATCH 1/7] GIDTokenClaim Implementation + Unit Tests (#550) --- GoogleSignIn/Sources/GIDTokenClaim.m | 75 +++++++++++++++++++ .../Public/GoogleSignIn/GIDTokenClaim.h | 48 ++++++++++++ .../Public/GoogleSignIn/GoogleSignIn.h | 1 + GoogleSignIn/Tests/Unit/GIDTokenClaimTest.m | 49 ++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 GoogleSignIn/Sources/GIDTokenClaim.m create mode 100644 GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h create mode 100644 GoogleSignIn/Tests/Unit/GIDTokenClaimTest.m diff --git a/GoogleSignIn/Sources/GIDTokenClaim.m b/GoogleSignIn/Sources/GIDTokenClaim.m new file mode 100644 index 00000000..61e83d6a --- /dev/null +++ b/GoogleSignIn/Sources/GIDTokenClaim.m @@ -0,0 +1,75 @@ +/* + * Copyright 2025 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/Public/GoogleSignIn/GIDTokenClaim.h" + +NSString * const kAuthTimeClaimName = @"auth_time"; + +// Private interface to declare the internal initializer +@interface GIDTokenClaim () + +- (instancetype)initWithName:(NSString *)name + essential:(BOOL)essential NS_DESIGNATED_INITIALIZER; + +@end + +@implementation GIDTokenClaim + +// Private designated initializer +- (instancetype)initWithName:(NSString *)name essential:(BOOL)essential { + self = [super init]; + if (self) { + _name = [name copy]; + _essential = essential; + } + return self; +} + +#pragma mark - Factory Methods + ++ (instancetype)authTimeClaim { + return [[self alloc] initWithName:kAuthTimeClaimName essential:NO]; +} + ++ (instancetype)essentialAuthTimeClaim { + return [[self alloc] initWithName:kAuthTimeClaimName essential:YES]; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + // 1. Check if the other object is the same instance in memory. + if (self == object) { + return YES; + } + + // 2. Check if the other object is not a GIDTokenClaim instance. + if (![object isKindOfClass:[GIDTokenClaim class]]) { + return NO; + } + + // 3. Compare the properties that define equality. + GIDTokenClaim *other = (GIDTokenClaim *)object; + return [self.name isEqualToString:other.name] && + self.isEssential == other.isEssential; +} + +- (NSUInteger)hash { + // The hash value should be based on the same properties used in isEqual: + return self.name.hash ^ @(self.isEssential).hash; +} + +@end diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h new file mode 100644 index 00000000..7a2351cb --- /dev/null +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h @@ -0,0 +1,48 @@ +/* + * Copyright 2025 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 + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kAuthTimeClaimName; + +/** + * An object representing a single OIDC claim to be requested for an ID token. + */ +@interface GIDTokenClaim : NSObject + +/// The name of the claim, e.g., "auth_time". +@property (nonatomic, readonly) NSString *name; + +/// Whether the claim is requested as essential. +@property (nonatomic, readonly, getter=isEssential) BOOL essential; + +// Making initializers unavailable to force use of factory methods. +- (instancetype)init NS_UNAVAILABLE; + +#pragma mark - Factory Methods + +/// Creates a *non-essential* (voluntary) "auth_time" claim object. ++ (instancetype)authTimeClaim; + +/// Creates an *essential* "auth_time" claim object. ++ (instancetype)essentialAuthTimeClaim; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h index 4fd17ede..02935be8 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h @@ -24,6 +24,7 @@ #import "GIDSignIn.h" #import "GIDToken.h" #import "GIDSignInResult.h" +#import "GIDTokenClaim.h" #if TARGET_OS_IOS || TARGET_OS_MACCATALYST #import "GIDSignInButton.h" #endif diff --git a/GoogleSignIn/Tests/Unit/GIDTokenClaimTest.m b/GoogleSignIn/Tests/Unit/GIDTokenClaimTest.m new file mode 100644 index 00000000..145e46f6 --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDTokenClaimTest.m @@ -0,0 +1,49 @@ +// Copyright 2025 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 +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" + +@interface GIDTokenClaimTest : XCTestCase +@end + +@implementation GIDTokenClaimTest + +- (void)testAuthTimeClaim_PropertiesAreCorrect { + GIDTokenClaim *claim = [GIDTokenClaim authTimeClaim]; + XCTAssertEqualObjects(claim.name, kAuthTimeClaimName); + XCTAssertFalse(claim.isEssential); +} + +- (void)testEssentialAuthTimeClaim_PropertiesAreCorrect { + GIDTokenClaim *claim = [GIDTokenClaim essentialAuthTimeClaim]; + XCTAssertEqualObjects(claim.name, kAuthTimeClaimName); + XCTAssertTrue(claim.isEssential); +} + +- (void)testEquality_WithEqualClaims { + GIDTokenClaim *claim1 = [GIDTokenClaim authTimeClaim]; + GIDTokenClaim *claim2 = [GIDTokenClaim authTimeClaim]; + XCTAssertEqualObjects(claim1, claim2); + XCTAssertEqual(claim1.hash, claim2.hash); +} + +- (void)testEquality_WithUnequalClaims { + GIDTokenClaim *claim1 = [GIDTokenClaim authTimeClaim]; + GIDTokenClaim *claim2 = [GIDTokenClaim essentialAuthTimeClaim]; + XCTAssertNotEqualObjects(claim1, claim2); +} + +@end From 51868b34bf0507de1150d877c24603c7cf29e5e8 Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:11:31 -0700 Subject: [PATCH 2/7] GIDTokenClaimsInternalOptions Implementation + Unit Tests #550 (#552) This pull request introduces the `GIDTokenClaimsInternalOptions` class, a new component designed to handle the validation and JSON serialization of token claims. Key changes: * Adds the `GIDTokenClaimsInternalOptions` class to validate the token claims and return a JSON object. * Adds the `GIDJSONSerializer` protocol with real and fake implementations to support serializing the token claims. * Provides unit tests to validate the implementation. --- .../GIDJSONSerializer/API/GIDJSONSerializer.h | 38 ++++++ .../Fake/GIDFakeJSONSerializerImpl.h | 37 ++++++ .../Fake/GIDFakeJSONSerializerImpl.m | 46 +++++++ .../Implementation/GIDJSONSerializerImpl.h | 26 ++++ .../Implementation/GIDJSONSerializerImpl.m | 46 +++++++ .../Sources/GIDTokenClaimsInternalOptions.h | 55 +++++++++ .../Sources/GIDTokenClaimsInternalOptions.m | 91 ++++++++++++++ .../Sources/Public/GoogleSignIn/GIDSignIn.h | 4 + .../Unit/GIDTokenClaimsInternalOptionsTest.m | 113 ++++++++++++++++++ 9 files changed, 456 insertions(+) create mode 100644 GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h create mode 100644 GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h create mode 100644 GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.m create mode 100644 GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h create mode 100644 GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.m create mode 100644 GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h create mode 100644 GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m create mode 100644 GoogleSignIn/Tests/Unit/GIDTokenClaimsInternalOptionsTest.m diff --git a/GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h b/GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h new file mode 100644 index 00000000..06187949 --- /dev/null +++ b/GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * A protocol for serializing an `NSDictionary` into a JSON string. + */ +@protocol GIDJSONSerializer + +/** + * Serializes the given dictionary into a `JSON` string. + * + * @param jsonObject The dictionary to be serialized. + * @param error A pointer to an `NSError` object to be populated upon failure. + * @return A `JSON` string representation of the dictionary, or `nil` if an error occurs. + */ +- (nullable NSString *)stringWithJSONObject:(NSDictionary *)jsonObject + error:(NSError *_Nullable *_Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h b/GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h new file mode 100644 index 00000000..469fc36d --- /dev/null +++ b/GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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/GIDJSONSerializer/API/GIDJSONSerializer.h" + +NS_ASSUME_NONNULL_BEGIN + +/** A fake implementation of `GIDJSONSerializer` for testing purposes. */ +@interface GIDFakeJSONSerializerImpl : NSObject + +/** + * An error to be returned by `stringWithJSONObject:error:`. + * + * If this property is set, `stringWithJSONObject:error:` will return `nil` and + * populate the error parameter with this error. + */ +@property(nonatomic, nullable) NSError *serializationError; + +/** The dictionary passed to the serialization method. */ +@property(nonatomic, readonly, nullable) NSDictionary *capturedJSONObject; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.m b/GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.m new file mode 100644 index 00000000..400c06ce --- /dev/null +++ b/GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.m @@ -0,0 +1,46 @@ +/* + * Copyright 2025 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/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h" + +#import "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" + +@implementation GIDFakeJSONSerializerImpl + +- (nullable NSString *)stringWithJSONObject:(NSDictionary *)jsonObject + error:(NSError *_Nullable *_Nullable)error { + _capturedJSONObject = [jsonObject copy]; + + // Check if a serialization error should be simulated. + if (self.serializationError) { + if (error) { + *error = self.serializationError; + } + return nil; + } + + // If not failing, fall back to the real serialization path. + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject + options:0 + error:error]; + if (!jsonData) { + return nil; + } + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + +@end diff --git a/GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h b/GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h new file mode 100644 index 00000000..5e1eb03d --- /dev/null +++ b/GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h @@ -0,0 +1,26 @@ +/* + * Copyright 2025 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/GIDJSONSerializer/API/GIDJSONSerializer.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kGIDJSONSerializationErrorDescription; + +@interface GIDJSONSerializerImpl : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.m b/GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.m new file mode 100644 index 00000000..8a3e3d84 --- /dev/null +++ b/GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.m @@ -0,0 +1,46 @@ +/* + * Copyright 2025 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/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h" + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" + +NSString * const kGIDJSONSerializationErrorDescription = + @"The provided object could not be serialized to a JSON string."; + +@implementation GIDJSONSerializerImpl + +- (nullable NSString *)stringWithJSONObject:(NSDictionary *)jsonObject + error:(NSError *_Nullable *_Nullable)error { + NSError *serializationError; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject + options:0 + error:&serializationError]; + if (!jsonData) { + if (error) { + *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeJSONSerializationFailure + userInfo:@{ + NSLocalizedDescriptionKey:kGIDJSONSerializationErrorDescription, + NSUnderlyingErrorKey:serializationError + }]; + } + return nil; + } + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + +@end diff --git a/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h b/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h new file mode 100644 index 00000000..adc56dd1 --- /dev/null +++ b/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h @@ -0,0 +1,55 @@ +/* + * Copyright 2025 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 + +@class GIDTokenClaim; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kGIDTokenClaimErrorDescription; +extern NSString *const kGIDTokenClaimEssentialPropertyKeyName; +extern NSString *const kGIDTokenClaimKeyName; + +@protocol GIDJSONSerializer; + +/** + * An internal utility class for processing and serializing the `NSSet` of `GIDTokenClaim` objects + * into the `JSON` format required for an `OIDAuthorizationRequest`. + */ +@interface GIDTokenClaimsInternalOptions : NSObject + +- (instancetype)init; + +- (instancetype)initWithJSONSerializer: + (id)jsonSerializer NS_DESIGNATED_INITIALIZER; + +/** + * Processes the `NSSet` of `GIDTokenClaim` objects, handling ambiguous claims, + * and returns a `JSON` string. + * + * @param claims The `NSSet` of `GIDTokenClaim` objects provided by the developer. + * @param error A pointer to an `NSError` object to be populated if an error occurs (e.g., if a + * claim is requested as both essential and non-essential). + * @return A `JSON` string representing the claims request, or `nil` if the input is empty or an + * error occurs. + */ +- (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet *)claims + error:(NSError *_Nullable *_Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m b/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m new file mode 100644 index 00000000..049f00e1 --- /dev/null +++ b/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m @@ -0,0 +1,91 @@ +/* + * Copyright 2025 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/GIDTokenClaimsInternalOptions.h" + +#import "GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h" +#import "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" + +NSString * const kGIDTokenClaimErrorDescription = + @"The claim was requested as both essential and non-essential. " + @"Please provide only one version."; +NSString * const kGIDTokenClaimEssentialPropertyKey = @"essential"; +NSString * const kGIDTokenClaimKeyName = @"id_token"; + +@interface GIDTokenClaimsInternalOptions () +@property(nonatomic, readonly) id jsonSerializer; +@end + +@implementation GIDTokenClaimsInternalOptions + +- (instancetype)init { + return [self initWithJSONSerializer:[[GIDJSONSerializerImpl alloc] init]]; +} + +- (instancetype)initWithJSONSerializer:(id)jsonSerializer { + if (self = [super init]) { + _jsonSerializer = jsonSerializer; + } + return self; +} + +- (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet *)claims + error:(NSError *_Nullable *_Nullable)error { + if (!claims || claims.count == 0) { + return nil; + } + + // === Step 1: Check for claims with ambiguous essential property. === + NSMutableDictionary *validTokenClaims = + [[NSMutableDictionary alloc] init]; + + for (GIDTokenClaim *currentClaim in claims) { + GIDTokenClaim *existingClaim = validTokenClaims[currentClaim.name]; + + // Check for a conflict: a claim with the same name but different essentiality. + if (existingClaim && existingClaim.isEssential != currentClaim.isEssential) { + if (error) { + *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeAmbiguousClaims + userInfo:@{ + NSLocalizedDescriptionKey:kGIDTokenClaimErrorDescription + }]; + } + return nil; + } + validTokenClaims[currentClaim.name] = currentClaim; + } + + // === Step 2: Build the dictionary structure required for OIDC JSON === + NSMutableDictionary *tokenClaimsDictionary = + [[NSMutableDictionary alloc] init]; + for (GIDTokenClaim *claim in validTokenClaims.allValues) { + if (claim.isEssential) { + tokenClaimsDictionary[claim.name] = @{ kGIDTokenClaimEssentialPropertyKey: @YES }; + } else { + tokenClaimsDictionary[claim.name] = @{ kGIDTokenClaimEssentialPropertyKey: @NO }; + } + } + NSDictionary *finalRequestDictionary = + @{ kGIDTokenClaimKeyName: tokenClaimsDictionary }; + + // === Step 3: Serialize the final dictionary into a JSON string === + return [_jsonSerializer stringWithJSONObject:finalRequestDictionary error:error]; +} + +@end diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index 1025a92a..29cc0ef7 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -45,10 +45,14 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { kGIDSignInErrorCodeCanceled = -5, /// Indicates an Enterprise Mobility Management related error has occurred. kGIDSignInErrorCodeEMM = -6, + /// Indicates a claim was requested as both essential and non-essential . + kGIDSignInErrorCodeAmbiguousClaims = -7, /// Indicates the requested scopes have already been granted to the `currentUser`. kGIDSignInErrorCodeScopesAlreadyGranted = -8, /// Indicates there is an operation on a previous user. kGIDSignInErrorCodeMismatchWithCurrentUser = -9, + /// Indicates that an object could not be serialized into a `JSON` string. + kGIDSignInErrorCodeJSONSerializationFailure = -10 }; /// This class is used to sign in users with their Google account and manage their session. diff --git a/GoogleSignIn/Tests/Unit/GIDTokenClaimsInternalOptionsTest.m b/GoogleSignIn/Tests/Unit/GIDTokenClaimsInternalOptionsTest.m new file mode 100644 index 00000000..4c90998f --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDTokenClaimsInternalOptionsTest.m @@ -0,0 +1,113 @@ +// Copyright 2025 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 + +#import "GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h" +#import "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h" +#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" + +static NSString *const kEssentialAuthTimeExpectedJSON = @"{\"id_token\":{\"auth_time\":{\"essential\":true}}}"; +static NSString *const kNonEssentialAuthTimeExpectedJSON = @"{\"id_token\":{\"auth_time\":{\"essential\":false}}}"; + +@interface GIDTokenClaimsInternalOptionsTest : XCTestCase + +@property(nonatomic) GIDFakeJSONSerializerImpl *jsonSerializerFake; +@property(nonatomic) GIDTokenClaimsInternalOptions *tokenClaimsInternalOptions; + +@end + +@implementation GIDTokenClaimsInternalOptionsTest + +- (void)setUp { + [super setUp]; + _jsonSerializerFake = [[GIDFakeJSONSerializerImpl alloc] init]; + _tokenClaimsInternalOptions = [[GIDTokenClaimsInternalOptions alloc] initWithJSONSerializer:_jsonSerializerFake]; +} + +- (void)tearDown { + _jsonSerializerFake = nil; + _tokenClaimsInternalOptions = nil; + [super tearDown]; +} + +#pragma mark - Input Validation Tests + +- (void)testValidatedJSONStringForClaims_WithNilInput_ShouldReturnNil { + XCTAssertNil([_tokenClaimsInternalOptions validatedJSONStringForClaims:nil error:nil]); +} + +- (void)testValidatedJSONStringForClaims_WithEmptyInput_ShouldReturnNil { + XCTAssertNil([_tokenClaimsInternalOptions validatedJSONStringForClaims:[NSSet set] error:nil]); +} + +#pragma mark - Correct Formatting Tests + +- (void)testValidatedJSONStringForClaims_WithNonEssentialClaim_IsCorrectlyFormatted { + NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]]; + NSError *error; + NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error]; + + XCTAssertNil(error); + XCTAssertEqualObjects(result, kNonEssentialAuthTimeExpectedJSON); +} + +- (void)testValidatedJSONStringForClaims_WithEssentialClaim_IsCorrectlyFormatted { + NSSet *claims = [NSSet setWithObject:[GIDTokenClaim essentialAuthTimeClaim]]; + NSError *error; + NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error]; + + XCTAssertNil(error); + XCTAssertEqualObjects(result, kEssentialAuthTimeExpectedJSON); +} + +#pragma mark - Client Error Handling Tests + +- (void)testValidatedJSONStringForClaims_WithConflictingClaims_ReturnsNilAndPopulatesError { + NSSet *claims = [NSSet setWithObjects:[GIDTokenClaim authTimeClaim], + [GIDTokenClaim essentialAuthTimeClaim], + nil]; + NSError *error; + NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error]; + + XCTAssertNil(result, @"Method should return nil for conflicting claims."); + XCTAssertNotNil(error, @"An error object should be populated."); + XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain, @"Error domain should be correct."); + XCTAssertEqual(error.code, kGIDSignInErrorCodeAmbiguousClaims, + @"Error code should be for ambiguous claims."); +} + +- (void)testValidatedJSONStringForClaims_WhenSerializationFails_ReturnsNilAndError { + NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]]; + NSError *expectedJSONError = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeJSONSerializationFailure + userInfo:@{ + NSLocalizedDescriptionKey: kGIDJSONSerializationErrorDescription, + }]; + _jsonSerializerFake.serializationError = expectedJSONError; + NSError *actualError; + NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims + error:&actualError]; + + XCTAssertNil(result, @"The result should be nil when JSON serialization fails."); + XCTAssertEqualObjects( + actualError, + expectedJSONError, + @"The error from serialization should be passed back to the caller." + ); +} + +@end From 076275836b3110f377d63b0fcf8ca37c1ed0d051 Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:44:13 -0700 Subject: [PATCH 3/7] Updated GIDSignIn + GIDSignInInternalOptions Implementations + Unit Tests (#553) --- GoogleSignIn/Sources/GIDSignIn.m | 149 ++++++++++++-- .../Sources/GIDSignInInternalOptions.h | 8 + .../Sources/GIDSignInInternalOptions.m | 5 + .../Sources/Public/GoogleSignIn/GIDSignIn.h | 170 ++++++++++++++++ .../Tests/Unit/GIDSignInInternalOptionsTest.m | 49 +++++ GoogleSignIn/Tests/Unit/GIDSignInTest.m | 184 ++++++++++++++++-- .../Tests/Unit/OIDTokenResponse+Testing.h | 12 ++ .../Tests/Unit/OIDTokenResponse+Testing.m | 38 +++- 8 files changed, 580 insertions(+), 35 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 1c043735..4da18b2c 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -28,6 +28,7 @@ #import "GoogleSignIn/Sources/GIDCallbackQueue.h" #import "GoogleSignIn/Sources/GIDScopes.h" #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" +#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import #import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h" @@ -136,6 +137,9 @@ static NSString *const kLoginHintParameter = @"login_hint"; static NSString *const kHostedDomainParameter = @"hd"; +// Parameter for requesting the token claims. +static NSString *const kTokenClaimsParameter = @"claims"; + // Parameters for auth and token exchange endpoints using App Attest. static NSString *const kClientAssertionParameter = @"client_assertion"; static NSString *const kClientAssertionTypeParameter = @"client_assertion_type"; @@ -169,6 +173,7 @@ @implementation GIDSignIn { // set when a sign-in flow is begun via |signInWithOptions:| when the options passed don't // represent a sign in continuation. GIDSignInInternalOptions *_currentOptions; + GIDTokenClaimsInternalOptions *_tokenClaimsInternalOptions; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST GIDAppCheck *_appCheck API_AVAILABLE(ios(14)); #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST @@ -284,14 +289,63 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon additionalScopes:(nullable NSArray *)additionalScopes nonce:(nullable NSString *)nonce completion:(nullable GIDSignInCompletion)completion { + [self signInWithPresentingViewController:presentingViewController + hint:hint + additionalScopes:additionalScopes + nonce:nonce + tokenClaims:nil + completion:completion]; +} + +- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable GIDSignInCompletion)completion { + [self signInWithPresentingViewController:presentingViewController + hint:nil + tokenClaims:tokenClaims + completion:completion]; +} + +- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController + hint:(nullable NSString *)hint + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable GIDSignInCompletion)completion { + [self signInWithPresentingViewController:presentingViewController + hint:hint + additionalScopes:@[] + tokenClaims:tokenClaims + completion:completion]; +} + +- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController + hint:(nullable NSString *)hint + additionalScopes:(nullable NSArray *)additionalScopes + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable GIDSignInCompletion)completion { + [self signInWithPresentingViewController:presentingViewController + hint:hint + additionalScopes:additionalScopes + nonce:nil + tokenClaims:tokenClaims + completion:completion]; +} + + +- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController + hint:(nullable NSString *)hint + additionalScopes:(nullable NSArray *)additionalScopes + nonce:(nullable NSString *)nonce + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable GIDSignInCompletion)completion { GIDSignInInternalOptions *options = - [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration - presentingViewController:presentingViewController - loginHint:hint - addScopesFlow:NO - scopes:additionalScopes - nonce:nonce - completion:completion]; + [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration + presentingViewController:presentingViewController + loginHint:hint + addScopesFlow:NO + scopes:additionalScopes + nonce:nonce + tokenClaims:tokenClaims + completion:completion]; [self signInWithOptions:options]; } @@ -375,14 +429,62 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow additionalScopes:(nullable NSArray *)additionalScopes nonce:(nullable NSString *)nonce completion:(nullable GIDSignInCompletion)completion { + [self signInWithPresentingWindow:presentingWindow + hint:hint + additionalScopes:additionalScopes + nonce:nonce + tokenClaims:nil + completion:completion]; +} + +- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable GIDSignInCompletion)completion { + [self signInWithPresentingWindow:presentingWindow + hint:nil + tokenClaims:tokenClaims + completion:completion]; +} + +- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow + hint:(nullable NSString *)hint + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable GIDSignInCompletion)completion { + [self signInWithPresentingWindow:presentingWindow + hint:hint + additionalScopes:@[] + tokenClaims:tokenClaims + completion:completion]; +} + +- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow + hint:(nullable NSString *)hint + additionalScopes:(nullable NSArray *)additionalScopes + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable GIDSignInCompletion)completion { + [self signInWithPresentingWindow:presentingWindow + hint:hint + additionalScopes:additionalScopes + nonce:nil + tokenClaims:tokenClaims + completion:completion]; +} + +- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow + hint:(nullable NSString *)hint + additionalScopes:(nullable NSArray *)additionalScopes + nonce:(nullable NSString *)nonce + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable GIDSignInCompletion)completion { GIDSignInInternalOptions *options = - [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration - presentingWindow:presentingWindow - loginHint:hint - addScopesFlow:NO - scopes:additionalScopes - nonce:nonce - completion:completion]; + [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration + presentingWindow:presentingWindow + loginHint:hint + addScopesFlow:NO + scopes:additionalScopes + nonce:nonce + tokenClaims:tokenClaims + completion:completion]; [self signInWithOptions:options]; } @@ -542,6 +644,7 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore self = [super init]; if (self) { _keychainStore = keychainStore; + _tokenClaimsInternalOptions = [[GIDTokenClaimsInternalOptions alloc] init]; // Get the bundle of the current executable. NSBundle *bundle = NSBundle.mainBundle; @@ -636,6 +739,21 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { } }]; } else { + NSError *claimsError; + + // If tokenClaims are invalid or JSON serialization fails, return with an error. + options.tokenClaimsAsJSON = [_tokenClaimsInternalOptions + validatedJSONStringForClaims:options.tokenClaims + error:&claimsError]; + if (claimsError) { + if (options.completion) { + self->_currentOptions = nil; + dispatch_async(dispatch_get_main_queue(), ^{ + options.completion(nil, claimsError); + }); + } + return; + } [self authenticateWithOptions:options]; } } @@ -765,6 +883,9 @@ - (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options comp if (options.configuration.hostedDomain) { additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain; } + if (options.tokenClaimsAsJSON) { + additionalParameters[kTokenClaimsParameter] = options.tokenClaimsAsJSON; + } #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST [additionalParameters addEntriesFromDictionary: diff --git a/GoogleSignIn/Sources/GIDSignInInternalOptions.h b/GoogleSignIn/Sources/GIDSignInInternalOptions.h index 1ea78f46..f21d75d7 100644 --- a/GoogleSignIn/Sources/GIDSignInInternalOptions.h +++ b/GoogleSignIn/Sources/GIDSignInInternalOptions.h @@ -68,6 +68,12 @@ NS_ASSUME_NONNULL_BEGIN /// and to mitigate replay attacks. @property(nonatomic, readonly, copy, nullable) NSString *nonce; +/// The tokenClaims requested by the Clients. +@property(nonatomic, readonly, copy, nullable) NSSet *tokenClaims; + +/// The JSON token claims to be used during the flow. +@property(nonatomic, copy, nullable) NSString *tokenClaimsAsJSON; + /// Creates the default options. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration @@ -82,6 +88,7 @@ NS_ASSUME_NONNULL_BEGIN addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes nonce:(nullable NSString *)nonce + tokenClaims:(nullable NSSet *)tokenClaims completion:(nullable GIDSignInCompletion)completion; #elif TARGET_OS_OSX @@ -97,6 +104,7 @@ NS_ASSUME_NONNULL_BEGIN addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes nonce:(nullable NSString *)nonce + tokenClaims:(nullable NSSet *)tokenClaims completion:(nullable GIDSignInCompletion)completion; #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDSignInInternalOptions.m b/GoogleSignIn/Sources/GIDSignInInternalOptions.m index 523bd48d..0799906a 100644 --- a/GoogleSignIn/Sources/GIDSignInInternalOptions.m +++ b/GoogleSignIn/Sources/GIDSignInInternalOptions.m @@ -32,6 +32,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes nonce:(nullable NSString *)nonce + tokenClaims:(nullable NSSet *)tokenClaims completion:(nullable GIDSignInCompletion)completion { #elif TARGET_OS_OSX + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration @@ -40,6 +41,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes nonce:(nullable NSString *)nonce + tokenClaims:(nullable NSSet *)tokenClaims completion:(nullable GIDSignInCompletion)completion { #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init]; @@ -57,6 +59,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con options->_completion = completion; options->_scopes = [GIDScopes scopesWithBasicProfile:scopes]; options->_nonce = nonce; + options->_tokenClaims = tokenClaims; } return options; } @@ -84,6 +87,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con addScopesFlow:addScopesFlow scopes:@[] nonce:nil + tokenClaims:nil completion:completion]; return options; } @@ -120,6 +124,7 @@ - (instancetype)optionsWithExtraParameters:(NSDictionary *)extraParams options->_loginHint = _loginHint; options->_completion = _completion; options->_scopes = _scopes; + options->_tokenClaims = _tokenClaims; options->_extraParams = [extraParams copy]; } return options; diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index 29cc0ef7..768d1764 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -26,6 +26,7 @@ @class GIDConfiguration; @class GIDGoogleUser; @class GIDSignInResult; +@class GIDTokenClaim; NS_ASSUME_NONNULL_BEGIN @@ -225,6 +226,95 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") NSError *_Nullable error))completion NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); +/// Starts an interactive sign-in flow on iOS using the provided tokenClaims. +/// +/// The completion will be called at the end of this process. Any saved sign-in state will be +/// replaced by the result of this flow. Note that this method should not be called when the app is +/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the +/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. +/// +/// @param presentingViewController The view controller used to present the authorization flow. +/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param completion The optional block that is called on completion. This block will +/// be called asynchronously on the main queue. +- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController + tokenClaims:(nullable NSSet *)tokenClaims + completion: + (nullable void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion + NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); + +/// Starts an interactive sign-in flow on iOS using the provided hint and tokenClaims. +/// +/// The completion will be called at the end of this process. Any saved sign-in state will be +/// replaced by the result of this flow. Note that this method should not be called when the app is +/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the +/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. +/// +/// @param presentingViewController The view controller used to present the authorization flow. +/// @param hint An optional hint for the authorization server, for example the user's ID or email +/// address, to be prefilled if possible. +/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param completion The optional block that is called on completion. This block will +/// be called asynchronously on the main queue. +- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController + hint:(nullable NSString *)hint + tokenClaims:(nullable NSSet *)tokenClaims + completion: + (nullable void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion + NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); + +/// Starts an interactive sign-in flow on iOS using the provided hint, additional scopes, +/// and tokenClaims. +/// +/// The completion will be called at the end of this process. Any saved sign-in state will be +/// replaced by the result of this flow. Note that this method should not be called when the app is +/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the +/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. +/// +/// @param presentingViewController The view controller used to present the authorization flow. +/// @param hint An optional hint for the authorization server, for example the user's ID or email +/// address, to be prefilled if possible. +/// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. +/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param completion The optional block that is called on completion. This block will +/// be called asynchronously on the main queue. +- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController + hint:(nullable NSString *)hint + additionalScopes:(nullable NSArray *)additionalScopes + tokenClaims:(nullable NSSet *)tokenClaims + completion: + (nullable void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion + NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); + +/// Starts an interactive sign-in flow on iOS using the provided hint, additional scopes, nonce, +/// and tokenClaims. +/// +/// The completion will be called at the end of this process. Any saved sign-in state will be +/// replaced by the result of this flow. Note that this method should not be called when the app is +/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the +/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. +/// +/// @param presentingViewController The view controller used to present the authorization flow. +/// @param hint An optional hint for the authorization server, for example the user's ID or email +/// address, to be prefilled if possible. +/// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. +/// @param nonce A custom nonce. +/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param completion The optional block that is called on completion. This block will +/// be called asynchronously on the main queue. +- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController + hint:(nullable NSString *)hint + additionalScopes:(nullable NSArray *)additionalScopes + nonce:(nullable NSString *)nonce + tokenClaims:(nullable NSSet *)tokenClaims + completion: + (nullable void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion + NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); + #elif TARGET_OS_OSX /// Starts an interactive sign-in flow on macOS. @@ -298,6 +388,86 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion; +/// Starts an interactive sign-in flow on macOS using the provided tokenClaims. +/// +/// The completion will be called at the end of this process. Any saved sign-in state will be +/// replaced by the result of this flow. Note that this method should not be called when the app is +/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the +/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. +/// +/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`. +/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param completion The optional block that is called on completion. This block will +/// be called asynchronously on the main queue. +- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion; + +/// Starts an interactive sign-in flow on macOS using the provided hint and tokenClaims. +/// +/// The completion will be called at the end of this process. Any saved sign-in state will be +/// replaced by the result of this flow. Note that this method should not be called when the app is +/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the +/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. +/// +/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`. +/// @param hint An optional hint for the authorization server, for example the user's ID or email +/// address, to be prefilled if possible. +/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param completion The optional block that is called on completion. This block will +/// be called asynchronously on the main queue. +- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow + hint:(nullable NSString *)hint + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion; + +/// Starts an interactive sign-in flow on macOS using the provided hint, additional scopes, +/// and tokenClaims. +/// +/// The completion will be called at the end of this process. Any saved sign-in state will be +/// replaced by the result of this flow. Note that this method should not be called when the app is +/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the +/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. +/// +/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`. +/// @param hint An optional hint for the authorization server, for example the user's ID or email +/// address, to be prefilled if possible. +/// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. +/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param completion The optional block that is called on completion. This block will +/// be called asynchronously on the main queue. +- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow + hint:(nullable NSString *)hint + additionalScopes:(nullable NSArray *)additionalScopes + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion; + +/// Starts an interactive sign-in flow on macOS using the provided hint, additional scopes, nonce, +/// and tokenClaims. +/// +/// The completion will be called at the end of this process. Any saved sign-in state will be +/// replaced by the result of this flow. Note that this method should not be called when the app is +/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the +/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. +/// +/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`. +/// @param hint An optional hint for the authorization server, for example the user's ID or email +/// address, to be prefilled if possible. +/// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. +/// @param nonce A custom nonce. +/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param completion The optional block that is called on completion. This block will +/// be called asynchronously on the main queue. +- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow + hint:(nullable NSString *)hint + additionalScopes:(nullable NSArray *)additionalScopes + nonce:(nullable NSString *)nonce + tokenClaims:(nullable NSSet *)tokenClaims + completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion; #endif diff --git a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m index bcc48910..1d6c2b3c 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m @@ -17,6 +17,7 @@ #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" #ifdef SWIFT_PACKAGE @import OCMock; @@ -63,6 +64,54 @@ - (void)testDefaultOptions { #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST } +- (void)testDefaultOptions_withAllParameters_initializesPropertiesCorrectly { + id configuration = OCMStrictClassMock([GIDConfiguration class]); +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + id presentingViewController = OCMStrictClassMock([UIViewController class]); +#elif TARGET_OS_OSX + id presentingWindow = OCMStrictClassMock([NSWindow class]); +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST + NSString *loginHint = @"login_hint"; + NSArray *scopes = @[@"scope1", @"scope2"]; + NSString *nonce = @"test_nonce"; + NSSet *tokenClaims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]]; + NSArray *expectedScopes = @[@"scope1", @"scope2", @"email", @"profile"]; + + GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult, + NSError * _Nullable error) {}; + GIDSignInInternalOptions *options = + [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + presentingViewController:presentingViewController +#elif TARGET_OS_OSX + presentingWindow:presentingWindow +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST + loginHint:loginHint + addScopesFlow:NO + scopes:scopes + nonce:nonce + tokenClaims:tokenClaims + completion:completion]; + XCTAssertTrue(options.interactive); + XCTAssertFalse(options.continuation); + XCTAssertFalse(options.addScopesFlow); + XCTAssertNil(options.extraParams); + + // Convert arrays to sets for comparison to make the test order-independent. + XCTAssertEqualObjects([NSSet setWithArray:options.scopes], [NSSet setWithArray:expectedScopes]); + XCTAssertEqualObjects(options.nonce, nonce); + XCTAssertEqualObjects(options.tokenClaims, tokenClaims); + XCTAssertNil(options.tokenClaimsAsJSON); + + OCMVerifyAll(configuration); +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + OCMVerifyAll(presentingViewController); +#elif TARGET_OS_OSX + OCMVerifyAll(presentingWindow); +#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST +} + + - (void)testSilentOptions { GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult, NSError * _Nullable error) {}; diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index b36e197a..17453785 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -32,6 +32,7 @@ #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" #import "GoogleSignIn/Sources/GIDSignIn_Private.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" +#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import @@ -159,6 +160,12 @@ static NSString *const kGrantedScope = @"grantedScope"; static NSString *const kNewScope = @"newScope"; +static NSString *const kEssentialAuthTimeClaimsJsonString = + @"{\"id_token\":{\"auth_time\":{\"essential\":true}}}"; +static NSString *const kNonEssentialAuthTimeClaimsJsonString = + @"{\"id_token\":{\"auth_time\":{\"essential\":false}}}"; + + #if TARGET_OS_IOS || TARGET_OS_MACCATALYST // This category is used to allow the test to swizzle a private method. @interface UIViewController (Testing) @@ -517,18 +524,18 @@ - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser { OCMStub([idTokenDecoded alloc]).andReturn(idTokenDecoded); OCMStub([idTokenDecoded initWithIDTokenString:OCMOCK_ANY]).andReturn(idTokenDecoded); OCMStub([idTokenDecoded subject]).andReturn(kFakeGaiaID); - + // Mock generating a GIDConfiguration when initializing GIDGoogleUser. OIDAuthorizationResponse *authResponse = [OIDAuthorizationResponse testInstance]; - + OCMStub([_authState lastAuthorizationResponse]).andReturn(authResponse); OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken); OCMStub([_tokenResponse request]).andReturn(_tokenRequest); OCMStub([_tokenRequest additionalParameters]).andReturn(nil); OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken); OCMStub([_tokenResponse accessTokenExpirationDate]).andReturn(nil); - + [_signIn restorePreviousSignInNoRefresh]; [idTokenDecoded verify]; @@ -691,12 +698,14 @@ - (void)testOAuthLogin_AdditionalScopes { tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO + tokenClaimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:YES additionalScopes:nil - manualNonce:nil]; + manualNonce:nil + tokenClaims:nil]; expectedScopeString = [@[ @"email", @"profile" ] componentsJoinedByString:@" "]; XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); @@ -706,12 +715,14 @@ - (void)testOAuthLogin_AdditionalScopes { tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO + tokenClaimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:YES additionalScopes:@[ kScope ] - manualNonce:nil]; + manualNonce:nil + tokenClaims:nil]; expectedScopeString = [@[ kScope, @"email", @"profile" ] componentsJoinedByString:@" "]; XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); @@ -721,17 +732,101 @@ - (void)testOAuthLogin_AdditionalScopes { tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO + tokenClaimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:YES additionalScopes:@[ kScope, kScope2 ] - manualNonce:nil]; + manualNonce:nil + tokenClaims:nil]; expectedScopeString = [@[ kScope, kScope2, @"email", @"profile" ] componentsJoinedByString:@" "]; XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); } +- (void)testOAuthLogin_WithTokenClaims_FormatsParametersCorrectly { + GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; + GIDTokenClaim *essentialAuthTimeClaim = [GIDTokenClaim essentialAuthTimeClaim]; + + OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation){ + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + keychainError:NO + tokenClaimsError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO + useAdditionalScopes:NO + additionalScopes:nil + manualNonce:nil + tokenClaims:[NSSet setWithObject:essentialAuthTimeClaim]]; + + XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], + kEssentialAuthTimeClaimsJsonString, + @"Claims JSON should be correctly formatted"); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + keychainError:NO + tokenClaimsError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO + useAdditionalScopes:NO + additionalScopes:nil + manualNonce:nil + tokenClaims:[NSSet setWithObject:authTimeClaim]]; + + XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], + kNonEssentialAuthTimeClaimsJsonString, + @"Claims JSON should be correctly formatted"); +} + +- (void)testOAuthLogin_WithTokenClaims_ReturnsIdTokenWithCorrectClaims { + GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; + + OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation){ + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + keychainError:NO + tokenClaimsError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO + useAdditionalScopes:NO + additionalScopes:nil + manualNonce:nil + tokenClaims:[NSSet setWithObject:authTimeClaim]]; + + XCTAssertNotNil(_signIn.currentUser, @"The currentUser should not be nil after a successful sign-in."); + NSString *idTokenString = _signIn.currentUser.idToken.tokenString; + XCTAssertNotNil(idTokenString, @"ID token string should not be nil."); + NSArray *components = [idTokenString componentsSeparatedByString:@"."]; + XCTAssertEqual(components.count, 3, @"JWT should have 3 parts."); + NSData *payloadData = [[NSData alloc] + initWithBase64EncodedString:components[1] + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSDictionary *claims = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil]; + XCTAssertEqualObjects(claims[@"auth_time"], + kAuthTime, + @"The 'auth_time' claim should be present and correct."); +} + - (void)testAddScopes { // Restore the previous sign-in account. This is the preparation for adding scopes. OCMStub( @@ -752,7 +847,7 @@ - (void)testAddScopes { id profile = OCMStrictClassMock([GIDProfileData class]); OCMStub([profile email]).andReturn(kUserEmail); - + // Mock for the method `addScopes`. GIDConfiguration *configuration = [[GIDConfiguration alloc] initWithClientID:kClientId serverClientID:nil @@ -784,7 +879,7 @@ - (void)testAddScopes { [parsedScopes removeObject:@""]; grantedScopes = [parsedScopes copy]; } - + NSArray *expectedScopes = @[kNewScope, kGrantedScope]; XCTAssertEqualObjects(grantedScopes, expectedScopes); @@ -831,18 +926,20 @@ - (void)testManualNonce { }); NSString* manualNonce = @"manual_nonce"; - + [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO + tokenClaimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:NO additionalScopes:@[] - manualNonce:manualNonce]; + manualNonce:manualNonce + tokenClaims:nil]; XCTAssertEqualObjects(_savedAuthorizationRequest.nonce, manualNonce, @@ -950,6 +1047,36 @@ - (void)testOAuthLogin_KeychainError { XCTAssertEqual(_authError.code, kGIDSignInErrorCodeKeychain); } +- (void)testOAuthLogin_TokenClaims_FailsWithError { + GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; + GIDTokenClaim *essentialAuthTimeClaim = [GIDTokenClaim essentialAuthTimeClaim]; + NSSet *conflictingClaims = [NSSet setWithObjects:authTimeClaim, essentialAuthTimeClaim, nil]; + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + keychainError:NO + tokenClaimsError:YES + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO + useAdditionalScopes:NO + additionalScopes:nil + manualNonce:nil + tokenClaims:conflictingClaims]; + + // Wait for the completion handler to be called + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + XCTAssertNotNil(_authError, @"An error object should have been returned."); + XCTAssertEqual(_authError.code, kGIDSignInErrorCodeAmbiguousClaims, + @"The error code should be for ambiguous claims."); + XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain, + @"The error domain should be the GIDSignIn error domain."); + XCTAssertEqualObjects(_authError.localizedDescription, kGIDTokenClaimErrorDescription, + @"The error description should clearly explain the ambiguity."); +} + - (void)testSignOut { #if TARGET_OS_IOS || !TARGET_OS_MACCATALYST // OCMStub([_authorization authState]).andReturn(_authState); @@ -1339,7 +1466,7 @@ - (void)testTokenEndpointEMMError { NSError *handledError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeEMM userInfo:emmError.userInfo]; - + completion(handledError); [self waitForExpectationsWithTimeout:1 handler:nil]; @@ -1424,12 +1551,14 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow tokenError:tokenError emmPasscodeInfoRequired:emmPasscodeInfoRequired keychainError:keychainError + tokenClaimsError:NO restoredSignIn:restoredSignIn oldAccessToken:oldAccessToken modalCancel:modalCancel useAdditionalScopes:NO additionalScopes:nil - manualNonce:nil]; + manualNonce:nil + tokenClaims:nil]; } // The authorization flow with parameters to control which branches to take. @@ -1438,12 +1567,14 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow tokenError:(NSError *)tokenError emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired keychainError:(BOOL)keychainError + tokenClaimsError:(BOOL)tokenClaimsError restoredSignIn:(BOOL)restoredSignIn oldAccessToken:(BOOL)oldAccessToken modalCancel:(BOOL)modalCancel useAdditionalScopes:(BOOL)useAdditionalScopes - additionalScopes:(NSArray *)additionalScopes - manualNonce:(NSString *)nonce { + additionalScopes:(NSArray *)additionalScopes + manualNonce:(NSString *)nonce + tokenClaims:(NSSet *)tokenClaims { if (restoredSignIn) { // clearAndAuthenticateWithOptions [[[_authorization expect] andReturn:_authState] authState]; @@ -1458,13 +1589,21 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow nonce:nonce errorString:authError]; + NSString *idToken = tokenClaims ? [OIDTokenResponse fatIDTokenWithAuthTime] : [OIDTokenResponse fatIDToken]; OIDTokenResponse *tokenResponse = - [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] + [OIDTokenResponse testInstanceWithIDToken:idToken accessToken:restoredSignIn ? kAccessToken : nil expiresIn:oldAccessToken ? @(300) : nil refreshToken:kRefreshToken tokenRequest:nil]; + if (tokenClaims) { + // Creating this stub to use `currentUser.idToken`. + id mockIDToken = OCMClassMock([GIDToken class]); + OCMStub([mockIDToken tokenString]).andReturn(tokenResponse.idToken); + OCMStub([_user idToken]).andReturn(mockIDToken); + } + OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc] initWithConfiguration:authResponse.request.configuration grantType:OIDGrantTypeRefreshToken @@ -1533,10 +1672,17 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow hint:_hint additionalScopes:nil nonce:nonce + tokenClaims:tokenClaims completion:completion]; } } + // When token claims are invalid, sign-in fails skipping the entire authorization flow. + // Thus, no need to verify `_authorization` or `_authState` as they won't be generated. + if (tokenClaimsError) { + return; + } + [_authorization verify]; [_authState verify]; @@ -1631,7 +1777,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow profileData:SAVE_TO_ARG_BLOCK(profileData)]; } } - + // CompletionCallback - mock server auth code parsing if (!keychainError) { [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; @@ -1653,9 +1799,9 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow return; } [self waitForExpectationsWithTimeout:1 handler:nil]; - + [_authState verify]; - + XCTAssertTrue(_keychainSaved, @"should save to keychain"); if (addScopesFlow) { XCTAssertNotNil(updatedTokenResponse); @@ -1692,7 +1838,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertFalse(_keychainRemoved, @"should not remove keychain"); XCTAssertFalse(_keychainSaved, @"should not save to keychain again"); - + if (restoredSignIn) { // Ignore the return value OCMVerify((void)[_keychainStore retrieveAuthSessionWithError:OCMArg.anyObjectRef]); diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h index b565a392..b8329c67 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h @@ -33,6 +33,7 @@ extern NSString *const kUserID; extern NSString *const kHostedDomain; extern NSString *const kIssuer; extern NSString *const kAudience; +extern NSString *const kAuthTime; extern NSTimeInterval const kIDTokenExpires; extern NSTimeInterval const kIssuedAt; @@ -59,10 +60,19 @@ extern NSString * const kFatPictureURL; refreshToken:(NSString *)refreshToken tokenRequest:(OIDTokenRequest *)tokenRequest; ++ (instancetype)testInstanceWithIDToken:(NSString *)idToken + accessToken:(NSString *)accessToken + expiresIn:(NSNumber *)expiresIn + refreshToken:(NSString *)refreshToken + authTime:(NSString *)authTime + tokenRequest:(OIDTokenRequest *)tokenRequest; + + (NSString *)idToken; + (NSString *)fatIDToken; ++ (NSString *)fatIDTokenWithAuthTime; + /** * @sub The subject of the ID token. * @exp The interval between 00:00:00 UTC on 1 January 1970 and the expiration date of the ID token. @@ -71,4 +81,6 @@ extern NSString * const kFatPictureURL; + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat; ++ (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat authTime:(NSString *)authTime; + @end diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m index 3285ec8d..bf2a5fa7 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m @@ -38,6 +38,7 @@ NSString *const kHostedDomain = @"fakehosteddomain.com"; NSString *const kIssuer = @"https://test.com"; NSString *const kAudience = @"audience"; +NSString *const kAuthTime = @"1757753868"; NSTimeInterval const kIDTokenExpires = 1000; NSTimeInterval const kIssuedAt = 0; @@ -70,6 +71,21 @@ + (instancetype)testInstanceWithIDToken:(NSString *)idToken expiresIn:(NSNumber *)expiresIn refreshToken:(NSString *)refreshToken tokenRequest:(OIDTokenRequest *)tokenRequest { + return [OIDTokenResponse testInstanceWithIDToken:idToken + accessToken:accessToken + expiresIn:expiresIn + refreshToken:refreshToken + authTime:nil + tokenRequest:tokenRequest]; +} + ++ (instancetype)testInstanceWithIDToken:(NSString *)idToken + accessToken:(NSString *)accessToken + expiresIn:(NSNumber *)expiresIn + refreshToken:(NSString *)refreshToken + authTime:(NSString *)authTime + tokenRequest:(OIDTokenRequest *)tokenRequest { + NSMutableDictionary *parameters = [[NSMutableDictionary alloc] initWithDictionary:@{ @"access_token" : accessToken ?: kAccessToken, @"expires_in" : expiresIn ?: @(kAccessTokenExpiresIn), @@ -93,11 +109,24 @@ + (NSString *)fatIDToken { return [self idTokenWithSub:kUserID exp:@(kIDTokenExpires) fat:YES]; } ++ (NSString *)fatIDTokenWithAuthTime { + return [self idTokenWithSub:kUserID exp:@(kIDTokenExpires) fat:YES authTime:kAuthTime]; +} + + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp { return [self idTokenWithSub:sub exp:exp fat:NO]; } -+ (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat { ++ (NSString *)idTokenWithSub:(NSString *)sub + exp:(NSNumber *)exp + fat:(BOOL)fat { + return [self idTokenWithSub:kUserID exp:exp fat:fat authTime:nil]; +} + ++ (NSString *)idTokenWithSub:(NSString *)sub + exp:(NSNumber *)exp + fat:(BOOL)fat + authTime:(NSString *)authTime{ NSError *error; NSDictionary *headerContents = @{ @"alg" : kAlg, @@ -110,7 +139,7 @@ + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat { if (error || !headerJson) { return nil; } - NSMutableDictionary *payloadContents = + NSMutableDictionary *payloadContents = [NSMutableDictionary dictionaryWithDictionary:@{ @"sub" : sub, @"hd" : kHostedDomain, @@ -127,6 +156,11 @@ + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat { kFatPictureURLKey : kFatPictureURL, }]; } + if (authTime) { + [payloadContents addEntriesFromDictionary:@{ + @"auth_time": kAuthTime, + }]; + } NSData *payloadJson = [NSJSONSerialization dataWithJSONObject:payloadContents options:NSJSONWritingPrettyPrinted error:&error]; From 40b969295bb20c33713d1367ba7dc8e292bdabbd Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:05:50 -0700 Subject: [PATCH 4/7] Updated sample app to support auth_time (#555) --- .../Services/GoogleSignInAuthenticator.swift | 9 +++- .../ViewModels/AuthenticationViewModel.swift | 54 +++++++++++++++++++ .../iOS/UserProfileView.swift | 3 ++ .../macOS/UserProfileView.swift | 3 ++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift index e12d79a2..88d00f2e 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift @@ -20,6 +20,7 @@ import GoogleSignIn /// An observable class for authenticating via Google. final class GoogleSignInAuthenticator: ObservableObject { private var authViewModel: AuthenticationViewModel + private var tokenClaims: Set = Set([GIDTokenClaim.authTime()]) /// Creates an instance of this authenticator. /// - parameter authViewModel: The view model this authenticator will set logged in status on. @@ -41,7 +42,8 @@ final class GoogleSignInAuthenticator: ObservableObject { withPresenting: rootViewController, hint: nil, additionalScopes: nil, - nonce: manualNonce + nonce: manualNonce, + tokenClaims: tokenClaims ) { signInResult, error in guard let signInResult = signInResult else { print("Error! \(String(describing: error))") @@ -66,7 +68,10 @@ final class GoogleSignInAuthenticator: ObservableObject { return } - GIDSignIn.sharedInstance.signIn(withPresenting: presentingWindow) { signInResult, error in + GIDSignIn.sharedInstance.signIn( + withPresenting: presentingWindow, + tokenClaims: tokenClaims + ) { signInResult, error in guard let signInResult = signInResult else { print("Error! \(String(describing: error))") return diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift index 15bee104..b528fc68 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift @@ -25,6 +25,19 @@ final class AuthenticationViewModel: ObservableObject { private var authenticator: GoogleSignInAuthenticator { return GoogleSignInAuthenticator(authViewModel: self) } + + /// The user's `auth_time` as found in `idToken`. + /// - note: If the user is logged out, then this will default to `nil`. + var authTime: Date? { + switch state { + case .signedIn(let user): + guard let idToken = user.idToken?.tokenString else { return nil } + return decodeAuthTime(fromJWT: idToken) + case .signedOut: + return nil + } + } + /// The user-authorized scopes. /// - note: If the user is logged out, then this will default to empty. var authorizedScopes: [String] { @@ -69,7 +82,48 @@ final class AuthenticationViewModel: ObservableObject { @MainActor func addBirthdayReadScope(completion: @escaping () -> Void) { authenticator.addBirthdayReadScope(completion: completion) } + + var formattedAuthTimeString: String? { + guard let date = authTime else { return nil } + let formatter = DateFormatter() + formatter.dateFormat = "MMM d, yyyy 'at' h:mm a" + return formatter.string(from: date) + } +} +private extension AuthenticationViewModel { + func decodeAuthTime(fromJWT jwt: String) -> Date? { + let segments = jwt.components(separatedBy: ".") + guard let parts = decodeJWTSegment(segments[1]), + let authTimeInterval = parts["auth_time"] as? TimeInterval else { + return nil + } + return Date(timeIntervalSince1970: authTimeInterval) + } + + func decodeJWTSegment(_ segment: String) -> [String: Any]? { + guard let segmentData = base64UrlDecode(segment), + let segmentJSON = try? JSONSerialization.jsonObject(with: segmentData, options: []), + let payload = segmentJSON as? [String: Any] else { + return nil + } + return payload + } + + func base64UrlDecode(_ value: String) -> Data? { + var base64 = value + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + let length = Double(base64.lengthOfBytes(using: String.Encoding.utf8)) + let requiredLength = 4 * ceil(length / 4.0) + let paddingLength = requiredLength - length + if paddingLength > 0 { + let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) + base64 = base64 + padding + } + return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) + } } extension AuthenticationViewModel { diff --git a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift index 93366f47..256b777b 100644 --- a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift @@ -35,6 +35,9 @@ struct UserProfileView: View { Text(userProfile.name) .font(.headline) Text(userProfile.email) + if let authTimeString = authViewModel.formattedAuthTimeString { + Text("Last sign-in date: \(authTimeString)") + } } } NavigationLink(NSLocalizedString("View Days Until Birthday", comment: "View birthday days"), diff --git a/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift index d7faad97..3fddc744 100644 --- a/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift @@ -19,6 +19,9 @@ struct UserProfileView: View { Text(userProfile.name) .font(.headline) Text(userProfile.email) + if let authTimeString = authViewModel.formattedAuthTimeString { + Text("Last sign-in date: \(authTimeString)") + } } } Button(NSLocalizedString("Sign Out", comment: "Sign out button"), action: signOut) From 5a0d207e5f72c0753db535d861d367fa4f31e28b Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:31:37 -0700 Subject: [PATCH 5/7] =?UTF-8?q?Update=20`AddScopes`=20to=20include=20previ?= =?UTF-8?q?ously=20requested=20`tokenClaims`=20in=20the=E2=80=A6=20(#557)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GoogleSignIn/Sources/GIDSignIn.m | 45 +++++-- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 122 +++++++++++++++++- .../Unit/OIDAuthorizationRequest+Testing.h | 4 +- .../Unit/OIDAuthorizationRequest+Testing.m | 8 +- .../Unit/OIDAuthorizationResponse+Testing.m | 5 +- 5 files changed, 163 insertions(+), 21 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 4da18b2c..ff4fcdae 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -367,6 +367,14 @@ - (void)addScopes:(NSArray *)scopes addScopesFlow:YES completion:completion]; + OIDAuthorizationRequest *lastAuthorizationRequest = + self.currentUser.authState.lastAuthorizationResponse.request; + NSString *lastTokenClaimsAsJSON = + lastAuthorizationRequest.additionalParameters[kTokenClaimsParameter]; + if (lastTokenClaimsAsJSON) { + options.tokenClaimsAsJSON = lastTokenClaimsAsJSON; + } + NSSet *requestedScopes = [NSSet setWithArray:scopes]; NSMutableSet *grantedScopes = [NSMutableSet setWithArray:self.currentUser.grantedScopes]; @@ -499,6 +507,14 @@ - (void)addScopes:(NSArray *)scopes addScopesFlow:YES completion:completion]; + OIDAuthorizationRequest *lastAuthorizationRequest = + self.currentUser.authState.lastAuthorizationResponse.request; + NSString *lastTokenClaimsAsJSON = + lastAuthorizationRequest.additionalParameters[kTokenClaimsParameter]; + if (lastTokenClaimsAsJSON) { + options.tokenClaimsAsJSON = lastTokenClaimsAsJSON; + } + NSSet *requestedScopes = [NSSet setWithArray:scopes]; NSMutableSet *grantedScopes = [NSMutableSet setWithArray:self.currentUser.grantedScopes]; @@ -739,20 +755,23 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { } }]; } else { - NSError *claimsError; - - // If tokenClaims are invalid or JSON serialization fails, return with an error. - options.tokenClaimsAsJSON = [_tokenClaimsInternalOptions - validatedJSONStringForClaims:options.tokenClaims - error:&claimsError]; - if (claimsError) { - if (options.completion) { - self->_currentOptions = nil; - dispatch_async(dispatch_get_main_queue(), ^{ - options.completion(nil, claimsError); - }); + // Only serialize tokenClaims if options.tokenClaimsAsJSON isn't already set. + if (!options.tokenClaimsAsJSON) { + NSError *claimsError; + + // If tokenClaims are invalid or JSON serialization fails, return with an error. + options.tokenClaimsAsJSON = [_tokenClaimsInternalOptions + validatedJSONStringForClaims:options.tokenClaims + error:&claimsError]; + if (claimsError) { + if (options.completion) { + _currentOptions = nil; + dispatch_async(dispatch_get_main_queue(), ^{ + options.completion(nil, claimsError); + }); + } + return; } - return; } [self authenticateWithOptions:options]; } diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 17453785..55f239d0 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -644,6 +644,7 @@ - (void)testOAuthLogin { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -661,6 +662,7 @@ - (void)testOAuthLogin_RestoredSignIn { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:NO @@ -678,6 +680,7 @@ - (void)testOAuthLogin_RestoredSignInOldAccessToken { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:YES @@ -697,6 +700,7 @@ - (void)testOAuthLogin_AdditionalScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO tokenClaimsError:NO restoredSignIn:NO @@ -714,6 +718,7 @@ - (void)testOAuthLogin_AdditionalScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO tokenClaimsError:NO restoredSignIn:NO @@ -731,6 +736,7 @@ - (void)testOAuthLogin_AdditionalScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO tokenClaimsError:NO restoredSignIn:NO @@ -758,6 +764,7 @@ - (void)testOAuthLogin_WithTokenClaims_FormatsParametersCorrectly { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO tokenClaimsError:NO restoredSignIn:NO @@ -776,6 +783,7 @@ - (void)testOAuthLogin_WithTokenClaims_FormatsParametersCorrectly { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO tokenClaimsError:NO restoredSignIn:NO @@ -803,6 +811,7 @@ - (void)testOAuthLogin_WithTokenClaims_ReturnsIdTokenWithCorrectClaims { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO tokenClaimsError:NO restoredSignIn:NO @@ -838,6 +847,7 @@ - (void)testAddScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:NO @@ -856,11 +866,13 @@ - (void)testAddScopes { OCMStub([_user configuration]).andReturn(configuration); OCMStub([_user profile]).andReturn(profile); OCMStub([_user grantedScopes]).andReturn(@[kGrantedScope]); + OCMStub([_user authState]).andReturn(_authState); [self OAuthLoginWithAddScopesFlow:YES authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -888,6 +900,76 @@ - (void)testAddScopes { [profile stopMocking]; } +- (void)testAddScopes_WithPreviouslyRequestedClaims { + GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; + // Restore the previous sign-in account. This is the preparation for adding scopes. + OCMStub( + [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation) { + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO + keychainError:NO + tokenClaimsError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO + useAdditionalScopes:NO + additionalScopes:nil + manualNonce:nil + tokenClaims:[NSSet setWithObject:authTimeClaim]]; + + XCTAssertNotNil(_signIn.currentUser); + + id profile = OCMStrictClassMock([GIDProfileData class]); + OCMStub([profile email]).andReturn(kUserEmail); + + 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]); + OCMStub([_user authState]).andReturn(_authState); + + [self OAuthLoginWithAddScopesFlow:YES + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:YES + keychainError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO]; + + NSArray *grantedScopes; + NSString *grantedScopeString = _savedAuthorizationRequest.scope; + + if (grantedScopeString) { + NSCharacterSet *whiteSpaceChars = [NSCharacterSet whitespaceCharacterSet]; + grantedScopeString = + [grantedScopeString stringByTrimmingCharactersInSet:whiteSpaceChars]; + NSMutableArray *parsedScopes = + [[grantedScopeString componentsSeparatedByString:@" "] mutableCopy]; + [parsedScopes removeObject:@""]; + grantedScopes = [parsedScopes copy]; + } + + NSArray *expectedScopes = @[kNewScope, kGrantedScope]; + XCTAssertEqualObjects(grantedScopes, expectedScopes); + XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], + kNonEssentialAuthTimeClaimsJsonString, + @"Claims JSON should be correctly formatted"); + + [_user verify]; + [profile verify]; +} + - (void)testOpenIDRealm { _signIn.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId serverClientID:nil @@ -904,6 +986,7 @@ - (void)testOpenIDRealm { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -931,6 +1014,7 @@ - (void)testManualNonce { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO tokenClaimsError:NO restoredSignIn:NO @@ -959,6 +1043,7 @@ - (void)testOAuthLogin_LoginHint { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -984,6 +1069,7 @@ - (void)testOAuthLogin_HostedDomain { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -998,6 +1084,7 @@ - (void)testOAuthLogin_ConsentCanceled { authError:@"access_denied" tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1012,6 +1099,7 @@ - (void)testOAuthLogin_ModalCanceled { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1036,6 +1124,7 @@ - (void)testOAuthLogin_KeychainError { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:YES restoredSignIn:NO oldAccessToken:NO @@ -1056,6 +1145,7 @@ - (void)testOAuthLogin_TokenClaims_FailsWithError { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO tokenClaimsError:YES restoredSignIn:NO @@ -1093,6 +1183,7 @@ - (void)testSignOut { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:NO @@ -1339,6 +1430,7 @@ - (void)testEmmSupportRequestParameters { authError:nil tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1390,6 +1482,7 @@ - (void)testEmmPasscodeInfo { authError:nil tokenError:nil emmPasscodeInfoRequired:YES + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1421,6 +1514,7 @@ - (void)testAuthEndpointEMMError { authError:callbackParams[@"error"] tokenError:nil emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1458,6 +1552,7 @@ - (void)testTokenEndpointEMMError { authError:nil tokenError:emmError emmPasscodeInfoRequired:NO + tokenClaimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1542,6 +1637,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow authError:(NSString *)authError tokenError:(NSError *)tokenError emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired + tokenClaimsAsJSONRequired:(BOOL)tokenClaimsAsJSONRequired keychainError:(BOOL)keychainError restoredSignIn:(BOOL)restoredSignIn oldAccessToken:(BOOL)oldAccessToken @@ -1550,6 +1646,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow authError:authError tokenError:tokenError emmPasscodeInfoRequired:emmPasscodeInfoRequired + tokenClaimsAsJSONRequired:tokenClaimsAsJSONRequired keychainError:keychainError tokenClaimsError:NO restoredSignIn:restoredSignIn @@ -1566,6 +1663,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow authError:(NSString *)authError tokenError:(NSError *)tokenError emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired + tokenClaimsAsJSONRequired:(BOOL)tokenClaimsAsJSONRequired keychainError:(BOOL)keychainError tokenClaimsError:(BOOL)tokenClaimsError restoredSignIn:(BOOL)restoredSignIn @@ -1582,8 +1680,9 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow [[[_authState expect] andReturnValue:[NSNumber numberWithBool:isAuthorized]] isAuthorized]; } - NSDictionary *additionalParameters = emmPasscodeInfoRequired ? - @{ @"emm_passcode_info_required" : @"1" } : nil; + NSDictionary *additionalParameters = + [self additionalParametersWithEMMPasscodeInfoRequired:emmPasscodeInfoRequired + tokenClaimsAsJSONRequired:tokenClaimsAsJSONRequired]; OIDAuthorizationResponse *authResponse = [OIDAuthorizationResponse testInstanceWithAdditionalParameters:additionalParameters nonce:nonce @@ -1646,6 +1745,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow self->_authError = error; }; if (addScopesFlow) { + [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse]; [_signIn addScopes:@[kNewScope] #if TARGET_OS_IOS || TARGET_OS_MACCATALYST presentingViewController:_presentingViewController @@ -1846,4 +1946,22 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow } } +#pragma mark - Private Helpers + +- (NSDictionary *) + additionalParametersWithEMMPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired + tokenClaimsAsJSONRequired:(BOOL)tokenClaimsAsJSONRequired { + NSMutableDictionary *additionalParameters = + [NSMutableDictionary dictionary]; + + if (emmPasscodeInfoRequired) { + additionalParameters[@"emm_passcode_info_required"] = @"1"; + } + if (tokenClaimsAsJSONRequired) { + additionalParameters[@"claims"] = kNonEssentialAuthTimeClaimsJsonString; + } + + return [additionalParameters copy]; +} + @end diff --git a/GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h b/GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h index a70d5e80..d3e691c5 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h @@ -29,6 +29,6 @@ extern NSString * _Nonnull const OIDAuthorizationRequestTestingCodeVerifier; + (instancetype _Nonnull)testInstance; -+ (instancetype _Nonnull)testInstanceWithNonce:(nullable NSString *)nonce; - ++ (instancetype _Nonnull)testInstanceWithNonce:(nullable NSString *)nonce + additionalParameters:(nullable NSDictionary *)additionalParameters; @end diff --git a/GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.m b/GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.m index a8dd0b81..a45dac90 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.m @@ -32,10 +32,12 @@ @implementation OIDAuthorizationRequest (Testing) + (instancetype)testInstance { - return [self testInstanceWithNonce:nil]; + return [self testInstanceWithNonce:nil additionalParameters:nil]; } -+ (instancetype)testInstanceWithNonce:(nullable NSString *)nonce { ++ (instancetype)testInstanceWithNonce:(nullable NSString *)nonce + additionalParameters: + (nullable NSDictionary *)additionalParameters { return [[OIDAuthorizationRequest alloc] initWithConfiguration:[OIDServiceConfiguration testInstance] clientId:OIDAuthorizationRequestTestingClientID @@ -44,7 +46,7 @@ + (instancetype)testInstanceWithNonce:(nullable NSString *)nonce { redirectURL:[NSURL URLWithString:@"http://test.com"] responseType:OIDResponseTypeCode nonce:nonce - additionalParameters:nil]; + additionalParameters:additionalParameters]; } @end diff --git a/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m b/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m index cc4f1211..5404d411 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m @@ -46,7 +46,10 @@ + (instancetype)testInstanceWithAdditionalParameters: [parameters addEntriesFromDictionary:additionalParameters]; } } - return [[OIDAuthorizationResponse alloc] initWithRequest:[OIDAuthorizationRequest testInstanceWithNonce:nonce] + OIDAuthorizationRequest *request = + [OIDAuthorizationRequest testInstanceWithNonce:nonce + additionalParameters:additionalParameters]; + return [[OIDAuthorizationResponse alloc] initWithRequest:request parameters:parameters]; } From 30ee7a54c9ec459c75dd331fc19de2a1d6d6cd5c Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Mon, 29 Sep 2025 08:33:01 -0700 Subject: [PATCH 6/7] Renamed `tokenClaims to `claims` throughout the code. (#558) --- .../Sources/{GIDTokenClaim.m => GIDClaim.m} | 10 +- ...alOptions.h => GIDClaimsInternalOptions.h} | 18 +-- ...alOptions.m => GIDClaimsInternalOptions.m} | 36 ++--- GoogleSignIn/Sources/GIDSignIn.m | 76 +++++----- .../Sources/GIDSignInInternalOptions.h | 10 +- .../Sources/GIDSignInInternalOptions.m | 10 +- .../{GIDTokenClaim.h => GIDClaim.h} | 2 +- .../Sources/Public/GoogleSignIn/GIDSignIn.h | 50 +++---- .../Public/GoogleSignIn/GoogleSignIn.h | 2 +- .../{GIDTokenClaimTest.m => GIDClaimTest.m} | 18 +-- ...sTest.m => GIDClaimsInternalOptionsTest.m} | 36 ++--- .../Tests/Unit/GIDSignInInternalOptionsTest.m | 10 +- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 138 +++++++++--------- .../Services/GoogleSignInAuthenticator.swift | 6 +- 14 files changed, 211 insertions(+), 211 deletions(-) rename GoogleSignIn/Sources/{GIDTokenClaim.m => GIDClaim.m} (89%) rename GoogleSignIn/Sources/{GIDTokenClaimsInternalOptions.h => GIDClaimsInternalOptions.h} (75%) rename GoogleSignIn/Sources/{GIDTokenClaimsInternalOptions.m => GIDClaimsInternalOptions.m} (70%) rename GoogleSignIn/Sources/Public/GoogleSignIn/{GIDTokenClaim.h => GIDClaim.h} (97%) rename GoogleSignIn/Tests/Unit/{GIDTokenClaimTest.m => GIDClaimTest.m} (69%) rename GoogleSignIn/Tests/Unit/{GIDTokenClaimsInternalOptionsTest.m => GIDClaimsInternalOptionsTest.m} (70%) diff --git a/GoogleSignIn/Sources/GIDTokenClaim.m b/GoogleSignIn/Sources/GIDClaim.m similarity index 89% rename from GoogleSignIn/Sources/GIDTokenClaim.m rename to GoogleSignIn/Sources/GIDClaim.m index 61e83d6a..13792cae 100644 --- a/GoogleSignIn/Sources/GIDTokenClaim.m +++ b/GoogleSignIn/Sources/GIDClaim.m @@ -14,19 +14,19 @@ * limitations under the License. */ -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h" NSString * const kAuthTimeClaimName = @"auth_time"; // Private interface to declare the internal initializer -@interface GIDTokenClaim () +@interface GIDClaim () - (instancetype)initWithName:(NSString *)name essential:(BOOL)essential NS_DESIGNATED_INITIALIZER; @end -@implementation GIDTokenClaim +@implementation GIDClaim // Private designated initializer - (instancetype)initWithName:(NSString *)name essential:(BOOL)essential { @@ -57,12 +57,12 @@ - (BOOL)isEqual:(id)object { } // 2. Check if the other object is not a GIDTokenClaim instance. - if (![object isKindOfClass:[GIDTokenClaim class]]) { + if (![object isKindOfClass:[GIDClaim class]]) { return NO; } // 3. Compare the properties that define equality. - GIDTokenClaim *other = (GIDTokenClaim *)object; + GIDClaim *other = (GIDClaim *)object; return [self.name isEqualToString:other.name] && self.isEssential == other.isEssential; } diff --git a/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h b/GoogleSignIn/Sources/GIDClaimsInternalOptions.h similarity index 75% rename from GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h rename to GoogleSignIn/Sources/GIDClaimsInternalOptions.h index adc56dd1..5306bcff 100644 --- a/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h +++ b/GoogleSignIn/Sources/GIDClaimsInternalOptions.h @@ -16,21 +16,21 @@ #import -@class GIDTokenClaim; +@class GIDClaim; NS_ASSUME_NONNULL_BEGIN -extern NSString *const kGIDTokenClaimErrorDescription; -extern NSString *const kGIDTokenClaimEssentialPropertyKeyName; -extern NSString *const kGIDTokenClaimKeyName; +extern NSString *const kGIDClaimErrorDescription; +extern NSString *const kGIDClaimEssentialPropertyKeyName; +extern NSString *const kGIDClaimKeyName; @protocol GIDJSONSerializer; /** - * An internal utility class for processing and serializing the `NSSet` of `GIDTokenClaim` objects + * An internal utility class for processing and serializing the `NSSet` of `GIDClaim` objects * into the `JSON` format required for an `OIDAuthorizationRequest`. */ -@interface GIDTokenClaimsInternalOptions : NSObject +@interface GIDClaimsInternalOptions : NSObject - (instancetype)init; @@ -38,16 +38,16 @@ extern NSString *const kGIDTokenClaimKeyName; (id)jsonSerializer NS_DESIGNATED_INITIALIZER; /** - * Processes the `NSSet` of `GIDTokenClaim` objects, handling ambiguous claims, + * Processes the `NSSet` of `GIDClaim` objects, handling ambiguous claims, * and returns a `JSON` string. * - * @param claims The `NSSet` of `GIDTokenClaim` objects provided by the developer. + * @param claims The `NSSet` of `GIDClaim` objects provided by the developer. * @param error A pointer to an `NSError` object to be populated if an error occurs (e.g., if a * claim is requested as both essential and non-essential). * @return A `JSON` string representing the claims request, or `nil` if the input is empty or an * error occurs. */ -- (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet *)claims +- (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet *)claims error:(NSError *_Nullable *_Nullable)error; @end diff --git a/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m b/GoogleSignIn/Sources/GIDClaimsInternalOptions.m similarity index 70% rename from GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m rename to GoogleSignIn/Sources/GIDClaimsInternalOptions.m index 049f00e1..44a01cf0 100644 --- a/GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m +++ b/GoogleSignIn/Sources/GIDClaimsInternalOptions.m @@ -14,24 +14,24 @@ * limitations under the License. */ -#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h" +#import "GoogleSignIn/Sources/GIDClaimsInternalOptions.h" #import "GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h" #import "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h" -NSString * const kGIDTokenClaimErrorDescription = +NSString * const kGIDClaimErrorDescription = @"The claim was requested as both essential and non-essential. " @"Please provide only one version."; -NSString * const kGIDTokenClaimEssentialPropertyKey = @"essential"; -NSString * const kGIDTokenClaimKeyName = @"id_token"; +NSString * const kGIDClaimEssentialPropertyKey = @"essential"; +NSString * const kGIDClaimKeyName = @"id_token"; -@interface GIDTokenClaimsInternalOptions () +@interface GIDClaimsInternalOptions () @property(nonatomic, readonly) id jsonSerializer; @end -@implementation GIDTokenClaimsInternalOptions +@implementation GIDClaimsInternalOptions - (instancetype)init { return [self initWithJSONSerializer:[[GIDJSONSerializerImpl alloc] init]]; @@ -44,18 +44,18 @@ - (instancetype)initWithJSONSerializer:(id)jsonSerializer { return self; } -- (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet *)claims +- (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet *)claims error:(NSError *_Nullable *_Nullable)error { if (!claims || claims.count == 0) { return nil; } // === Step 1: Check for claims with ambiguous essential property. === - NSMutableDictionary *validTokenClaims = + NSMutableDictionary *validClaims = [[NSMutableDictionary alloc] init]; - for (GIDTokenClaim *currentClaim in claims) { - GIDTokenClaim *existingClaim = validTokenClaims[currentClaim.name]; + for (GIDClaim *currentClaim in claims) { + GIDClaim *existingClaim = validClaims[currentClaim.name]; // Check for a conflict: a claim with the same name but different essentiality. if (existingClaim && existingClaim.isEssential != currentClaim.isEssential) { @@ -63,26 +63,26 @@ - (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet *tokenClaimsDictionary = + NSMutableDictionary *claimsDictionary = [[NSMutableDictionary alloc] init]; - for (GIDTokenClaim *claim in validTokenClaims.allValues) { + for (GIDClaim *claim in validClaims.allValues) { if (claim.isEssential) { - tokenClaimsDictionary[claim.name] = @{ kGIDTokenClaimEssentialPropertyKey: @YES }; + claimsDictionary[claim.name] = @{ kGIDClaimEssentialPropertyKey: @YES }; } else { - tokenClaimsDictionary[claim.name] = @{ kGIDTokenClaimEssentialPropertyKey: @NO }; + claimsDictionary[claim.name] = @{ kGIDClaimEssentialPropertyKey: @NO }; } } NSDictionary *finalRequestDictionary = - @{ kGIDTokenClaimKeyName: tokenClaimsDictionary }; + @{ kGIDClaimKeyName: claimsDictionary }; // === Step 3: Serialize the final dictionary into a JSON string === return [_jsonSerializer stringWithJSONObject:finalRequestDictionary error:error]; diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index ff4fcdae..138337c2 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -28,7 +28,7 @@ #import "GoogleSignIn/Sources/GIDCallbackQueue.h" #import "GoogleSignIn/Sources/GIDScopes.h" #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" -#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h" +#import "GoogleSignIn/Sources/GIDClaimsInternalOptions.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import #import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h" @@ -138,7 +138,7 @@ static NSString *const kHostedDomainParameter = @"hd"; // Parameter for requesting the token claims. -static NSString *const kTokenClaimsParameter = @"claims"; +static NSString *const kClaimsParameter = @"claims"; // Parameters for auth and token exchange endpoints using App Attest. static NSString *const kClientAssertionParameter = @"client_assertion"; @@ -173,7 +173,7 @@ @implementation GIDSignIn { // set when a sign-in flow is begun via |signInWithOptions:| when the options passed don't // represent a sign in continuation. GIDSignInInternalOptions *_currentOptions; - GIDTokenClaimsInternalOptions *_tokenClaimsInternalOptions; + GIDClaimsInternalOptions *_claimsInternalOptions; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST GIDAppCheck *_appCheck API_AVAILABLE(ios(14)); #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST @@ -293,40 +293,40 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon hint:hint additionalScopes:additionalScopes nonce:nonce - tokenClaims:nil + claims:nil completion:completion]; } - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { [self signInWithPresentingViewController:presentingViewController hint:nil - tokenClaims:tokenClaims + claims:claims completion:completion]; } - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { [self signInWithPresentingViewController:presentingViewController hint:hint additionalScopes:@[] - tokenClaims:tokenClaims + claims:claims completion:completion]; } - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { [self signInWithPresentingViewController:presentingViewController hint:hint additionalScopes:additionalScopes nonce:nil - tokenClaims:tokenClaims + claims:claims completion:completion]; } @@ -335,7 +335,7 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes nonce:(nullable NSString *)nonce - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration @@ -344,7 +344,7 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon addScopesFlow:NO scopes:additionalScopes nonce:nonce - tokenClaims:tokenClaims + claims:claims completion:completion]; [self signInWithOptions:options]; } @@ -369,10 +369,10 @@ - (void)addScopes:(NSArray *)scopes OIDAuthorizationRequest *lastAuthorizationRequest = self.currentUser.authState.lastAuthorizationResponse.request; - NSString *lastTokenClaimsAsJSON = - lastAuthorizationRequest.additionalParameters[kTokenClaimsParameter]; - if (lastTokenClaimsAsJSON) { - options.tokenClaimsAsJSON = lastTokenClaimsAsJSON; + NSString *lastClaimsAsJSON = + lastAuthorizationRequest.additionalParameters[kClaimsParameter]; + if (lastClaimsAsJSON) { + options.claimsAsJSON = lastClaimsAsJSON; } NSSet *requestedScopes = [NSSet setWithArray:scopes]; @@ -441,40 +441,40 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:hint additionalScopes:additionalScopes nonce:nonce - tokenClaims:nil + claims:nil completion:completion]; } - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { [self signInWithPresentingWindow:presentingWindow hint:nil - tokenClaims:tokenClaims + claims:claims completion:completion]; } - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { [self signInWithPresentingWindow:presentingWindow hint:hint additionalScopes:@[] - tokenClaims:tokenClaims + claims:claims completion:completion]; } - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { [self signInWithPresentingWindow:presentingWindow hint:hint additionalScopes:additionalScopes nonce:nil - tokenClaims:tokenClaims + claims:claims completion:completion]; } @@ -482,7 +482,7 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes nonce:(nullable NSString *)nonce - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration @@ -491,7 +491,7 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow addScopesFlow:NO scopes:additionalScopes nonce:nonce - tokenClaims:tokenClaims + claims:claims completion:completion]; [self signInWithOptions:options]; } @@ -509,10 +509,10 @@ - (void)addScopes:(NSArray *)scopes OIDAuthorizationRequest *lastAuthorizationRequest = self.currentUser.authState.lastAuthorizationResponse.request; - NSString *lastTokenClaimsAsJSON = - lastAuthorizationRequest.additionalParameters[kTokenClaimsParameter]; - if (lastTokenClaimsAsJSON) { - options.tokenClaimsAsJSON = lastTokenClaimsAsJSON; + NSString *lastClaimsAsJSON = + lastAuthorizationRequest.additionalParameters[kClaimsParameter]; + if (lastClaimsAsJSON) { + options.claimsAsJSON = lastClaimsAsJSON; } NSSet *requestedScopes = [NSSet setWithArray:scopes]; @@ -660,7 +660,7 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore self = [super init]; if (self) { _keychainStore = keychainStore; - _tokenClaimsInternalOptions = [[GIDTokenClaimsInternalOptions alloc] init]; + _claimsInternalOptions = [[GIDClaimsInternalOptions alloc] init]; // Get the bundle of the current executable. NSBundle *bundle = NSBundle.mainBundle; @@ -755,14 +755,14 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { } }]; } else { - // Only serialize tokenClaims if options.tokenClaimsAsJSON isn't already set. - if (!options.tokenClaimsAsJSON) { + // Only serialize claims if options.claimsAsJSON isn't already set. + if (!options.claimsAsJSON) { NSError *claimsError; - // If tokenClaims are invalid or JSON serialization fails, return with an error. - options.tokenClaimsAsJSON = [_tokenClaimsInternalOptions - validatedJSONStringForClaims:options.tokenClaims - error:&claimsError]; + // If claims are invalid or JSON serialization fails, return with an error. + options.claimsAsJSON = [_claimsInternalOptions + validatedJSONStringForClaims:options.claims + error:&claimsError]; if (claimsError) { if (options.completion) { _currentOptions = nil; @@ -902,8 +902,8 @@ - (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options comp if (options.configuration.hostedDomain) { additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain; } - if (options.tokenClaimsAsJSON) { - additionalParameters[kTokenClaimsParameter] = options.tokenClaimsAsJSON; + if (options.claimsAsJSON) { + additionalParameters[kClaimsParameter] = options.claimsAsJSON; } #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDSignInInternalOptions.h b/GoogleSignIn/Sources/GIDSignInInternalOptions.h index f21d75d7..9e3b7ce0 100644 --- a/GoogleSignIn/Sources/GIDSignInInternalOptions.h +++ b/GoogleSignIn/Sources/GIDSignInInternalOptions.h @@ -68,11 +68,11 @@ NS_ASSUME_NONNULL_BEGIN /// and to mitigate replay attacks. @property(nonatomic, readonly, copy, nullable) NSString *nonce; -/// The tokenClaims requested by the Clients. -@property(nonatomic, readonly, copy, nullable) NSSet *tokenClaims; +/// The claims requested by the Clients. +@property(nonatomic, readonly, copy, nullable) NSSet *claims; /// The JSON token claims to be used during the flow. -@property(nonatomic, copy, nullable) NSString *tokenClaimsAsJSON; +@property(nonatomic, copy, nullable) NSString *claimsAsJSON; /// Creates the default options. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST @@ -88,7 +88,7 @@ NS_ASSUME_NONNULL_BEGIN addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes nonce:(nullable NSString *)nonce - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion; #elif TARGET_OS_OSX @@ -104,7 +104,7 @@ NS_ASSUME_NONNULL_BEGIN addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes nonce:(nullable NSString *)nonce - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion; #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDSignInInternalOptions.m b/GoogleSignIn/Sources/GIDSignInInternalOptions.m index 0799906a..4a87bddf 100644 --- a/GoogleSignIn/Sources/GIDSignInInternalOptions.m +++ b/GoogleSignIn/Sources/GIDSignInInternalOptions.m @@ -32,7 +32,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes nonce:(nullable NSString *)nonce - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { #elif TARGET_OS_OSX + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration @@ -41,7 +41,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con addScopesFlow:(BOOL)addScopesFlow scopes:(nullable NSArray *)scopes nonce:(nullable NSString *)nonce - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable GIDSignInCompletion)completion { #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init]; @@ -59,7 +59,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con options->_completion = completion; options->_scopes = [GIDScopes scopesWithBasicProfile:scopes]; options->_nonce = nonce; - options->_tokenClaims = tokenClaims; + options->_claims = claims; } return options; } @@ -87,7 +87,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con addScopesFlow:addScopesFlow scopes:@[] nonce:nil - tokenClaims:nil + claims:nil completion:completion]; return options; } @@ -124,7 +124,7 @@ - (instancetype)optionsWithExtraParameters:(NSDictionary *)extraParams options->_loginHint = _loginHint; options->_completion = _completion; options->_scopes = _scopes; - options->_tokenClaims = _tokenClaims; + options->_claims = _claims; options->_extraParams = [extraParams copy]; } return options; diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h similarity index 97% rename from GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h rename to GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h index 7a2351cb..bc266930 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h @@ -23,7 +23,7 @@ extern NSString *const kAuthTimeClaimName; /** * An object representing a single OIDC claim to be requested for an ID token. */ -@interface GIDTokenClaim : NSObject +@interface GIDClaim : NSObject /// The name of the claim, e.g., "auth_time". @property (nonatomic, readonly) NSString *name; diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index 768d1764..c02d97f8 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -26,7 +26,7 @@ @class GIDConfiguration; @class GIDGoogleUser; @class GIDSignInResult; -@class GIDTokenClaim; +@class GIDClaim; NS_ASSUME_NONNULL_BEGIN @@ -226,7 +226,7 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") NSError *_Nullable error))completion NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); -/// Starts an interactive sign-in flow on iOS using the provided tokenClaims. +/// Starts an interactive sign-in flow on iOS using the provided claims. /// /// The completion will be called at the end of this process. Any saved sign-in state will be /// replaced by the result of this flow. Note that this method should not be called when the app is @@ -234,17 +234,17 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. /// /// @param presentingViewController The view controller used to present the authorization flow. -/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param claims An optional `NSSet` of claims to request. /// @param completion The optional block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion: (nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); -/// Starts an interactive sign-in flow on iOS using the provided hint and tokenClaims. +/// Starts an interactive sign-in flow on iOS using the provided hint and claims. /// /// The completion will be called at the end of this process. Any saved sign-in state will be /// replaced by the result of this flow. Note that this method should not be called when the app is @@ -254,19 +254,19 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// @param presentingViewController The view controller used to present the authorization flow. /// @param hint An optional hint for the authorization server, for example the user's ID or email /// address, to be prefilled if possible. -/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param claims An optional `NSSet` of claims to request. /// @param completion The optional block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion: (nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); /// Starts an interactive sign-in flow on iOS using the provided hint, additional scopes, -/// and tokenClaims. +/// and claims. /// /// The completion will be called at the end of this process. Any saved sign-in state will be /// replaced by the result of this flow. Note that this method should not be called when the app is @@ -277,20 +277,20 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// @param hint An optional hint for the authorization server, for example the user's ID or email /// address, to be prefilled if possible. /// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. -/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param claims An optional `NSSet` of claims to request. /// @param completion The optional block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion: (nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions."); /// Starts an interactive sign-in flow on iOS using the provided hint, additional scopes, nonce, -/// and tokenClaims. +/// and claims. /// /// The completion will be called at the end of this process. Any saved sign-in state will be /// replaced by the result of this flow. Note that this method should not be called when the app is @@ -302,14 +302,14 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// address, to be prefilled if possible. /// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. /// @param nonce A custom nonce. -/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param claims An optional `NSSet` of claims to request. /// @param completion The optional block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes nonce:(nullable NSString *)nonce - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion: (nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion @@ -388,7 +388,7 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion; -/// Starts an interactive sign-in flow on macOS using the provided tokenClaims. +/// Starts an interactive sign-in flow on macOS using the provided claims. /// /// The completion will be called at the end of this process. Any saved sign-in state will be /// replaced by the result of this flow. Note that this method should not be called when the app is @@ -396,15 +396,15 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in. /// /// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`. -/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param claims An optional `NSSet` of claims to request. /// @param completion The optional block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion; -/// Starts an interactive sign-in flow on macOS using the provided hint and tokenClaims. +/// Starts an interactive sign-in flow on macOS using the provided hint and claims. /// /// The completion will be called at the end of this process. Any saved sign-in state will be /// replaced by the result of this flow. Note that this method should not be called when the app is @@ -414,17 +414,17 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`. /// @param hint An optional hint for the authorization server, for example the user's ID or email /// address, to be prefilled if possible. -/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param claims An optional `NSSet` of claims to request. /// @param completion The optional block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion; /// Starts an interactive sign-in flow on macOS using the provided hint, additional scopes, -/// and tokenClaims. +/// and claims. /// /// The completion will be called at the end of this process. Any saved sign-in state will be /// replaced by the result of this flow. Note that this method should not be called when the app is @@ -435,18 +435,18 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// @param hint An optional hint for the authorization server, for example the user's ID or email /// address, to be prefilled if possible. /// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. -/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param claims An optional `NSSet` of claims to request. /// @param completion The optional block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion; /// Starts an interactive sign-in flow on macOS using the provided hint, additional scopes, nonce, -/// and tokenClaims. +/// and claims. /// /// The completion will be called at the end of this process. Any saved sign-in state will be /// replaced by the result of this flow. Note that this method should not be called when the app is @@ -458,14 +458,14 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// address, to be prefilled if possible. /// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes. /// @param nonce A custom nonce. -/// @param tokenClaims An optional `NSSet` of tokenClaims to request. +/// @param claims An optional `NSSet` of claims to request. /// @param completion The optional block that is called on completion. This block will /// be called asynchronously on the main queue. - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow hint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes nonce:(nullable NSString *)nonce - tokenClaims:(nullable NSSet *)tokenClaims + claims:(nullable NSSet *)claims completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion; diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h index 02935be8..2f25ae60 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h @@ -24,7 +24,7 @@ #import "GIDSignIn.h" #import "GIDToken.h" #import "GIDSignInResult.h" -#import "GIDTokenClaim.h" +#import "GIDClaim.h" #if TARGET_OS_IOS || TARGET_OS_MACCATALYST #import "GIDSignInButton.h" #endif diff --git a/GoogleSignIn/Tests/Unit/GIDTokenClaimTest.m b/GoogleSignIn/Tests/Unit/GIDClaimTest.m similarity index 69% rename from GoogleSignIn/Tests/Unit/GIDTokenClaimTest.m rename to GoogleSignIn/Tests/Unit/GIDClaimTest.m index 145e46f6..e014d885 100644 --- a/GoogleSignIn/Tests/Unit/GIDTokenClaimTest.m +++ b/GoogleSignIn/Tests/Unit/GIDClaimTest.m @@ -14,35 +14,35 @@ #import -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h" -@interface GIDTokenClaimTest : XCTestCase +@interface GIDClaimTest : XCTestCase @end -@implementation GIDTokenClaimTest +@implementation GIDClaimTest - (void)testAuthTimeClaim_PropertiesAreCorrect { - GIDTokenClaim *claim = [GIDTokenClaim authTimeClaim]; + GIDClaim *claim = [GIDClaim authTimeClaim]; XCTAssertEqualObjects(claim.name, kAuthTimeClaimName); XCTAssertFalse(claim.isEssential); } - (void)testEssentialAuthTimeClaim_PropertiesAreCorrect { - GIDTokenClaim *claim = [GIDTokenClaim essentialAuthTimeClaim]; + GIDClaim *claim = [GIDClaim essentialAuthTimeClaim]; XCTAssertEqualObjects(claim.name, kAuthTimeClaimName); XCTAssertTrue(claim.isEssential); } - (void)testEquality_WithEqualClaims { - GIDTokenClaim *claim1 = [GIDTokenClaim authTimeClaim]; - GIDTokenClaim *claim2 = [GIDTokenClaim authTimeClaim]; + GIDClaim *claim1 = [GIDClaim authTimeClaim]; + GIDClaim *claim2 = [GIDClaim authTimeClaim]; XCTAssertEqualObjects(claim1, claim2); XCTAssertEqual(claim1.hash, claim2.hash); } - (void)testEquality_WithUnequalClaims { - GIDTokenClaim *claim1 = [GIDTokenClaim authTimeClaim]; - GIDTokenClaim *claim2 = [GIDTokenClaim essentialAuthTimeClaim]; + GIDClaim *claim1 = [GIDClaim authTimeClaim]; + GIDClaim *claim2 = [GIDClaim essentialAuthTimeClaim]; XCTAssertNotEqualObjects(claim1, claim2); } diff --git a/GoogleSignIn/Tests/Unit/GIDTokenClaimsInternalOptionsTest.m b/GoogleSignIn/Tests/Unit/GIDClaimsInternalOptionsTest.m similarity index 70% rename from GoogleSignIn/Tests/Unit/GIDTokenClaimsInternalOptionsTest.m rename to GoogleSignIn/Tests/Unit/GIDClaimsInternalOptionsTest.m index 4c90998f..93b6acc5 100644 --- a/GoogleSignIn/Tests/Unit/GIDTokenClaimsInternalOptionsTest.m +++ b/GoogleSignIn/Tests/Unit/GIDClaimsInternalOptionsTest.m @@ -16,59 +16,59 @@ #import "GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h" #import "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h" -#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h" +#import "GoogleSignIn/Sources/GIDClaimsInternalOptions.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h" static NSString *const kEssentialAuthTimeExpectedJSON = @"{\"id_token\":{\"auth_time\":{\"essential\":true}}}"; static NSString *const kNonEssentialAuthTimeExpectedJSON = @"{\"id_token\":{\"auth_time\":{\"essential\":false}}}"; -@interface GIDTokenClaimsInternalOptionsTest : XCTestCase +@interface GIDClaimsInternalOptionsTest : XCTestCase @property(nonatomic) GIDFakeJSONSerializerImpl *jsonSerializerFake; -@property(nonatomic) GIDTokenClaimsInternalOptions *tokenClaimsInternalOptions; +@property(nonatomic) GIDClaimsInternalOptions *claimsInternalOptions; @end -@implementation GIDTokenClaimsInternalOptionsTest +@implementation GIDClaimsInternalOptionsTest - (void)setUp { [super setUp]; _jsonSerializerFake = [[GIDFakeJSONSerializerImpl alloc] init]; - _tokenClaimsInternalOptions = [[GIDTokenClaimsInternalOptions alloc] initWithJSONSerializer:_jsonSerializerFake]; + _claimsInternalOptions = [[GIDClaimsInternalOptions alloc] initWithJSONSerializer:_jsonSerializerFake]; } - (void)tearDown { _jsonSerializerFake = nil; - _tokenClaimsInternalOptions = nil; + _claimsInternalOptions = nil; [super tearDown]; } #pragma mark - Input Validation Tests - (void)testValidatedJSONStringForClaims_WithNilInput_ShouldReturnNil { - XCTAssertNil([_tokenClaimsInternalOptions validatedJSONStringForClaims:nil error:nil]); + XCTAssertNil([_claimsInternalOptions validatedJSONStringForClaims:nil error:nil]); } - (void)testValidatedJSONStringForClaims_WithEmptyInput_ShouldReturnNil { - XCTAssertNil([_tokenClaimsInternalOptions validatedJSONStringForClaims:[NSSet set] error:nil]); + XCTAssertNil([_claimsInternalOptions validatedJSONStringForClaims:[NSSet set] error:nil]); } #pragma mark - Correct Formatting Tests - (void)testValidatedJSONStringForClaims_WithNonEssentialClaim_IsCorrectlyFormatted { - NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]]; + NSSet *claims = [NSSet setWithObject:[GIDClaim authTimeClaim]]; NSError *error; - NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error]; + NSString *result = [_claimsInternalOptions validatedJSONStringForClaims:claims error:&error]; XCTAssertNil(error); XCTAssertEqualObjects(result, kNonEssentialAuthTimeExpectedJSON); } - (void)testValidatedJSONStringForClaims_WithEssentialClaim_IsCorrectlyFormatted { - NSSet *claims = [NSSet setWithObject:[GIDTokenClaim essentialAuthTimeClaim]]; + NSSet *claims = [NSSet setWithObject:[GIDClaim essentialAuthTimeClaim]]; NSError *error; - NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error]; + NSString *result = [_claimsInternalOptions validatedJSONStringForClaims:claims error:&error]; XCTAssertNil(error); XCTAssertEqualObjects(result, kEssentialAuthTimeExpectedJSON); @@ -77,11 +77,11 @@ - (void)testValidatedJSONStringForClaims_WithEssentialClaim_IsCorrectlyFormatted #pragma mark - Client Error Handling Tests - (void)testValidatedJSONStringForClaims_WithConflictingClaims_ReturnsNilAndPopulatesError { - NSSet *claims = [NSSet setWithObjects:[GIDTokenClaim authTimeClaim], - [GIDTokenClaim essentialAuthTimeClaim], + NSSet *claims = [NSSet setWithObjects:[GIDClaim authTimeClaim], + [GIDClaim essentialAuthTimeClaim], nil]; NSError *error; - NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error]; + NSString *result = [_claimsInternalOptions validatedJSONStringForClaims:claims error:&error]; XCTAssertNil(result, @"Method should return nil for conflicting claims."); XCTAssertNotNil(error, @"An error object should be populated."); @@ -91,7 +91,7 @@ - (void)testValidatedJSONStringForClaims_WithConflictingClaims_ReturnsNilAndPopu } - (void)testValidatedJSONStringForClaims_WhenSerializationFails_ReturnsNilAndError { - NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]]; + NSSet *claims = [NSSet setWithObject:[GIDClaim authTimeClaim]]; NSError *expectedJSONError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeJSONSerializationFailure userInfo:@{ @@ -99,7 +99,7 @@ - (void)testValidatedJSONStringForClaims_WhenSerializationFails_ReturnsNilAndErr }]; _jsonSerializerFake.serializationError = expectedJSONError; NSError *actualError; - NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims + NSString *result = [_claimsInternalOptions validatedJSONStringForClaims:claims error:&actualError]; XCTAssertNil(result, @"The result should be nil when JSON serialization fails."); diff --git a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m index 1d6c2b3c..13fcac4d 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m @@ -17,7 +17,7 @@ #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h" #ifdef SWIFT_PACKAGE @import OCMock; @@ -74,7 +74,7 @@ - (void)testDefaultOptions_withAllParameters_initializesPropertiesCorrectly { NSString *loginHint = @"login_hint"; NSArray *scopes = @[@"scope1", @"scope2"]; NSString *nonce = @"test_nonce"; - NSSet *tokenClaims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]]; + NSSet *claims = [NSSet setWithObject:[GIDClaim authTimeClaim]]; NSArray *expectedScopes = @[@"scope1", @"scope2", @"email", @"profile"]; GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult, @@ -90,7 +90,7 @@ - (void)testDefaultOptions_withAllParameters_initializesPropertiesCorrectly { addScopesFlow:NO scopes:scopes nonce:nonce - tokenClaims:tokenClaims + claims:claims completion:completion]; XCTAssertTrue(options.interactive); XCTAssertFalse(options.continuation); @@ -100,8 +100,8 @@ - (void)testDefaultOptions_withAllParameters_initializesPropertiesCorrectly { // Convert arrays to sets for comparison to make the test order-independent. XCTAssertEqualObjects([NSSet setWithArray:options.scopes], [NSSet setWithArray:expectedScopes]); XCTAssertEqualObjects(options.nonce, nonce); - XCTAssertEqualObjects(options.tokenClaims, tokenClaims); - XCTAssertNil(options.tokenClaimsAsJSON); + XCTAssertEqualObjects(options.claims, claims); + XCTAssertNil(options.claimsAsJSON); OCMVerifyAll(configuration); #if TARGET_OS_IOS || TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 55f239d0..13b4435e 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -32,7 +32,7 @@ #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" #import "GoogleSignIn/Sources/GIDSignIn_Private.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" -#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h" +#import "GoogleSignIn/Sources/GIDClaimsInternalOptions.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import @@ -644,7 +644,7 @@ - (void)testOAuthLogin { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -662,7 +662,7 @@ - (void)testOAuthLogin_RestoredSignIn { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:NO @@ -680,7 +680,7 @@ - (void)testOAuthLogin_RestoredSignInOldAccessToken { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:YES @@ -700,16 +700,16 @@ - (void)testOAuthLogin_AdditionalScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:NO + claimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:YES additionalScopes:nil manualNonce:nil - tokenClaims:nil]; + claims:nil]; expectedScopeString = [@[ @"email", @"profile" ] componentsJoinedByString:@" "]; XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); @@ -718,16 +718,16 @@ - (void)testOAuthLogin_AdditionalScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:NO + claimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:YES additionalScopes:@[ kScope ] manualNonce:nil - tokenClaims:nil]; + claims:nil]; expectedScopeString = [@[ kScope, @"email", @"profile" ] componentsJoinedByString:@" "]; XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); @@ -736,24 +736,24 @@ - (void)testOAuthLogin_AdditionalScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:NO + claimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:YES additionalScopes:@[ kScope, kScope2 ] manualNonce:nil - tokenClaims:nil]; + claims:nil]; expectedScopeString = [@[ kScope, kScope2, @"email", @"profile" ] componentsJoinedByString:@" "]; XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); } -- (void)testOAuthLogin_WithTokenClaims_FormatsParametersCorrectly { - GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; - GIDTokenClaim *essentialAuthTimeClaim = [GIDTokenClaim essentialAuthTimeClaim]; +- (void)testOAuthLogin_WithClaims_FormatsParametersCorrectly { + GIDClaim *authTimeClaim = [GIDClaim authTimeClaim]; + GIDClaim *essentialAuthTimeClaim = [GIDClaim essentialAuthTimeClaim]; OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] ).andDo(^(NSInvocation *invocation){ @@ -764,16 +764,16 @@ - (void)testOAuthLogin_WithTokenClaims_FormatsParametersCorrectly { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:NO + claimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:NO additionalScopes:nil manualNonce:nil - tokenClaims:[NSSet setWithObject:essentialAuthTimeClaim]]; + claims:[NSSet setWithObject:essentialAuthTimeClaim]]; XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], kEssentialAuthTimeClaimsJsonString, @@ -783,24 +783,24 @@ - (void)testOAuthLogin_WithTokenClaims_FormatsParametersCorrectly { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:NO + claimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:NO additionalScopes:nil manualNonce:nil - tokenClaims:[NSSet setWithObject:authTimeClaim]]; + claims:[NSSet setWithObject:authTimeClaim]]; XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], kNonEssentialAuthTimeClaimsJsonString, @"Claims JSON should be correctly formatted"); } -- (void)testOAuthLogin_WithTokenClaims_ReturnsIdTokenWithCorrectClaims { - GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; +- (void)testOAuthLogin_WithClaims_ReturnsIdTokenWithCorrectClaims { + GIDClaim *authTimeClaim = [GIDClaim authTimeClaim]; OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] ).andDo(^(NSInvocation *invocation){ @@ -811,16 +811,16 @@ - (void)testOAuthLogin_WithTokenClaims_ReturnsIdTokenWithCorrectClaims { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:NO + claimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:NO additionalScopes:nil manualNonce:nil - tokenClaims:[NSSet setWithObject:authTimeClaim]]; + claims:[NSSet setWithObject:authTimeClaim]]; XCTAssertNotNil(_signIn.currentUser, @"The currentUser should not be nil after a successful sign-in."); NSString *idTokenString = _signIn.currentUser.idToken.tokenString; @@ -847,7 +847,7 @@ - (void)testAddScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:NO @@ -872,7 +872,7 @@ - (void)testAddScopes { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -901,7 +901,7 @@ - (void)testAddScopes { } - (void)testAddScopes_WithPreviouslyRequestedClaims { - GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; + GIDClaim *authTimeClaim = [GIDClaim authTimeClaim]; // Restore the previous sign-in account. This is the preparation for adding scopes. OCMStub( [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] @@ -912,16 +912,16 @@ - (void)testAddScopes_WithPreviouslyRequestedClaims { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:NO + claimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:NO additionalScopes:nil manualNonce:nil - tokenClaims:[NSSet setWithObject:authTimeClaim]]; + claims:[NSSet setWithObject:authTimeClaim]]; XCTAssertNotNil(_signIn.currentUser); @@ -941,7 +941,7 @@ - (void)testAddScopes_WithPreviouslyRequestedClaims { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:YES + claimsAsJSONRequired:YES keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -986,7 +986,7 @@ - (void)testOpenIDRealm { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1014,16 +1014,16 @@ - (void)testManualNonce { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:NO + claimsError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:NO additionalScopes:@[] manualNonce:manualNonce - tokenClaims:nil]; + claims:nil]; XCTAssertEqualObjects(_savedAuthorizationRequest.nonce, manualNonce, @@ -1043,7 +1043,7 @@ - (void)testOAuthLogin_LoginHint { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1069,7 +1069,7 @@ - (void)testOAuthLogin_HostedDomain { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1084,7 +1084,7 @@ - (void)testOAuthLogin_ConsentCanceled { authError:@"access_denied" tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1099,7 +1099,7 @@ - (void)testOAuthLogin_ModalCanceled { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1124,7 +1124,7 @@ - (void)testOAuthLogin_KeychainError { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:YES restoredSignIn:NO oldAccessToken:NO @@ -1136,25 +1136,25 @@ - (void)testOAuthLogin_KeychainError { XCTAssertEqual(_authError.code, kGIDSignInErrorCodeKeychain); } -- (void)testOAuthLogin_TokenClaims_FailsWithError { - GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; - GIDTokenClaim *essentialAuthTimeClaim = [GIDTokenClaim essentialAuthTimeClaim]; +- (void)testOAuthLogin_Claims_FailsWithError { + GIDClaim *authTimeClaim = [GIDClaim authTimeClaim]; + GIDClaim *essentialAuthTimeClaim = [GIDClaim essentialAuthTimeClaim]; NSSet *conflictingClaims = [NSSet setWithObjects:authTimeClaim, essentialAuthTimeClaim, nil]; [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO - tokenClaimsError:YES + claimsError:YES restoredSignIn:NO oldAccessToken:NO modalCancel:NO useAdditionalScopes:NO additionalScopes:nil manualNonce:nil - tokenClaims:conflictingClaims]; + claims:conflictingClaims]; // Wait for the completion handler to be called [self waitForExpectationsWithTimeout:1.0 handler:nil]; @@ -1163,7 +1163,7 @@ - (void)testOAuthLogin_TokenClaims_FailsWithError { @"The error code should be for ambiguous claims."); XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain, @"The error domain should be the GIDSignIn error domain."); - XCTAssertEqualObjects(_authError.localizedDescription, kGIDTokenClaimErrorDescription, + XCTAssertEqualObjects(_authError.localizedDescription, kGIDClaimErrorDescription, @"The error description should clearly explain the ambiguity."); } @@ -1183,7 +1183,7 @@ - (void)testSignOut { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:NO @@ -1430,7 +1430,7 @@ - (void)testEmmSupportRequestParameters { authError:nil tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1482,7 +1482,7 @@ - (void)testEmmPasscodeInfo { authError:nil tokenError:nil emmPasscodeInfoRequired:YES - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1514,7 +1514,7 @@ - (void)testAuthEndpointEMMError { authError:callbackParams[@"error"] tokenError:nil emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1552,7 +1552,7 @@ - (void)testTokenEndpointEMMError { authError:nil tokenError:emmError emmPasscodeInfoRequired:NO - tokenClaimsAsJSONRequired:NO + claimsAsJSONRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO @@ -1637,7 +1637,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow authError:(NSString *)authError tokenError:(NSError *)tokenError emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired - tokenClaimsAsJSONRequired:(BOOL)tokenClaimsAsJSONRequired + claimsAsJSONRequired:(BOOL)claimsAsJSONRequired keychainError:(BOOL)keychainError restoredSignIn:(BOOL)restoredSignIn oldAccessToken:(BOOL)oldAccessToken @@ -1646,16 +1646,16 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow authError:authError tokenError:tokenError emmPasscodeInfoRequired:emmPasscodeInfoRequired - tokenClaimsAsJSONRequired:tokenClaimsAsJSONRequired + claimsAsJSONRequired:claimsAsJSONRequired keychainError:keychainError - tokenClaimsError:NO + claimsError:NO restoredSignIn:restoredSignIn oldAccessToken:oldAccessToken modalCancel:modalCancel useAdditionalScopes:NO additionalScopes:nil manualNonce:nil - tokenClaims:nil]; + claims:nil]; } // The authorization flow with parameters to control which branches to take. @@ -1663,16 +1663,16 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow authError:(NSString *)authError tokenError:(NSError *)tokenError emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired - tokenClaimsAsJSONRequired:(BOOL)tokenClaimsAsJSONRequired + claimsAsJSONRequired:(BOOL)claimsAsJSONRequired keychainError:(BOOL)keychainError - tokenClaimsError:(BOOL)tokenClaimsError + claimsError:(BOOL)claimsError restoredSignIn:(BOOL)restoredSignIn oldAccessToken:(BOOL)oldAccessToken modalCancel:(BOOL)modalCancel useAdditionalScopes:(BOOL)useAdditionalScopes additionalScopes:(NSArray *)additionalScopes manualNonce:(NSString *)nonce - tokenClaims:(NSSet *)tokenClaims { + claims:(NSSet *)claims { if (restoredSignIn) { // clearAndAuthenticateWithOptions [[[_authorization expect] andReturn:_authState] authState]; @@ -1682,13 +1682,13 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow NSDictionary *additionalParameters = [self additionalParametersWithEMMPasscodeInfoRequired:emmPasscodeInfoRequired - tokenClaimsAsJSONRequired:tokenClaimsAsJSONRequired]; + claimsAsJSONRequired:claimsAsJSONRequired]; OIDAuthorizationResponse *authResponse = [OIDAuthorizationResponse testInstanceWithAdditionalParameters:additionalParameters nonce:nonce errorString:authError]; - NSString *idToken = tokenClaims ? [OIDTokenResponse fatIDTokenWithAuthTime] : [OIDTokenResponse fatIDToken]; + NSString *idToken = claims ? [OIDTokenResponse fatIDTokenWithAuthTime] : [OIDTokenResponse fatIDToken]; OIDTokenResponse *tokenResponse = [OIDTokenResponse testInstanceWithIDToken:idToken accessToken:restoredSignIn ? kAccessToken : nil @@ -1696,7 +1696,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow refreshToken:kRefreshToken tokenRequest:nil]; - if (tokenClaims) { + if (claims) { // Creating this stub to use `currentUser.idToken`. id mockIDToken = OCMClassMock([GIDToken class]); OCMStub([mockIDToken tokenString]).andReturn(tokenResponse.idToken); @@ -1772,14 +1772,14 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow hint:_hint additionalScopes:nil nonce:nonce - tokenClaims:tokenClaims + claims:claims completion:completion]; } } // When token claims are invalid, sign-in fails skipping the entire authorization flow. // Thus, no need to verify `_authorization` or `_authState` as they won't be generated. - if (tokenClaimsError) { + if (claimsError) { return; } @@ -1950,14 +1950,14 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow - (NSDictionary *) additionalParametersWithEMMPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired - tokenClaimsAsJSONRequired:(BOOL)tokenClaimsAsJSONRequired { + claimsAsJSONRequired:(BOOL)claimsAsJSONRequired { NSMutableDictionary *additionalParameters = [NSMutableDictionary dictionary]; if (emmPasscodeInfoRequired) { additionalParameters[@"emm_passcode_info_required"] = @"1"; } - if (tokenClaimsAsJSONRequired) { + if (claimsAsJSONRequired) { additionalParameters[@"claims"] = kNonEssentialAuthTimeClaimsJsonString; } diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift index 88d00f2e..afa9ba49 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift @@ -20,7 +20,7 @@ import GoogleSignIn /// An observable class for authenticating via Google. final class GoogleSignInAuthenticator: ObservableObject { private var authViewModel: AuthenticationViewModel - private var tokenClaims: Set = Set([GIDTokenClaim.authTime()]) + private var claims: Set = Set([GIDClaim.authTime()]) /// Creates an instance of this authenticator. /// - parameter authViewModel: The view model this authenticator will set logged in status on. @@ -43,7 +43,7 @@ final class GoogleSignInAuthenticator: ObservableObject { hint: nil, additionalScopes: nil, nonce: manualNonce, - tokenClaims: tokenClaims + claims: claims ) { signInResult, error in guard let signInResult = signInResult else { print("Error! \(String(describing: error))") @@ -70,7 +70,7 @@ final class GoogleSignInAuthenticator: ObservableObject { GIDSignIn.sharedInstance.signIn( withPresenting: presentingWindow, - tokenClaims: tokenClaims + claims: claims ) { signInResult, error in guard let signInResult = signInResult else { print("Error! \(String(describing: error))") From e883939c8bcb928bb5f61d5211b20703be588976 Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:02:07 -0700 Subject: [PATCH 7/7] Ensure `additionalTokenRefreshParametersForAuthSession` returns a string dictionary (#561) --- GoogleSignIn/Sources/GIDEMMSupport.h | 9 +-- GoogleSignIn/Sources/GIDEMMSupport.m | 36 ++++++++-- GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m | 80 +++++++++++++++++++++ 3 files changed, 115 insertions(+), 10 deletions(-) diff --git a/GoogleSignIn/Sources/GIDEMMSupport.h b/GoogleSignIn/Sources/GIDEMMSupport.h index f57a6af7..61f445a4 100644 --- a/GoogleSignIn/Sources/GIDEMMSupport.h +++ b/GoogleSignIn/Sources/GIDEMMSupport.h @@ -34,12 +34,13 @@ NS_ASSUME_NONNULL_BEGIN completion:(void (^)(NSError *_Nullable))completion; /// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed. -+ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters; ++ (NSDictionary *)updatedEMMParametersWithParameters: + (NSDictionary *)parameters; /// Gets a new set of URL parameters that also contains EMM-related URL parameters if needed. -+ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters - emmSupport:(nullable NSString *)emmSupport - isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired; ++ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters + emmSupport:(nullable NSString *)emmSupport + isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired; @end diff --git a/GoogleSignIn/Sources/GIDEMMSupport.m b/GoogleSignIn/Sources/GIDEMMSupport.m index 0e7b0369..0914d841 100644 --- a/GoogleSignIn/Sources/GIDEMMSupport.m +++ b/GoogleSignIn/Sources/GIDEMMSupport.m @@ -84,16 +84,16 @@ + (void)handleTokenFetchEMMError:(nullable NSError *)error } } -+ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters { ++ (NSDictionary *)updatedEMMParametersWithParameters: + (NSDictionary *)parameters { return [self parametersWithParameters:parameters emmSupport:parameters[kEMMSupportParameterName] isPasscodeInfoRequired:parameters[kEMMPasscodeInfoParameterName] != nil]; } - -+ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters - emmSupport:(nullable NSString *)emmSupport - isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired { ++ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters + emmSupport:(nullable NSString *)emmSupport + isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired { if (!emmSupport) { return parameters; } @@ -109,7 +109,7 @@ + (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters if (isPasscodeInfoRequired) { allParameters[kEMMPasscodeInfoParameterName] = [GIDMDMPasscodeState passcodeState].info; } - return allParameters; + return [GIDEMMSupport dictionaryWithStringValuesFromDictionary:allParameters]; } #pragma mark - GTMAuthSessionDelegate @@ -128,6 +128,30 @@ - (void)updateErrorForAuthSession:(GTMAuthSession *)authSession }]; } +#pragma mark - Private Helpers + ++ (NSDictionary *) + dictionaryWithStringValuesFromDictionary:(NSDictionary *)originalDictionary { + NSMutableDictionary *stringifiedDictionary = + [NSMutableDictionary dictionaryWithCapacity:originalDictionary.count]; + + [originalDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + if ([value isKindOfClass:[NSString class]]) { + stringifiedDictionary[key] = value; + return; + } + if ([value isKindOfClass:[NSNumber class]]) { + if (CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID()) { + stringifiedDictionary[key] = [value boolValue] ? @"true" : @"false"; + } else { + stringifiedDictionary[key] = [value stringValue]; + } + return; + } + }]; + return stringifiedDictionary; +} + @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m b/GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m index 990aa733..9aece13b 100644 --- a/GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m +++ b/GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m @@ -274,6 +274,86 @@ - (void)testHandleTokenFetchEMMError_errorIsNotEMM { [self waitForExpectations:@[ called ] timeout:1]; } +# pragma mark - String Conversion Tests + +- (void)testParametersWithParameters_withAnyNumber_isConvertedToString { + NSDictionary *inputParameters = @{ @"number_key": @12345 }; + + NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters + emmSupport:@"1" + isPasscodeInfoRequired:NO]; + + XCTAssertEqualObjects(stringifiedParameters[@"number_key"], @"12345", + @"The NSNumber should be converted to a string."); + XCTAssertTrue([stringifiedParameters[@"number_key"] isKindOfClass:[NSString class]], + @"The final value should be of a NSString type."); +} + +- (void)testParametersWithParameters_withNumberOne_isConvertedToString { + NSDictionary *inputParameters = @{ @"number_key": @1 }; + + NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters + emmSupport:@"1" + isPasscodeInfoRequired:NO]; + + XCTAssertEqualObjects(stringifiedParameters[@"number_key"], @"1", + @"The NSNumber should be converted to a string."); + XCTAssertTrue([stringifiedParameters[@"number_key"] isKindOfClass:[NSString class]], + @"The final value should be of a NSString type."); +} + +- (void)testParametersWithParameters_withNumberZero_isConvertedToString { + NSDictionary *inputParameters = @{ @"number_key": @0}; + + NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters + emmSupport:@"1" + isPasscodeInfoRequired:NO]; + + XCTAssertEqualObjects(stringifiedParameters[@"number_key"], @"0", + @"The NSNumber should be converted to a string."); + XCTAssertTrue([stringifiedParameters[@"number_key"] isKindOfClass:[NSString class]], + @"The final value should be of a NSString type."); +} + +- (void)testParametersWithParameters_withBooleanYes_isConvertedToTrueString { + NSDictionary *inputParameters = @{ @"bool_key": @YES }; + + NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters + emmSupport:@"1" + isPasscodeInfoRequired:NO]; + + XCTAssertEqualObjects(stringifiedParameters[@"bool_key"], @"true", + @"The boolean YES should be converted to the string 'true'."); + XCTAssertTrue([stringifiedParameters[@"bool_key"] isKindOfClass:[NSString class]], + @"The final value should be of a NSString type."); +} + +- (void)testParametersWithParameters_withBooleanNo_isConvertedToFalseString { + NSDictionary *inputParameters = @{ @"bool_key": @NO }; + + NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters + emmSupport:@"1" + isPasscodeInfoRequired:NO]; + + XCTAssertEqualObjects(stringifiedParameters[@"bool_key"], @"false", + @"The boolean NO should be converted to the string 'false'."); + XCTAssertTrue([stringifiedParameters[@"bool_key"] isKindOfClass:[NSString class]], + @"The final value should be of a NSString type."); +} + +- (void)testParametersWithParameters_withString_remainsUnchanged { + NSDictionary *inputParameters = @{ @"string_key": @"hello" }; + + NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters + emmSupport:@"1" + isPasscodeInfoRequired:NO]; + + XCTAssertEqualObjects(stringifiedParameters[@"string_key"], @"hello", + @"The original string value should be preserved."); + XCTAssertTrue([stringifiedParameters[@"string_key"] isKindOfClass:[NSString class]], + @"The final value should be of a NSString type."); +} + # pragma mark - Helpers - (NSString *)systemVersion {