From f5aa90bac8bde887c5ad0873b7c2f4a508543e0d Mon Sep 17 00:00:00 2001 From: Brianna Morales Date: Tue, 3 Feb 2026 16:37:26 -0800 Subject: [PATCH 1/2] Expose refresh token expiration date Adds handling for "refresh_token_expires_in" and checks for refresh token expiration in refreshTokensIfNeeded. Fixes #565 --- GoogleSignIn/Sources/GIDGoogleUser.m | 21 +++++++++++- .../Sources/Public/GoogleSignIn/GIDSignIn.h | 4 ++- GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m | 33 +++++++++++++++++++ .../Tests/Unit/OIDAuthState+Testing.h | 15 ++++++++- .../Tests/Unit/OIDAuthState+Testing.m | 16 +++++++++ .../Tests/Unit/OIDTokenResponse+Testing.h | 8 +++++ .../Tests/Unit/OIDTokenResponse+Testing.m | 21 +++++++++++- 7 files changed, 114 insertions(+), 4 deletions(-) diff --git a/GoogleSignIn/Sources/GIDGoogleUser.m b/GoogleSignIn/Sources/GIDGoogleUser.m index ec300839..1da8f972 100644 --- a/GoogleSignIn/Sources/GIDGoogleUser.m +++ b/GoogleSignIn/Sources/GIDGoogleUser.m @@ -125,6 +125,16 @@ - (void)refreshTokensIfNeededWithCompletion:(GIDGoogleUserCompletion)completion }); return; } + if (self.refreshToken.expirationDate && [self.refreshToken.expirationDate timeIntervalSinceNow] <= 0) { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeRefreshTokenExpired + userInfo:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + completion(nil, error); + }); + return; + } + @synchronized (_tokenRefreshHandlerQueue) { // Push the handler into the callback queue. [_tokenRefreshHandlerQueue addObject:[completion copy]]; @@ -275,8 +285,17 @@ - (void)updateTokensWithAuthState:(OIDAuthState *)authState { self.accessToken = accessToken; } + NSDictionary *additionalParameters = authState.lastTokenResponse.additionalParameters; + NSNumber *refreshTokenExpiresIn = nil; + NSDate *refreshTokenExpirationDate = nil; + id expiresInValue = additionalParameters[@"refresh_token_expires_in"]; + if ([expiresInValue isKindOfClass:[NSNumber class]]) { + refreshTokenExpiresIn = (NSNumber *)expiresInValue; + NSTimeInterval interval = [refreshTokenExpiresIn doubleValue]; + refreshTokenExpirationDate = [NSDate dateWithTimeIntervalSinceNow:interval]; + } GIDToken *refreshToken = [[GIDToken alloc] initWithTokenString:authState.refreshToken - expirationDate:nil]; + expirationDate:refreshTokenExpirationDate]; if (![self.refreshToken isEqualToToken:refreshToken]) { self.refreshToken = refreshToken; } diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index c02d97f8..a6b95ead 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -53,7 +53,9 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { /// 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 + kGIDSignInErrorCodeJSONSerializationFailure = -10, + /// Indicates that the refresh token has expired and the user must be re-authorized. + kGIDSignInErrorCodeRefreshTokenExpired = -11, }; /// This class is used to sign in users with their Google account and manage their session. diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m index 3c99084e..dec1caaf 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m @@ -459,6 +459,25 @@ - (void)testRefreshTokensIfNeededWithCompletion_handleConcurrentRefresh { [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenRefreshTokenExpired { + NSTimeInterval expiresIn = -10; + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:expiresIn + idTokenExpiresIn:expiresIn + refreshTokenExpiresIn:expiresIn]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + XCTAssertNil(user); + XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain); + XCTAssertEqual(error.code, kGIDSignInErrorCodeRefreshTokenExpired); + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + # pragma mark - Test `addScopes:` - (void)testAddScopes_success { @@ -560,6 +579,20 @@ - (GIDGoogleUser *)googleUserWithAccessTokenExpiresIn:(NSTimeInterval)accessToke return [[GIDGoogleUser alloc] initWithAuthState:authState profileData:nil]; } +- (GIDGoogleUser *)googleUserWithAccessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn + idTokenExpiresIn:(NSTimeInterval)idTokenExpiresIn + refreshTokenExpiresIn:(NSTimeInterval)refreshTokenExpiresIn { + NSString *idToken = [self idTokenWithExpiresIn:idTokenExpiresIn]; + + OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:idToken + accessToken:kAccessToken + accessTokenExpiresIn:accessTokenExpiresIn + refreshToken:kRefreshToken + refreshTokenExpiresIn:refreshTokenExpiresIn]; + + return [[GIDGoogleUser alloc] initWithAuthState:authState profileData:nil]; +} + - (NSString *)idTokenWithExpiresIn:(NSTimeInterval)expiresIn { // The expireTime should be based on 1970. NSTimeInterval expireTime = [[NSDate date] timeIntervalSince1970] + expiresIn; diff --git a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h index 15feb758..f91f8e77 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h @@ -31,7 +31,7 @@ /** * @idToken The ID token. * @accessToken The access token string. - * @accessTokenExipresIn The life time of the access token starting from the moment when `OIDTokenResponse` is created. + * @accessTokenExpiresIn The life time of the access token starting from the moment when `OIDTokenResponse` is created. * @refreshToken The refresh token string. */ + (instancetype)testInstanceWithIDToken:(NSString *)idToken @@ -39,4 +39,17 @@ accessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn refreshToken:(NSString *)refreshToken; +/** + * @idToken The ID token. + * @accessToken The access token string. + * @accessTokenExpiresIn The life time of the access token starting from the moment when `OIDTokenResponse` is created. + * @refreshToken The refresh token string. + * @refreshTokenExpiresIn The life time of the refresh token starting from the moment when `OIDTokenResponse` is created. + */ ++ (instancetype)testInstanceWithIDToken:(NSString *)idToken + accessToken:(NSString *)accessToken + accessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn + refreshToken:(NSString *)refreshToken + refreshTokenExpiresIn:(NSTimeInterval)refreshTokenExpiresIn; + @end diff --git a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m index c49b2abd..98c317a6 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m @@ -46,5 +46,21 @@ + (instancetype)testInstanceWithIDToken:(NSString *)idToken return [self testInstanceWithTokenResponse:newResponse]; } ++ (instancetype)testInstanceWithIDToken:(NSString *)idToken + accessToken:(NSString *)accessToken + accessTokenExpiresIn:(NSTimeInterval)accessTokenExpiresIn + refreshToken:(NSString *)refreshToken + refreshTokenExpiresIn:(NSTimeInterval)refreshTokenExpiresIn { + OIDTokenResponse *newResponse = + [OIDTokenResponse testInstanceWithIDToken:idToken + accessToken:accessToken + expiresIn:@(accessTokenExpiresIn) + refreshToken:refreshToken + refreshExpiresIn:@(refreshTokenExpiresIn) + authTime:nil + tokenRequest:nil]; + return [self testInstanceWithTokenResponse:newResponse]; +} + @end diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h index b8329c67..3443a20a 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h @@ -67,6 +67,14 @@ extern NSString * const kFatPictureURL; authTime:(NSString *)authTime tokenRequest:(OIDTokenRequest *)tokenRequest; ++ (instancetype)testInstanceWithIDToken:(NSString *)idToken + accessToken:(NSString *)accessToken + expiresIn:(NSNumber *)expiresIn + refreshToken:(NSString *)refreshToken + refreshExpiresIn:(NSNumber *)refreshExpiresIn + authTime:(NSString *)authTime + tokenRequest:(OIDTokenRequest *)tokenRequest; + + (NSString *)idToken; + (NSString *)fatIDToken; diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m index bf2a5fa7..5b4d6448 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m @@ -85,8 +85,24 @@ + (instancetype)testInstanceWithIDToken:(NSString *)idToken refreshToken:(NSString *)refreshToken authTime:(NSString *)authTime tokenRequest:(OIDTokenRequest *)tokenRequest { + + return [OIDTokenResponse testInstanceWithIDToken:idToken + accessToken:accessToken + expiresIn:expiresIn + refreshToken:refreshToken + refreshExpiresIn:nil + authTime:authTime + tokenRequest:tokenRequest]; +} - NSMutableDictionary *parameters = [[NSMutableDictionary alloc] initWithDictionary:@{ ++ (instancetype)testInstanceWithIDToken:(NSString *)idToken + accessToken:(NSString *)accessToken + expiresIn:(NSNumber *)expiresIn + refreshToken:(NSString *)refreshToken + refreshExpiresIn:(NSNumber *)refreshExpiresIn + authTime:(NSString *)authTime + tokenRequest:(OIDTokenRequest *)tokenRequest { + NSMutableDictionary *> *parameters = [[NSMutableDictionary alloc] initWithDictionary:@{ @"access_token" : accessToken ?: kAccessToken, @"expires_in" : expiresIn ?: @(kAccessTokenExpiresIn), @"token_type" : @"example_token_type", @@ -97,6 +113,9 @@ + (instancetype)testInstanceWithIDToken:(NSString *)idToken if (idToken) { parameters[@"id_token"] = idToken; } + if (refreshExpiresIn) { + parameters[@"refresh_token_expires_in"] = refreshExpiresIn; + } return [[OIDTokenResponse alloc] initWithRequest:tokenRequest ?: [OIDTokenRequest testInstance] parameters:parameters]; } From 864ae2dcae030652cb225ff7714427ad0b3b0bab Mon Sep 17 00:00:00 2001 From: Brianna Morales Date: Tue, 3 Feb 2026 16:55:36 -0800 Subject: [PATCH 2/2] Add a stub for use of `additionalParameters`. --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 1 + 1 file changed, 1 insertion(+) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 13b4435e..99fe7ed3 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -310,6 +310,7 @@ - (void)setUp { OCMStub([_authState alloc]).andReturn(_authState); OCMStub([_authState initWithAuthorizationResponse:OCMOCK_ANY]).andReturn(_authState); _tokenResponse = OCMStrictClassMock([OIDTokenResponse class]); + OCMStub([_tokenResponse additionalParameters]).andReturn(@{}); _tokenRequest = OCMStrictClassMock([OIDTokenRequest class]); _authorization = OCMStrictClassMock([GTMAuthSession class]); _keychainStore = OCMStrictClassMock([GTMKeychainStore class]);