Pārlūkot izejas kodu

Update the current user's scopes instead of create a new user when adding new scopes. (#68)

pinlu 4 gadi atpakaļ
vecāks
revīzija
74b7d7f4fc

+ 72 - 48
GoogleSignIn/Sources/GIDGoogleUser.m

@@ -30,61 +30,91 @@ static NSString *const kHostedDomainIDTokenClaimKey = @"hd";
 
 // Key constants used for encode and decode.
 static NSString *const kAuthenticationKey = @"authentication";
-static NSString *const kGrantedScopesKey = @"grantedScopes";
-static NSString *const kUserIDKey = @"userID";
-static NSString *const kServerAuthCodeKey = @"serverAuthCode";
 static NSString *const kProfileDataKey = @"profileData";
-static NSString *const kHostedDomainKey = @"hostedDomain";
+static NSString *const kAuthState = @"authState";
 
 // Parameters for the token exchange endpoint.
 static NSString *const kAudienceParameter = @"audience";
 static NSString *const kOpenIDRealmParameter = @"openid.realm";
 
-
-@implementation GIDGoogleUser
+@implementation GIDGoogleUser {
+  OIDAuthState *_authState;
+}
 
 - (instancetype)initWithAuthState:(OIDAuthState *)authState
                       profileData:(nullable GIDProfileData *)profileData {
   self = [super init];
   if (self) {
-    _authentication = [[GIDAuthentication alloc] initWithAuthState:authState];
-
-    NSArray<NSString *> *grantedScopes;
-    NSString *grantedScopeString = authState.lastTokenResponse.scope;
-    if (grantedScopeString) {
-      // If we have a 'scope' parameter from the backend, this is authoritative.
-      // Remove leading and trailing whitespace.
-      grantedScopeString = [grantedScopeString stringByTrimmingCharactersInSet:
-          [NSCharacterSet whitespaceCharacterSet]];
-      // Tokenize with space as a delimiter.
-      NSMutableArray<NSString *> *parsedScopes =
-          [[grantedScopeString componentsSeparatedByString:@" "] mutableCopy];
-      // Remove empty strings.
-      [parsedScopes removeObject:@""];
-      grantedScopes = [parsedScopes copy];
+    [self updateAuthState:authState profileData:profileData];
+  }
+  return self;
+}
+
+- (nullable NSString *)userID {
+  NSString *idToken = [self idToken];
+  if (idToken) {
+    OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken];
+    if (idTokenDecoded && idTokenDecoded.subject) {
+      return [idTokenDecoded.subject copy];
     }
-    _grantedScopes = grantedScopes;
-
-    _serverAuthCode = [authState.lastTokenResponse.additionalParameters[@"server_code"] copy];
-    _profile = [profileData copy];
-
-    NSString *idToken = authState.lastTokenResponse.idToken;
-    if (idToken) {
-      OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken];
-      if (idTokenDecoded.subject) {
-        _userID = [idTokenDecoded.subject copy];
-      }
-      if (idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) {
-        _hostedDomain = [idTokenDecoded.claims[kHostedDomainIDTokenClaimKey] copy];
-      }
+  }
+
+  return nil;
+}
+
+- (nullable NSString *)hostedDomain {
+  NSString *idToken = [self idToken];
+  if (idToken) {
+    OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken];
+    if (idTokenDecoded && idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) {
+      return [idTokenDecoded.claims[kHostedDomainIDTokenClaimKey] copy];
     }
+  }
+
+  return nil;
+}
 
-    _serverClientID =
-        [authState.lastTokenResponse.request.additionalParameters[kAudienceParameter] copy];
-    _openIDRealm =
-        [authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter] copy];
+- (nullable NSString *)serverAuthCode {
+  return [_authState.lastTokenResponse.additionalParameters[@"server_code"] copy];
+}
+
+- (nullable NSString *)serverClientID {
+  return [_authState.lastTokenResponse.request.additionalParameters[kAudienceParameter] copy];
+}
+
+- (nullable NSString *)openIDRealm {
+  return [_authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter] copy];
+}
+
+- (nullable NSArray<NSString *> *)grantedScopes {
+  NSArray<NSString *> *grantedScopes;
+  NSString *grantedScopeString = _authState.lastTokenResponse.scope;
+  if (grantedScopeString) {
+    // If we have a 'scope' parameter from the backend, this is authoritative.
+    // Remove leading and trailing whitespace.
+    grantedScopeString = [grantedScopeString stringByTrimmingCharactersInSet:
+        [NSCharacterSet whitespaceCharacterSet]];
+    // Tokenize with space as a delimiter.
+    NSMutableArray<NSString *> *parsedScopes =
+        [[grantedScopeString componentsSeparatedByString:@" "] mutableCopy];
+    // Remove empty strings.
+    [parsedScopes removeObject:@""];
+    grantedScopes = [parsedScopes copy];
   }
-  return self;
+  return grantedScopes;
+}
+
+#pragma mark - Private Methods
+
+- (void)updateAuthState:(OIDAuthState *)authState
+            profileData:(nullable GIDProfileData *)profileData {
+  _authState = authState;
+  _authentication = [[GIDAuthentication alloc] initWithAuthState:authState];
+  _profile = profileData;
+}
+
+- (NSString *)idToken {
+  return _authState ? _authState.lastTokenResponse.idToken : nil;
 }
 
 #pragma mark - NSSecureCoding
