Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion GoogleSignIn/Sources/GIDGoogleUser.m
Original file line number Diff line number Diff line change
Expand Up @@ -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]];
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 3 additions & 1 deletion GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,25 @@ - (void)testRefreshTokensIfNeededWithCompletion_handleConcurrentRefresh {
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenRefreshTokenExpired {
NSTimeInterval expiresIn = -10;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make the other tokens not expired, so that we're testing only the specific refresh token behavior?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do this then this would not reach the refresh token logic. This is due to the fact that we check if either IdToken and/or AccessToken are expired first (ref). If both of these tokens aren't expired, then we immediately return them to the client thus not reaching the refreshToken check that comes after.

The test where both of the tokens are not expired is already made here. Although this doesn't have the refreshToken expiration, it should produce the same output. I could add the expired refreshToken to this test if we want to be safe.

Separately, I could add another test where if only one of the tokens are expired, then we should still continue to the refreshToken logic.

Let me know what you think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! All good then, thank you for explaining :)

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 {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions GoogleSignIn/Tests/Unit/GIDSignInTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
15 changes: 14 additions & 1 deletion GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,25 @@
/**
* @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
accessToken:(NSString *)accessToken
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
16 changes: 16 additions & 0 deletions GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 20 additions & 1 deletion GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString *, NSString *> *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<NSString *, NSObject<NSCopying> *> *parameters = [[NSMutableDictionary alloc] initWithDictionary:@{
@"access_token" : accessToken ?: kAccessToken,
@"expires_in" : expiresIn ?: @(kAccessTokenExpiresIn),
@"token_type" : @"example_token_type",
Expand All @@ -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];
}
Expand Down
Loading