From b904bc992f0435976ea590ffa7cd8b5735667a62 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 10 Sep 2025 09:08:35 -0700 Subject: [PATCH 01/10] Updated GIDSignIn + GIDSignInInternalOptions Implementations + Unit Tests --- GoogleSignIn/Sources/GIDSignIn.m | 164 +++++++++++++++-- .../Sources/GIDSignInInternalOptions.h | 8 + .../Sources/GIDSignInInternalOptions.m | 4 + .../Sources/Public/GoogleSignIn/GIDSignIn.h | 174 ++++++++++++++++++ .../Tests/Unit/GIDSignInInternalOptionsTest.m | 50 +++++ GoogleSignIn/Tests/Unit/GIDSignInTest.m | 143 ++++++++++++-- 6 files changed, 511 insertions(+), 32 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 1c043735..c9df3308 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,18 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { } }]; } else { + NSError *claimsError; + + // If tokenClaims are invalid or JSON serialization fails, return with an error. + if (![self processTokenClaimsForOptions:options error:&claimsError]) { + if (options.completion) { + self->_currentOptions = nil; + dispatch_async(dispatch_get_main_queue(), ^{ + options.completion(nil, claimsError); + }); + } + return; + } [self authenticateWithOptions:options]; } } @@ -765,6 +880,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: @@ -1149,6 +1267,24 @@ - (void)assertValidPresentingViewController { } } +- (BOOL)processTokenClaimsForOptions:(GIDSignInInternalOptions *)options + error:(NSError **)error { + if (!options.tokenClaims) { + return YES; // Success + } + + NSString *tokenClaimsAsJSON = + [_tokenClaimsInternalOptions validatedJSONStringForClaims:options.tokenClaims + error:error]; + + if (!tokenClaimsAsJSON) { + return NO; // Failure + } + + options.tokenClaimsAsJSON = tokenClaimsAsJSON; + return YES; // Success +} + // Checks whether or not this is the first time the app runs. - (BOOL)isFreshInstall { NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; 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..07f070df 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 @@ -57,6 +58,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con options->_completion = completion; options->_scopes = [GIDScopes scopesWithBasicProfile:scopes]; options->_nonce = nonce; + options->_tokenClaims = tokenClaims; } return options; } @@ -84,6 +86,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con addScopesFlow:addScopesFlow scopes:@[] nonce:nil + tokenClaims:nil completion:completion]; return options; } @@ -120,6 +123,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..b6dffdc5 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,99 @@ 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 `SFSafariViewController` on +/// iOS 9 and 10. +/// @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 `SFSafariViewController` on +/// iOS 9 and 10. +/// @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 `SFSafariViewController` on +/// iOS 9 and 10. +/// @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 `SFSafariViewController` on +/// iOS 9 and 10. +/// @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 +392,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..d6e02942 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m @@ -63,6 +63,56 @@ - (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]]; + + // The expected scopes array will contain the provided scopes plus the default profile scopes. + 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..ef51eeb3 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -8,6 +8,10 @@ // // 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 COhttp://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. @@ -32,6 +36,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 +164,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 +528,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 +702,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 +719,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 +736,65 @@ - (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_TokenClaims { + 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)testAddScopes { // Restore the previous sign-in account. This is the preparation for adding scopes. OCMStub( @@ -752,7 +815,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 +847,7 @@ - (void)testAddScopes { [parsedScopes removeObject:@""]; grantedScopes = [parsedScopes copy]; } - + NSArray *expectedScopes = @[kNewScope, kGrantedScope]; XCTAssertEqualObjects(grantedScopes, expectedScopes); @@ -831,18 +894,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 +1015,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 +1434,7 @@ - (void)testTokenEndpointEMMError { NSError *handledError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeEMM userInfo:emmError.userInfo]; - + completion(handledError); [self waitForExpectationsWithTimeout:1 handler:nil]; @@ -1424,12 +1519,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 +1535,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]; @@ -1533,10 +1632,18 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow hint:_hint additionalScopes:nil nonce:nonce + tokenClaims:tokenClaims completion:completion]; } } + if (tokenClaimsError) { + XCTestExpectation *tokenClaimsErrorExpectation = + [self expectationWithDescription:@"Callback called"]; + [tokenClaimsErrorExpectation fulfill]; + return; + } + [_authorization verify]; [_authState verify]; @@ -1631,7 +1738,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 +1760,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 +1799,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]); From 441dd3b8a01b00f4e3fc5c0292812d7216db5da7 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:07:50 -0700 Subject: [PATCH 02/10] Updated DefaultOptionsWithConfiguration Initializer for macOS in GIDSignInInternalOptions --- GoogleSignIn/Sources/GIDSignInInternalOptions.m | 1 + 1 file changed, 1 insertion(+) diff --git a/GoogleSignIn/Sources/GIDSignInInternalOptions.m b/GoogleSignIn/Sources/GIDSignInInternalOptions.m index 07f070df..0799906a 100644 --- a/GoogleSignIn/Sources/GIDSignInInternalOptions.m +++ b/GoogleSignIn/Sources/GIDSignInInternalOptions.m @@ -41,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]; From d5846058fe29a234e366f4cb3b4bc1d2f61df351 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:17:51 -0700 Subject: [PATCH 03/10] Updated imports in GIDSignInInternalOptionsTest --- GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m | 1 + 1 file changed, 1 insertion(+) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m index d6e02942..10b88303 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; From d22b4fd07a6da3cfe1b30c8534b7c088cdf58218 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:10:40 -0700 Subject: [PATCH 04/10] Updated SignInFlow with improved tokenClaims processing --- GoogleSignIn/Sources/GIDSignIn.m | 23 ++++--------------- .../Sources/Public/GoogleSignIn/GIDSignIn.h | 16 +++++-------- .../Tests/Unit/GIDSignInInternalOptionsTest.m | 2 -- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 4 ---- 4 files changed, 10 insertions(+), 35 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index c9df3308..4da18b2c 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -742,7 +742,10 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { NSError *claimsError; // If tokenClaims are invalid or JSON serialization fails, return with an error. - if (![self processTokenClaimsForOptions:options error:&claimsError]) { + options.tokenClaimsAsJSON = [_tokenClaimsInternalOptions + validatedJSONStringForClaims:options.tokenClaims + error:&claimsError]; + if (claimsError) { if (options.completion) { self->_currentOptions = nil; dispatch_async(dispatch_get_main_queue(), ^{ @@ -1267,24 +1270,6 @@ - (void)assertValidPresentingViewController { } } -- (BOOL)processTokenClaimsForOptions:(GIDSignInInternalOptions *)options - error:(NSError **)error { - if (!options.tokenClaims) { - return YES; // Success - } - - NSString *tokenClaimsAsJSON = - [_tokenClaimsInternalOptions validatedJSONStringForClaims:options.tokenClaims - error:error]; - - if (!tokenClaimsAsJSON) { - return NO; // Failure - } - - options.tokenClaimsAsJSON = tokenClaimsAsJSON; - return YES; // Success -} - // Checks whether or not this is the first time the app runs. - (BOOL)isFreshInstall { NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index b6dffdc5..eb188928 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -233,8 +233,7 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// 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 `SFSafariViewController` on -/// iOS 9 and 10. +/// @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. @@ -245,15 +244,14 @@ 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 hint, and tokenClaims. +/// 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 `SFSafariViewController` on -/// iOS 9 and 10. +/// @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. @@ -275,8 +273,7 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// 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 `SFSafariViewController` on -/// iOS 9 and 10. +/// @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. @@ -292,7 +289,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 hint, additional scopes, nonce +/// 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 @@ -300,8 +297,7 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.") /// 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 `SFSafariViewController` on -/// iOS 9 and 10. +/// @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. diff --git a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m index 10b88303..1d6c2b3c 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m @@ -75,8 +75,6 @@ - (void)testDefaultOptions_withAllParameters_initializesPropertiesCorrectly { NSArray *scopes = @[@"scope1", @"scope2"]; NSString *nonce = @"test_nonce"; NSSet *tokenClaims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]]; - - // The expected scopes array will contain the provided scopes plus the default profile scopes. NSArray *expectedScopes = @[@"scope1", @"scope2", @"email", @"profile"]; GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult, diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index ef51eeb3..200967b7 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -8,7 +8,6 @@ // // 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 COhttp://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, @@ -1638,9 +1637,6 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow } if (tokenClaimsError) { - XCTestExpectation *tokenClaimsErrorExpectation = - [self expectationWithDescription:@"Callback called"]; - [tokenClaimsErrorExpectation fulfill]; return; } From 68e7752f7a09ce2fdbfa69a1026c6bea11250a6e Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:20:41 -0700 Subject: [PATCH 05/10] Updated styling in GIDSignIn.h comments --- GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index eb188928..768d1764 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -404,7 +404,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 hint, and tokenClaims. +/// 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 @@ -423,7 +423,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 hint, additional scopes +/// 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 @@ -445,7 +445,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 hint, additional scopes, nonce +/// 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 From 39f49534a2ae18431bb43c7f2d43760a874f812c Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:24:58 -0700 Subject: [PATCH 06/10] Updated initial comment in GIDSignInTest --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 200967b7..05e89ebd 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -8,9 +8,6 @@ // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, -// -// 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. From 5a409daf3f71c27375a9db1d6b329c834e31f588 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Thu, 11 Sep 2025 07:19:40 -0700 Subject: [PATCH 07/10] Added comment for handling tokenClaimsError in GIDSignInTest --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 05e89ebd..927665f6 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -1633,6 +1633,8 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow } } + // 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; } From c3a08ca845be958108d8664fb7b2f03870268172 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 12 Sep 2025 09:51:17 -0700 Subject: [PATCH 08/10] Added a unit test for testing idToken with claims. Also updated OIDTokenResponse + Testing --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 52 ++++++++++++++++--- .../Tests/Unit/OIDTokenResponse+Testing.h | 12 +++++ .../Tests/Unit/OIDTokenResponse+Testing.m | 38 +++++++++++++- 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 927665f6..5dfb5232 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -852,6 +852,32 @@ - (void)testAddScopes { [profile stopMocking]; } +- (void)testOAuthLogin_TokenClaims_Passes { + 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]]; + + OCMVerifyAll(_user); + OCMVerifyAll(_authState); +} + + - (void)testOpenIDRealm { _signIn.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId serverClientID:nil @@ -1553,12 +1579,20 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow nonce:nonce errorString:authError]; - OIDTokenResponse *tokenResponse = - [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] - accessToken:restoredSignIn ? kAccessToken : nil - expiresIn:oldAccessToken ? @(300) : nil - refreshToken:kRefreshToken - tokenRequest:nil]; + OIDTokenResponse *tokenResponse; + if (tokenClaims) { + tokenResponse = [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse iDTokenWithAuthTime] + accessToken:restoredSignIn ? kAccessToken : nil + expiresIn:oldAccessToken ? @(300) : nil + refreshToken:kRefreshToken + tokenRequest:nil]; + } else { + tokenResponse = [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] + accessToken:restoredSignIn ? kAccessToken : nil + expiresIn:oldAccessToken ? @(300) : nil + refreshToken:kRefreshToken + tokenRequest:nil]; + } OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc] initWithConfiguration:authResponse.request.configuration @@ -1749,6 +1783,12 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow } else { // Simulate token endpoint response. _savedTokenCallback(tokenResponse, nil); + if (tokenClaims) { + XCTAssertEqualObjects(tokenResponse.idToken, + [OIDTokenResponse iDTokenWithAuthTime], + @"ID Token string should contain authTime"); + } + } if (keychainError) { diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h index b565a392..565f8c55 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 *)iDTokenWithAuthTime; + /** * @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..20260026 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 = @"123333"; 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 *)iDTokenWithAuthTime { + 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 880a4f8c33493c193dbfa67e76dbd87c2c94d2c1 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:53:33 -0700 Subject: [PATCH 09/10] Updated unit test for requesting claims in GIDSignInTest --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 72 ++++++++++++++----------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 5dfb5232..a9d13444 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -745,6 +745,41 @@ - (void)testOAuthLogin_AdditionalScopes { XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); } +- (void)testOAuthLogin_requestClaimsSuccessfully { + 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)testOAuthLogin_TokenClaims { GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; GIDTokenClaim *essentialAuthTimeClaim = [GIDTokenClaim essentialAuthTimeClaim]; @@ -852,32 +887,6 @@ - (void)testAddScopes { [profile stopMocking]; } -- (void)testOAuthLogin_TokenClaims_Passes { - 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]]; - - OCMVerifyAll(_user); - OCMVerifyAll(_authState); -} - - - (void)testOpenIDRealm { _signIn.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId serverClientID:nil @@ -1586,6 +1595,11 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow expiresIn:oldAccessToken ? @(300) : nil refreshToken:kRefreshToken tokenRequest:nil]; + + // Creating this stub to use `currentUser.idToken`. + id mockIDToken = OCMClassMock([GIDToken class]); + OCMStub([mockIDToken tokenString]).andReturn(tokenResponse.idToken); + OCMStub([_user idToken]).andReturn(mockIDToken); } else { tokenResponse = [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] accessToken:restoredSignIn ? kAccessToken : nil @@ -1783,12 +1797,6 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow } else { // Simulate token endpoint response. _savedTokenCallback(tokenResponse, nil); - if (tokenClaims) { - XCTAssertEqualObjects(tokenResponse.idToken, - [OIDTokenResponse iDTokenWithAuthTime], - @"ID Token string should contain authTime"); - } - } if (keychainError) { From 29312e882868433914bd00613371ed0da1d31521 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:16:14 -0700 Subject: [PATCH 10/10] Updated method name within OIDTokenResponse from idTokenWithAuthTime to fatIDTokenWithAuthTime --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 78 +++++++++---------- .../Tests/Unit/OIDTokenResponse+Testing.h | 2 +- .../Tests/Unit/OIDTokenResponse+Testing.m | 4 +- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index a9d13444..17453785 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -745,8 +745,9 @@ - (void)testOAuthLogin_AdditionalScopes { XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); } -- (void)testOAuthLogin_requestClaimsSuccessfully { +- (void)testOAuthLogin_WithTokenClaims_FormatsParametersCorrectly { GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; + GIDTokenClaim *essentialAuthTimeClaim = [GIDTokenClaim essentialAuthTimeClaim]; OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] ).andDo(^(NSInvocation *invocation){ @@ -765,29 +766,11 @@ - (void)testOAuthLogin_requestClaimsSuccessfully { 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)testOAuthLogin_TokenClaims { - GIDTokenClaim *authTimeClaim = [GIDTokenClaim authTimeClaim]; - GIDTokenClaim *essentialAuthTimeClaim = [GIDTokenClaim essentialAuthTimeClaim]; + tokenClaims:[NSSet setWithObject:essentialAuthTimeClaim]]; - OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] - ).andDo(^(NSInvocation *invocation){ - self->_keychainSaved = self->_saveAuthorizationReturnValue; - }); + XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], + kEssentialAuthTimeClaimsJsonString, + @"Claims JSON should be correctly formatted"); [self OAuthLoginWithAddScopesFlow:NO authError:nil @@ -801,11 +784,20 @@ - (void)testOAuthLogin_TokenClaims { useAdditionalScopes:NO additionalScopes:nil manualNonce:nil - tokenClaims:[NSSet setWithObject:essentialAuthTimeClaim]]; + tokenClaims:[NSSet setWithObject:authTimeClaim]]; XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], - kEssentialAuthTimeClaimsJsonString, + 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 @@ -821,9 +813,18 @@ - (void)testOAuthLogin_TokenClaims { manualNonce:nil tokenClaims:[NSSet setWithObject:authTimeClaim]]; - XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], - kNonEssentialAuthTimeClaimsJsonString, - @"Claims JSON should be correctly formatted"); + 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 { @@ -1588,24 +1589,19 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow nonce:nonce errorString:authError]; - OIDTokenResponse *tokenResponse; - if (tokenClaims) { - tokenResponse = [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse iDTokenWithAuthTime] - accessToken:restoredSignIn ? kAccessToken : nil - expiresIn:oldAccessToken ? @(300) : nil - refreshToken:kRefreshToken - tokenRequest:nil]; + NSString *idToken = tokenClaims ? [OIDTokenResponse fatIDTokenWithAuthTime] : [OIDTokenResponse fatIDToken]; + OIDTokenResponse *tokenResponse = + [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); - } else { - tokenResponse = [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] - accessToken:restoredSignIn ? kAccessToken : nil - expiresIn:oldAccessToken ? @(300) : nil - refreshToken:kRefreshToken - tokenRequest:nil]; } OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc] diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h index 565f8c55..b8329c67 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h @@ -71,7 +71,7 @@ extern NSString * const kFatPictureURL; + (NSString *)fatIDToken; -+ (NSString *)iDTokenWithAuthTime; ++ (NSString *)fatIDTokenWithAuthTime; /** * @sub The subject of the ID token. diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m index 20260026..bf2a5fa7 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m @@ -38,7 +38,7 @@ NSString *const kHostedDomain = @"fakehosteddomain.com"; NSString *const kIssuer = @"https://test.com"; NSString *const kAudience = @"audience"; -NSString *const kAuthTime = @"123333"; +NSString *const kAuthTime = @"1757753868"; NSTimeInterval const kIDTokenExpires = 1000; NSTimeInterval const kIssuedAt = 0; @@ -109,7 +109,7 @@ + (NSString *)fatIDToken { return [self idTokenWithSub:kUserID exp:@(kIDTokenExpires) fat:YES]; } -+ (NSString *)iDTokenWithAuthTime { ++ (NSString *)fatIDTokenWithAuthTime { return [self idTokenWithSub:kUserID exp:@(kIDTokenExpires) fat:YES authTime:kAuthTime]; }