@@ -98,22 +128,16 @@ static NSString *const kOpenIDRealmParameter = @"openid.realm";
   if (self) {
     _authentication = [decoder decodeObjectOfClass:[GIDAuthentication class]
                                             forKey:kAuthenticationKey];
-    _grantedScopes = [decoder decodeObjectOfClass:[NSArray class] forKey:kGrantedScopesKey];
-    _userID = [decoder decodeObjectOfClass:[NSString class] forKey:kUserIDKey];
-    _serverAuthCode = [decoder decodeObjectOfClass:[NSString class] forKey:kServerAuthCodeKey];
     _profile = [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey];
-    _hostedDomain = [decoder decodeObjectOfClass:[NSString class] forKey:kHostedDomainKey];
+    _authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState];
   }
   return self;
 }
 
 - (void)encodeWithCoder:(NSCoder *)encoder {
   [encoder encodeObject:_authentication forKey:kAuthenticationKey];
-  [encoder encodeObject:_grantedScopes forKey:kGrantedScopesKey];
-  [encoder encodeObject:_userID forKey:kUserIDKey];
-  [encoder encodeObject:_serverAuthCode forKey:kServerAuthCodeKey];
   [encoder encodeObject:_profile forKey:kProfileDataKey];
-  [encoder encodeObject:_hostedDomain forKey:kHostedDomainKey];
+  [encoder encodeObject:_authState forKey:kAuthState];
 }
 
 @end

+ 4 - 0
GoogleSignIn/Sources/GIDGoogleUser_Private.h

@@ -27,6 +27,10 @@ NS_ASSUME_NONNULL_BEGIN
 - (instancetype)initWithAuthState:(OIDAuthState *)authState
                       profileData:(nullable GIDProfileData *)profileData;
 
+// Update the auth state and profile data.
+- (void)updateAuthState:(OIDAuthState *)authState
+            profileData:(nullable GIDProfileData *)profileData;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 12 - 4
GoogleSignIn/Sources/GIDSignIn.m

@@ -212,6 +212,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
                                        presentingViewController:presentingViewController
                                                       loginHint:hint
+                                                   addScopesFlow:NO
                                                        callback:callback];
   [self signInWithOptions:options];
 }
@@ -251,6 +252,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
                                        presentingViewController:presentingViewController
                                                       loginHint:self.currentUser.profile.email
+                                                   addScopesFlow:YES
                                                        callback:callback];
 
   NSSet<NSString *> *requestedScopes = [NSSet setWithArray:scopes];
@@ -631,9 +633,15 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
                                                  code:kGIDSignInErrorCodeKeychain];
         return;
       }
-      GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState
-                                                         profileData:handlerAuthFlow.profileData];
-      [self setCurrentUserWithKVO:user];
+
+      if (_currentOptions.addScopesFlow) {
+        [self->_currentUser updateAuthState:authState
+                                profileData:handlerAuthFlow.profileData];
+      } else {
+        GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState
+                                                           profileData:handlerAuthFlow.profileData];
+        [self setCurrentUserWithKVO:user];
+      }
     }
   }];
 }
@@ -649,7 +657,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
       return;
     }
     OIDIDToken *idToken =
