Browse Source

Add Three token properties in GIDGoogleUser API (#189)

Add three tokens in GIDGoogleUser API
pinlu 3 years ago
parent
commit
5c8fa73534

+ 74 - 36
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 <AppAuth/AppAuth.h>
 #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 kAuthState = @"authState";
 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 @@ static NSString *const kOpenIDRealmParameter = @"openid.realm";
   @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 @@ static NSString *const kOpenIDRealmParameter = @"openid.realm";
 
 - (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 @@ static NSString *const kOpenIDRealmParameter = @"openid.realm";
 - (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;
 }

+ 40 - 2
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 @@ static NSString *const kExpirationDateKey = @"expirationDate";
   [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

+ 2 - 2
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
 

+ 14 - 1
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

+ 5 - 0
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;
 

+ 8 - 2
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 @@
   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

+ 7 - 0
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 @@
   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 {

+ 1 - 0
GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m

@@ -37,6 +37,7 @@
   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 =

+ 7 - 2
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -397,6 +397,7 @@ static void *kTestObserverContext = &kTestObserverContext;
 - (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 @@ static void *kTestObserverContext = &kTestObserverContext;
   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 @@ static void *kTestObserverContext = &kTestObserverContext;
     }
   } 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);

+ 43 - 5
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 @@ static NSString * const tokenString = @"tokenString";
 
 @implementation GIDTokenTest
 
-- (void)setUP {
+- (void)setUp {
   [super setUp];
   _date = [[NSDate alloc]initWithTimeIntervalSince1970:1000];
 }
@@ -36,16 +37,53 @@ static NSString * const tokenString = @"tokenString";
   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 {