diff --git a/GoogleSignIn/Sources/GIDGoogleUser.m b/GoogleSignIn/Sources/GIDGoogleUser.m index 8cefa6fa..b03bd55d 100644 --- a/GoogleSignIn/Sources/GIDGoogleUser.m +++ b/GoogleSignIn/Sources/GIDGoogleUser.m @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ #import "GoogleSignIn/Sources/GIDAuthentication_Private.h" #import "GoogleSignIn/Sources/GIDProfileData_Private.h" +#import "GoogleSignIn/Sources/GIDToken_Private.h" #ifdef SWIFT_PACKAGE @import AppAuth; @@ -27,8 +28,6 @@ #import #endif -NS_ASSUME_NONNULL_BEGIN - // The ID Token claim key for the hosted domain value. static NSString *const kHostedDomainIDTokenClaimKey = @"hd"; @@ -41,15 +40,21 @@ static NSString *const kAudienceParameter = @"audience"; static NSString *const kOpenIDRealmParameter = @"openid.realm"; +NS_ASSUME_NONNULL_BEGIN + @implementation GIDGoogleUser { OIDAuthState *_authState; GIDConfiguration *_cachedConfiguration; + GIDToken *_cachedAccessToken; + GIDToken *_cachedRefreshToken; + GIDToken *_cachedIdToken; } - (nullable NSString *)userID { - NSString *idToken = [self idToken]; - if (idToken) { - OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken]; + NSString *idTokenString = self.idToken.tokenString; + if (idTokenString) { + OIDIDToken *idTokenDecoded = + [[OIDIDToken alloc] initWithIDTokenString:idTokenString]; if (idTokenDecoded && idTokenDecoded.subject) { return [idTokenDecoded.subject copy]; } @@ -80,16 +85,56 @@ - (GIDConfiguration *)configuration { @synchronized(self) { // Caches the configuration since it would not change for one GIDGoogleUser instance. if (!_cachedConfiguration) { - _cachedConfiguration = [[GIDConfiguration alloc] initWithClientID:[self clientID] - serverClientID:[self serverClientID] + NSString *clientID = _authState.lastAuthorizationResponse.request.clientID; + NSString *serverClientID = + _authState.lastTokenResponse.request.additionalParameters[kAudienceParameter]; + NSString *openIDRealm = + _authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter]; + + _cachedConfiguration = [[GIDConfiguration alloc] initWithClientID:clientID + serverClientID:serverClientID hostedDomain:[self hostedDomain] - openIDRealm:[self openIDRealm]]; + openIDRealm:openIDRealm]; }; } return _cachedConfiguration; } +- (GIDToken *)accessToken { + @synchronized(self) { + if (!_cachedAccessToken) { + _cachedAccessToken = [[GIDToken alloc] initWithTokenString:_authState.lastTokenResponse.accessToken + expirationDate:_authState.lastTokenResponse. + accessTokenExpirationDate]; + } + } + return _cachedAccessToken; +} + +- (GIDToken *)refreshToken { + @synchronized(self) { + if (!_cachedRefreshToken) { + _cachedRefreshToken = [[GIDToken alloc] initWithTokenString:_authState.refreshToken + expirationDate:nil]; + } + } + return _cachedRefreshToken; +} + +- (nullable GIDToken *)idToken { + @synchronized(self) { + NSString *idTokenString = _authState.lastTokenResponse.idToken; + if (!_cachedIdToken && idTokenString) { + NSDate *idTokenExpirationDate = [[[OIDIDToken alloc] + initWithIDTokenString:idTokenString] expiresAt]; + _cachedIdToken = [[GIDToken alloc] initWithTokenString:idTokenString + expirationDate:idTokenExpirationDate]; + } + } + return _cachedIdToken; +} + #pragma mark - Private Methods - (instancetype)initWithAuthState:(OIDAuthState *)authState @@ -103,40 +148,31 @@ - (instancetype)initWithAuthState:(OIDAuthState *)authState - (void)updateAuthState:(OIDAuthState *)authState profileData:(nullable GIDProfileData *)profileData { - _authState = authState; - _authentication = [[GIDAuthentication alloc] initWithAuthState:authState]; - _profile = profileData; + @synchronized(self) { + _authState = authState; + _authentication = [[GIDAuthentication alloc] initWithAuthState:authState]; + _profile = profileData; + + // These three tokens will be generated in the getter and cached . + _cachedAccessToken = nil; + _cachedRefreshToken = nil; + _cachedIdToken = nil; + } } #pragma mark - Helpers -- (NSString *)clientID { - return _authState.lastAuthorizationResponse.request.clientID; -} - - (nullable NSString *)hostedDomain { - NSString *idToken = [self idToken]; - if (idToken) { - OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken]; + NSString *idTokenString = self.idToken.tokenString; + if (idTokenString) { + OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString]; if (idTokenDecoded && idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) { - return [idTokenDecoded.claims[kHostedDomainIDTokenClaimKey] copy]; + return idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]; } } return nil; } -- (NSString *)idToken { - return _authState ? _authState.lastTokenResponse.idToken : nil; -} - -- (nullable NSString *)serverClientID { - return [_authState.lastTokenResponse.request.additionalParameters[kAudienceParameter] copy]; -} - -- (nullable NSString *)openIDRealm { - return [_authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter] copy]; -} - #pragma mark - NSSecureCoding + (BOOL)supportsSecureCoding { @@ -146,15 +182,17 @@ + (BOOL)supportsSecureCoding { - (nullable instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; if (self) { - _profile = [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey]; + GIDProfileData *profileData = + [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey]; + OIDAuthState *authState; if ([decoder containsValueForKey:kAuthState]) { // Current encoding - _authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState]; + authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState]; } else { // Old encoding GIDAuthentication *authentication = [decoder decodeObjectOfClass:[GIDAuthentication class] forKey:kAuthenticationKey]; - _authState = authentication.authState; + authState = authentication.authState; } - _authentication = [[GIDAuthentication alloc] initWithAuthState:_authState]; + [self updateAuthState:authState profileData:profileData]; } return self; } diff --git a/GoogleSignIn/Sources/GIDToken.m b/GoogleSignIn/Sources/GIDToken.m index 05d3f1dc..2702a952 100644 --- a/GoogleSignIn/Sources/GIDToken.m +++ b/GoogleSignIn/Sources/GIDToken.m @@ -22,13 +22,15 @@ static NSString *const kTokenStringKey = @"tokenString"; static NSString *const kExpirationDateKey = @"expirationDate"; +NS_ASSUME_NONNULL_BEGIN + @implementation GIDToken - (instancetype)initWithTokenString:(NSString *)tokenString - expirationDate:(NSDate *)expirationDate { + expirationDate:(nullable NSDate *)expirationDate { self = [super init]; if (self) { - _tokenString = tokenString; + _tokenString = [tokenString copy]; _expirationDate = expirationDate; } @@ -55,4 +57,40 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:_expirationDate forKey:kExpirationDateKey]; } +#pragma mark - isEqual + +- (BOOL)isEqual:(nullable id)object { + if (object == nil) { + return NO; + } + if (self == object) { + return YES; + } + if (![object isKindOfClass:[GIDToken class]]) { + return NO; + } + return [self isEqualToToken:(GIDToken *)object]; +} + +- (BOOL)isEqualToToken:(GIDToken *)otherToken { + return [_tokenString isEqual:otherToken.tokenString] && + [self isTheSameDate:_expirationDate with:otherToken.expirationDate]; +} + +// The date is nullable in GIDToken. Two `nil` dates are considered equal so +// token equality check succeeds if token strings are equal and have no expiration. +- (BOOL)isTheSameDate:(nullable NSDate *)date1 + with:(nullable NSDate *)date2 { + if (!date1 && !date2) { + return YES; + } + return [date1 isEqualToDate:date2]; +} + +- (NSUInteger)hash { + return [self.tokenString hash] ^ [self.expirationDate hash]; +} + @end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDToken_Private.h b/GoogleSignIn/Sources/GIDToken_Private.h index b078cb5a..0ce94af4 100644 --- a/GoogleSignIn/Sources/GIDToken_Private.h +++ b/GoogleSignIn/Sources/GIDToken_Private.h @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" NS_ASSUME_NONNULL_BEGIN @@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN // @param token The token String. // @param expirationDate The expiration date of the token. - (instancetype)initWithTokenString:(NSString *)tokenString - expirationDate:(NSDate *)expirationDate; + expirationDate:(nullable NSDate *)expirationDate; @end diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h index 9a17af70..1d3f09c6 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN @class GIDAuthentication; @class GIDConfiguration; +@class GIDToken; @class GIDProfileData; /// This class represents a user account. @@ -40,6 +41,18 @@ NS_ASSUME_NONNULL_BEGIN /// The configuration that was used to sign in this user. @property(nonatomic, readonly) GIDConfiguration *configuration; +/// The OAuth2 access token to access Google services. +@property(nonatomic, readonly) GIDToken *accessToken; + +/// The OAuth2 refresh token to exchange for new access tokens. +@property(nonatomic, readonly) GIDToken *refreshToken; + +/// An OpenID Connect ID token that identifies the user. +/// +/// Send this token to your server to authenticate the user there. For more information on this topic, +/// see https://developers.google.com/identity/sign-in/ios/backend-auth. +@property(nonatomic, readonly, nullable) GIDToken *idToken; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h index 29f5d3b9..866ede4c 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h @@ -27,6 +27,11 @@ NS_ASSUME_NONNULL_BEGIN /// The estimated expiration date of the token. @property(nonatomic, readonly, nullable) NSDate *expirationDate; +/// Check if current token is equal to another one. +/// +/// @param otherToken - Another token to compare. +- (BOOL)isEqualToToken:(GIDToken *)otherToken; + /// Unsupported. + (instancetype)new NS_UNAVAILABLE; diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m b/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m index de89f8d3..f6db0e85 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m @@ -15,6 +15,8 @@ #import "GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" + #import "GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h" #import "GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.h" #import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h" @@ -35,13 +37,17 @@ - (BOOL)isEqualToGoogleUser:(GIDGoogleUser *)other { return [self.authentication isEqual:other.authentication] && [self.userID isEqual:other.userID] && [self.profile isEqual:other.profile] && - [self.configuration isEqual:other.configuration]; + [self.configuration isEqual:other.configuration] && + [self.idToken isEqual:other.idToken] && + [self.refreshToken isEqual:other.refreshToken] && + [self.accessToken isEqual:other.accessToken]; } // Not the hash implemention you want to use on prod, but just to match |isEqual:| here. - (NSUInteger)hash { return [self.authentication hash] ^ [self.userID hash] ^ [self.configuration hash] ^ - [self.profile hash] ; + [self.profile hash] ^ [self.idToken hash] ^ [self.refreshToken hash] ^ + [self.accessToken hash]; } @end diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m index dea0abad..8196da40 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m @@ -19,6 +19,7 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" #import "GoogleSignIn/Sources/GIDAuthentication_Private.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" @@ -53,6 +54,12 @@ - (void)testInitWithAuthState { XCTAssertEqualObjects(user.configuration.hostedDomain, kHostedDomain); XCTAssertEqualObjects(user.configuration.clientID, OIDAuthorizationRequestTestingClientID); XCTAssertEqualObjects(user.profile, [GIDProfileData testInstance]); + XCTAssertEqualObjects(user.accessToken.tokenString, kAccessToken); + XCTAssertEqualObjects(user.refreshToken.tokenString, kRefreshToken); + + OIDIDToken *idToken = [[OIDIDToken alloc] + initWithIDTokenString:authState.lastTokenResponse.idToken]; + XCTAssertEqualObjects(user.idToken.expirationDate, [idToken expiresAt]); } - (void)testCoding { diff --git a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m index a1d2d381..cc1d63d2 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m @@ -37,6 +37,7 @@ - (void)testDefaultOptions { id presentingWindow = OCMStrictClassMock([NSWindow class]); #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST NSString *loginHint = @"login_hint"; + void (^completion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error) = ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {}; GIDSignInInternalOptions *options = diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 3d837916..5d9dac77 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -397,6 +397,7 @@ - (void)testShareInstance { - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser { [[[_authorization expect] andReturn:_authState] authState]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); + OCMStub([_authState refreshToken]).andReturn(kRefreshToken); id idTokenDecoded = OCMClassMock([OIDIDToken class]); OCMStub([idTokenDecoded alloc]).andReturn(idTokenDecoded); @@ -412,6 +413,9 @@ - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser { 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]; @@ -1228,8 +1232,9 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow } } else { XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called"]; - void (^completion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error) = - ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) { + GIDUserAuthCompletion completion = + ^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) { + [expectation fulfill]; if (userAuth) { XCTAssertEqualObjects(userAuth.serverAuthCode, kServerAuthCode); diff --git a/GoogleSignIn/Tests/Unit/GIDTokenTest.m b/GoogleSignIn/Tests/Unit/GIDTokenTest.m index aa9d1b1a..94a8ba06 100644 --- a/GoogleSignIn/Tests/Unit/GIDTokenTest.m +++ b/GoogleSignIn/Tests/Unit/GIDTokenTest.m @@ -18,6 +18,7 @@ #import "GoogleSignIn/Sources/GIDToken_Private.h" static NSString * const tokenString = @"tokenString"; +static NSString * const tokenString2 = @"tokenString2"; @interface GIDTokenTest : XCTestCase { NSDate *_date; @@ -26,7 +27,7 @@ @interface GIDTokenTest : XCTestCase { @implementation GIDTokenTest -- (void)setUP { +- (void)setUp { [super setUp]; _date = [[NSDate alloc]initWithTimeIntervalSince1970:1000]; } @@ -36,16 +37,53 @@ - (void)testInitializer { XCTAssertEqualObjects(token.tokenString, tokenString); XCTAssertEqualObjects(token.expirationDate, _date); } + +- (void)testTokensWithSameTokenStringAndExpirationDateAreEqual { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + XCTAssertEqualObjects(token, token2); +} + +- (void)testEqualTokensHaveTheSameHash { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + XCTAssertEqualObjects(token, token2); + XCTAssertEqual(token.hash, token2.hash); +} + +- (void)testTokensWithDifferentTokenStringsAreNotEqual { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString2 expirationDate:_date]; + XCTAssertNotEqualObjects(token, token2); +} + +- (void)testTokensWithSameTokenStringAndNoExpirationDateAreEqual { + GIDToken *refreshToken = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil]; + GIDToken *refreshToken2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil]; + XCTAssertEqualObjects(refreshToken, refreshToken2); +} + +- (void)testTokensWithSameTokenStringAndDifferentExpirationDateAreNotEqual { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + NSDate *date2 = [[NSDate alloc]initWithTimeIntervalSince1970:2000]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:date2]; + XCTAssertNotEqualObjects(token, token2); +} + +- (void)testTokensWithSameTokenStringAndOneHasNoExpirationDateAreNotEqual { + GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; + GIDToken *token2 = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:nil]; + XCTAssertNotEqualObjects(token, token2); +} - (void)testCoding { if (@available(iOS 11, macOS 10.13, *)) { GIDToken *token = [[GIDToken alloc]initWithTokenString:tokenString expirationDate:_date]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:token requiringSecureCoding:YES error:nil]; GIDToken *newToken = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDToken class] - fromData:data - error:nil]; - XCTAssertEqualObjects(token.tokenString, newToken.tokenString); - XCTAssertEqualObjects(token.expirationDate, newToken.expirationDate); + fromData:data + error:nil]; + XCTAssertEqualObjects(token, newToken); XCTAssertTrue([GIDToken supportsSecureCoding]); } else {