-        [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken];
+        [[OIDIDToken alloc] initWithIDTokenString: authState.lastTokenResponse.idToken];
     // If the profile data are present in the ID token, use them.
     if (idToken) {
       handlerAuthFlow.profileData = [self profileDataWithIDToken:idToken];

+ 5 - 1
GoogleSignIn/Sources/GIDSignInInternalOptions.h

@@ -31,6 +31,9 @@ NS_ASSUME_NONNULL_BEGIN
 /// Whether the sign-in is a continuation of the previous one.
 @property(nonatomic, readonly) BOOL continuation;
 
+/// Whether the sign-in is an addScopes flow. NO means it is a sign in flow.
+@property(nonatomic, readonly) BOOL addScopesFlow;
+
 /// The extra parameters used in the sign-in URL.
 @property(nonatomic, readonly, nullable) NSDictionary *extraParams;
 
@@ -54,7 +57,8 @@ NS_ASSUME_NONNULL_BEGIN
                        presentingViewController:
                            (nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
-                                       callback:(GIDSignInCallback)callback;
+                                  addScopesFlow:(BOOL)addScopesFlow
+                                       callback:(nullable GIDSignInCallback)callback;
 
 /// Creates the options to sign in silently.
 + (instancetype)silentOptionsWithCallback:(GIDSignInCallback)callback;

+ 5 - 1
GoogleSignIn/Sources/GIDSignInInternalOptions.m

@@ -24,11 +24,13 @@ NS_ASSUME_NONNULL_BEGIN
                        presentingViewController:
                            (nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
-                                       callback:(GIDSignInCallback)callback {
+                                  addScopesFlow:(BOOL)addScopesFlow
+                                       callback:(nullable GIDSignInCallback)callback {
   GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
   if (options) {
     options->_interactive = YES;
     options->_continuation = NO;
+    options->_addScopesFlow = addScopesFlow;
     options->_configuration = configuration;
     options->_presentingViewController = presentingViewController;
     options->_loginHint = loginHint;
@@ -42,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN
   GIDSignInInternalOptions *options = [self defaultOptionsWithConfiguration:nil
                                                    presentingViewController:nil
                                                                   loginHint:nil
+                                                               addScopesFlow:NO
                                                                    callback:callback];
   if (options) {
     options->_interactive = NO;
@@ -55,6 +58,7 @@ NS_ASSUME_NONNULL_BEGIN
   if (options) {
     options->_interactive = _interactive;
     options->_continuation = continuation;
+    options->_addScopesFlow = _addScopesFlow;
     options->_configuration = _configuration;
     options->_presentingViewController = _presentingViewController;
     options->_loginHint = _loginHint;

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

@@ -39,9 +39,11 @@
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
                                        presentingViewController:presentingViewController
                                                       loginHint:loginHint
+                                                   addScopesFlow:NO
                                                        callback:callback];
   XCTAssertTrue(options.interactive);
   XCTAssertFalse(options.continuation);
+  XCTAssertFalse(options.addScopesFlow);
   XCTAssertNil(options.extraParams);
 
   OCMVerifyAll(configuration);

+ 92 - 10
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -134,6 +134,9 @@ static NSString *const kFakeURL = @"http://foo.com";
 
 static NSString *const kEMMSupport = @"1";
 
+static NSString *const kGrantedScope = @"grantedScope";
+static NSString *const kNewScope = @"newScope";
+
 /// Unique pointer value for KVO tests.
 static void *kTestObserverContext = &kTestObserverContext;
 
@@ -363,6 +366,8 @@ static void *kTestObserverContext = &kTestObserverContext;
   [_authState verify];
   [_tokenResponse verify];
   XCTAssertEqual(_signIn.currentUser.userID, kFakeGaiaID);
+
+  [idTokenDecoded stopMocking];
 }
 
 - (void)testRestoredPreviousSignInNoRefresh_hasNoPreviousUser {
@@ -453,6 +458,67 @@ static void *kTestObserverContext = &kTestObserverContext;
                   modalCancel:NO];
 }
 
+- (void)testAddScopes {
+  // Restore the previous sign-in account. This is the preparation for adding scopes.
+  [self OAuthLoginWithOptions:nil
+                    authError:nil
+                   tokenError:nil
+      emmPasscodeInfoRequired:NO
+                keychainError:NO
+               restoredSignIn:YES
+               oldAccessToken:NO
+                  modalCancel:NO];
+
+  XCTAssertNotNil(_signIn.currentUser);
+
+
+  GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:nil
+                                                                      presentingViewController:nil
+                                                                                     loginHint:nil
+                                                                                 addScopesFlow:YES
+                                                                                      callback:nil];
+
+  id profile = OCMStrictClassMock([GIDProfileData class]);
+  OCMStub([profile email]).andReturn(kUserEmail);
+
+  OCMStub([_user authentication]).andReturn(_authentication);
+  OCMStub([_authentication clientID]).andReturn(kClientId);
+  OCMStub([_user serverClientID]).andReturn(nil);
+  OCMStub([_user hostedDomain]).andReturn(nil);
+
+  OCMStub([_user openIDRealm]).andReturn(kOpenIDRealm);
+  OCMStub([_user profile]).andReturn(profile);
+  OCMStub([_user grantedScopes]).andReturn(@[kGrantedScope]);
+
+  [self OAuthLoginWithOptions:options
+                    authError:nil
+                   tokenError:nil
+      emmPasscodeInfoRequired:NO
+                keychainError:NO
+               restoredSignIn:NO
+               oldAccessToken:NO
+                  modalCancel:NO];
+
+  NSArray<NSString *> *grantedScopes;
+  NSString *grantedScopeString = _savedAuthorizationRequest.scope;
+
+  if (grantedScopeString) {
+    grantedScopeString = [grantedScopeString stringByTrimmingCharactersInSet:
+        [NSCharacterSet whitespaceCharacterSet]];
+    // Tokenize with space as a delimiter.
+    NSMutableArray<NSString *> *parsedScopes =
+        [[grantedScopeString componentsSeparatedByString:@" "] mutableCopy];
+    // Remove empty strings.
+    [parsedScopes removeObject:@""];
+    grantedScopes = [parsedScopes copy];
+  }
+  
+  NSArray<NSString *> *expectedScopes = @[kNewScope, kGrantedScope];
+  XCTAssertEqualObjects(grantedScopes, expectedScopes);
+
+  [profile stopMocking];
+}
+
 - (void)testOpenIDRealm {
   _configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
                                                serverClientID:nil
@@ -1013,10 +1079,7 @@ static void *kTestObserverContext = &kTestObserverContext;
     }
   } else {
     XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called"];
-    [_signIn signInWithConfiguration:_configuration
-            presentingViewController:_presentingViewController
-                                hint:_hint
-                            callback:^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {
+    GIDSignInCallback callback = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {
       [expectation fulfill];
       if (!user) {
         XCTAssertNotNil(error, @"should have an error if user is nil");
@@ -1024,7 +1087,17 @@ static void *kTestObserverContext = &kTestObserverContext;
       XCTAssertFalse(self->_callbackCalled, @"callback already called");
       self->_callbackCalled = YES;
       self->_authError = error;
-    }];
+    };
+    if (options.addScopesFlow) {
+      [_signIn addScopes:@[kNewScope]
+        presentingViewController:_presentingViewController
+                        callback:callback];
+    } else {
+      [_signIn signInWithConfiguration:_configuration
+              presentingViewController:_presentingViewController
+                                  hint:_hint
+                              callback:callback];
+    }
 
     [_authorization verify];
     [_authState verify];
@@ -1090,15 +1163,20 @@ static void *kTestObserverContext = &kTestObserverContext;
   [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
 
   // SaveAuthCallback
-  [[[_user stub] andReturn:_user] alloc];
   __block OIDAuthState *authState;
   __block GIDProfileData *profileData;
 
   if (keychainError) {
     _saveAuthorizationReturnValue = NO;
   } else {
-    (void)[[[_user expect] andReturn:_user] initWithAuthState:SAVE_TO_ARG_BLOCK(authState)
-                                                  profileData:SAVE_TO_ARG_BLOCK(profileData)];
+    if (options.addScopesFlow) {
+      [[_user expect] updateAuthState:SAVE_TO_ARG_BLOCK(authState)
+                          profileData:SAVE_TO_ARG_BLOCK(profileData)];
+    } else {
+      [[[_user stub] andReturn:_user] alloc];
+      (void)[[[_user expect] andReturn:_user] initWithAuthState:SAVE_TO_ARG_BLOCK(authState)
+                                                    profileData:SAVE_TO_ARG_BLOCK(profileData)];
+    }
   }
 
   if (restoredSignIn && !oldAccessToken) {
@@ -1131,8 +1209,12 @@ static void *kTestObserverContext = &kTestObserverContext;
   _keychainRemoved = NO;
   _keychainSaved = NO;
   _authError = nil;
-  [[[_user expect] andReturn:_authentication] authentication];
-  [[[_user expect] andReturn:_authentication] authentication];
+
+  if (!options.addScopesFlow) {
+    [[[_user expect] andReturn:_authentication] authentication];
+    [[[_user expect] andReturn:_authentication] authentication];
+  }
+
   __block GIDAuthenticationAction action;
   [[_authentication expect] doWithFreshTokens:SAVE_TO_ARG_BLOCK(action)];