Browse Source

Implement -addScopes:presentingViewController:callback:

Peter Andrews 4 năm trước cách đây
mục cha
commit
bf6dca94ff

+ 13 - 3
GoogleSignIn/Sources/GIDGoogleUser.m

@@ -36,6 +36,11 @@ static NSString *const kServerAuthCodeKey = @"serverAuthCode";
 static NSString *const kProfileDataKey = @"profileData";
 static NSString *const kHostedDomainKey = @"hostedDomain";
 
+// Parameters for the token exchange endpoint.
+static NSString *const kAudienceParameter = @"audience";
+static NSString *const kOpenIDRealmParameter = @"openid.realm";
+
+
 @implementation GIDGoogleUser
 
 - (instancetype)initWithAuthState:(OIDAuthState *)authState
@@ -44,7 +49,7 @@ static NSString *const kHostedDomainKey = @"hostedDomain";
   if (self) {
     _authentication = [[GIDAuthentication alloc] initWithAuthState:authState];
 
-    NSArray *grantedScopes;
+    NSArray<NSString *> *grantedScopes;
     NSString *grantedScopeString = authState.lastTokenResponse.scope;
     if (grantedScopeString) {
       // If we have a 'scope' parameter from the backend, this is authoritative.
@@ -52,8 +57,8 @@ static NSString *const kHostedDomainKey = @"hostedDomain";
       grantedScopeString = [grantedScopeString stringByTrimmingCharactersInSet:
           [NSCharacterSet whitespaceCharacterSet]];
       // Tokenize with space as a delimiter.
-      NSMutableArray *parsedScopes = [[grantedScopeString componentsSeparatedByString:@" "]
-          mutableCopy];
+      NSMutableArray<NSString *> *parsedScopes =
+          [[grantedScopeString componentsSeparatedByString:@" "] mutableCopy];
       // Remove empty strings.
       [parsedScopes removeObject:@""];
       grantedScopes = [parsedScopes copy];
@@ -73,6 +78,11 @@ static NSString *const kHostedDomainKey = @"hostedDomain";
         _hostedDomain = [idTokenDecoded.claims[kHostedDomainIDTokenClaimKey] copy];
       }
     }
+
+    _serverClientID =
+        [authState.lastTokenResponse.request.additionalParameters[kAudienceParameter] copy];
+    _openIDRealm =
+        [authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter] copy];
   }
   return self;
 }

+ 51 - 10
GoogleSignIn/Sources/GIDSignIn.m

@@ -150,8 +150,6 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
 
 #pragma mark - Public methods
 
-// Invoked when the app delegate receives a callback at |application:openURL:options:| or
-// |application:openURL:sourceApplication:annotation|.
 - (BOOL)handleURL:(NSURL *)url {
   // Check if the callback path matches the expected one for a URL from Safari/Chrome/SafariVC.
   if ([url.path isEqual:kBrowserCallbackPath]) {
@@ -191,8 +189,6 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   [self signInWithOptions:[GIDSignInInternalOptions silentOptionsWithCallback:callback]];
 }
 
-// Authenticates the user by first searching the keychain, then attempting to retrieve the refresh
-// token from a Google Sign In app, and finally through the standard OAuth 2.0 web flow.
 - (void)signInWithConfiguration:(GIDConfiguration *)configuration
        presentingViewController:(UIViewController *)presentingViewController
                        callback:(GIDSignInCallback)callback {
@@ -203,6 +199,51 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   [self signInWithOptions:options];
 }
 
+- (void)addScopes:(NSArray<NSString *> *)scopes
+    presentingViewController:(UIViewController *)presentingViewController
+                    callback:(GIDSignInCallback)callback {
+  // A currentUser must be available in order to complete this flow.
+  if (!self.currentUser) {
+    // No currentUser is set, notify callback of failure.
+    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                         code:kGIDSignInErrorCodeNoCurrentUser
+                                     userInfo:nil];
+    callback(nil, error);
+    return;
+  }
+
+  GIDConfiguration *configuration =
+      [[GIDConfiguration alloc] initWithClientID:self.currentUser.authentication.clientID
+                                  serverClientID:self.currentUser.serverClientID
+                                       loginHint:self.currentUser.profile.email
+                                    hostedDomain:self.currentUser.hostedDomain
+                                     openIDRealm:self.currentUser.openIDRealm];
+  GIDSignInInternalOptions *options =
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
+                                       presentingViewController:presentingViewController
+                                                       callback:callback];
+
+  NSSet<NSString *> *requestedScopes = [NSSet setWithArray:scopes];
+  NSMutableSet<NSString *> *grantedScopes =
+      [NSMutableSet setWithArray:self.currentUser.grantedScopes];
+
+  // Check to see if all requested scopes have already been granted.
+  if ([requestedScopes isSubsetOfSet:grantedScopes]) {
+    // All requested scopes have already been granted, notify callback of failure.
+    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                         code:kGIDSignInErrorCodeScopesAlreadyGranted
+                                     userInfo:nil];
+    callback(nil, error);
+    return;
+  }
+
+  // Use the union of granted and requested scopes.
+  [grantedScopes unionSet:requestedScopes];
+  options.scopes = [grantedScopes allObjects];
+
+  [self signInWithOptions:options];
+}
+
 - (void)signOut {
   // Clear the current user if there is one.
   if (_currentUser) {
@@ -422,7 +463,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
 
     [self addDecodeIdTokenCallback:authFlow];
     [self addSaveAuthCallback:authFlow];
-    [self addCallDelegateCallback:authFlow];
+    [self addCallbackCallback:authFlow];
   }];
 }
 
@@ -439,7 +480,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   OIDAuthState *authState = [self loadAuthState];
 
   if (![authState isAuthorized]) {
-    // No valid auth in keychain, per documentation/spec, notify delegate of failure.
+    // No valid auth in keychain, per documentation/spec, notify callback of failure.
     NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
                                          code:kGIDSignInErrorCodeHasNoAuthInKeychain
                                      userInfo:nil];
@@ -455,7 +496,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   } : nil];
   [self addDecodeIdTokenCallback:authFlow];
   [self addSaveAuthCallback:authFlow];
-  [self addCallDelegateCallback:authFlow];
+  [self addCallbackCallback:authFlow];
 }
 
 // Fetches the access token if necessary as part of the auth flow. If |fallback|
@@ -601,8 +642,8 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   }];
 }
 
-// Adds a callback to the auth flow to call the sign-in delegate.
-- (void)addCallDelegateCallback:(GIDAuthFlow *)authFlow {
+// Adds a callback to the auth flow to call the sign-in callback.
+- (void)addCallbackCallback:(GIDAuthFlow *)authFlow {
   __weak GIDAuthFlow *weakAuthFlow = authFlow;
   [authFlow addCallback:^() {
     GIDAuthFlow *handlerAuthFlow = weakAuthFlow;
@@ -700,7 +741,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   }
 }
 
-// Assert that the UI Delegate has been set.
+// Assert that the presenting view controller has been set.
 - (void)assertValidPresentingViewController {
   if (!_currentOptions.presentingViewController) {
     // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)

+ 2 - 2
GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h

@@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
 /// The client ID of the app from the Google Cloud Console.
 @property(nonatomic, readonly) NSString *clientID;
 
-/// The client ID of the home web server.  This will be returned as the `audience` property of the
+/// The client ID of the home server.  This will be returned as the `audience` property of the
 /// OpenID Connect ID token.  For more info on the ID token:
 /// https://developers.google.com/identity/sign-in/ios/backend-auth
 @property(nonatomic, readonly, nullable) NSString *serverClientID;
@@ -37,7 +37,7 @@ NS_ASSUME_NONNULL_BEGIN
 /// `GIDGoogleUser`'s `hostedDomain` property.
 @property(nonatomic, readonly, nullable) NSString *hostedDomain;
 
-/// The OpenID2 realm of the home web server. This allows Google to include the user's OpenID
+/// The OpenID2 realm of the home server. This allows Google to include the user's OpenID
 /// Identifier in the OpenID Connect ID token.
 @property(nonatomic, readonly, nullable) NSString *openIDRealm;
 

+ 9 - 4
GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h

@@ -27,23 +27,28 @@ NS_ASSUME_NONNULL_BEGIN
 /// The Google user ID.
 @property(nonatomic, readonly, nullable) NSString *userID;
 
-/// Representation of the Basic profile data. It is only available if
-/// `GIDSignIn.shouldFetchBasicProfile` is set and either `-[GIDSignIn signIn]` or
-/// `-[GIDSignIn restorePreviousSignIn]` has been completed successfully.
+/// Representation of basic profile data for the user.
 @property(nonatomic, readonly, nullable) GIDProfileData *profile;
 
 /// The authentication object for the user.
 @property(nonatomic, readonly) GIDAuthentication *authentication;
 
 /// The API scopes granted to the app in an array of `NSString`.
-@property(nonatomic, readonly, nullable) NSArray *grantedScopes;
+@property(nonatomic, readonly, nullable) NSArray<NSString *> *grantedScopes;
 
 /// For Google Apps hosted accounts, the domain of the user.
 @property(nonatomic, readonly, nullable) NSString *hostedDomain;
 
+/// The client ID of the home server.
+@property(nonatomic, readonly, nullable) NSString *serverClientID;
+
 /// An OAuth2 authorization code for the home server.
 @property(nonatomic, readonly, nullable) NSString *serverAuthCode;
 
+/// The OpenID2 realm of the home server.
+@property(nonatomic, readonly, nullable) NSString *openIDRealm;
+
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 14 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h

@@ -38,6 +38,10 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) {
   kGIDSignInErrorCodeCanceled = -5,
   /// Indicates an Enterprise Mobility Management related error has occurred.
   kGIDSignInErrorCodeEMM = -6,
+  /// Indicates there is no `currentUser`.
+  kGIDSignInErrorCodeNoCurrentUser = -7,
+  /// Indicates the requested scopes have already been granted to the `currentUser`.
+  kGIDSignInErrorCodeScopesAlreadyGranted = -8,
 };
 
 /// Represents a callback block that takes a `GIDGoogleUser` on success or an error if the operation
@@ -97,6 +101,16 @@ typedef void (^GIDDisconnectCallback)(NSError *_Nullable error);
        presentingViewController:(UIViewController *)presentingViewController
                        callback:(GIDSignInCallback)callback;
 
+/// Starts an interactive consent flow to add scopes to the current user's grants.
+///
+/// @param scopes The scopes to ask the user to consent to.
+/// @param presentingViewController The view controller used to present `SFSafariViewContoller` on
+///     iOS 9 and 10.
+/// @param callback The `GIDSignInCallback` block that is called on completion.
+- (void)addScopes:(NSArray<NSString *> *)scopes
+    presentingViewController:(UIViewController *)presentingViewController
+                    callback:(GIDSignInCallback)callback;
+
 /// Marks current user as being in the signed out state.
 - (void)signOut;
 

+ 1 - 1
Sample/Source/AuthInspectorViewController.m

@@ -133,7 +133,7 @@ static CGFloat const kTableViewCellPadding = 22.f;
   UIFont *font = [UIFont systemFontOfSize:kTableViewCellFontSize];
   NSDictionary *attributes = @{ NSFontAttributeName : font };
   size = [content boundingRectWithSize:constraintSize
-                               options:0
+                               options:NSStringDrawingUsesLineFragmentOrigin
                             attributes:attributes
                                context:NULL].size;
   return size.height + kTableViewCellPadding;

+ 2 - 0
Sample/Source/SignInViewController.h

@@ -36,6 +36,8 @@
 @property(weak, nonatomic) IBOutlet UIButton *signOutButton;
 // A button to disconnect user from this application.
 @property(weak, nonatomic) IBOutlet UIButton *disconnectButton;
+// A button to add scopes for this application.
+@property(weak, nonatomic) IBOutlet UIButton *addScopesButton;
 // A button to inspect the authorization object.
 @property(weak, nonatomic) IBOutlet UIButton *credentialsButton;
 // A dynamically-created slider for controlling the sign-in button width.

+ 41 - 26
Sample/Source/SignInViewController.m

@@ -135,21 +135,6 @@ static NSString * const kClientID =
   [super viewWillAppear:animated];
 }
 
-- (IBAction)signInPressed:(id)sender {
-  [GIDSignIn.sharedInstance signInWithConfiguration:_configuration
-                           presentingViewController:self
-                                           callback:^(GIDGoogleUser * _Nullable user,
-                                                      NSError * _Nullable error) {
-    if (error) {
-      self->_signInAuthStatus.text =
-          [NSString stringWithFormat:@"Status: Authentication error: %@", error];
-      return;
-    }
-    [self reportAuthStatus];
-    [self updateButtons];
-  }];
-}
-
 #pragma mark - Helper methods
 
 // Updates the GIDSignIn shared instance and the GIDSignInButton
@@ -249,28 +234,43 @@ static NSString * const kClientID =
   });
 }
 
-// Adjusts "Sign in", "Sign out", and "Disconnect" buttons to reflect
-// the current sign-in state (ie, the "Sign in" button becomes disabled
-// when a user is already signed in).
+// Adjusts "Sign in", "Sign out", "Disconnect", and "Add Scopes" buttons to reflect the current
+// sign-in state (ie, the "Sign in" button becomes disabled when a user is already signed in).
 - (void)updateButtons {
-  BOOL authenticated = (GIDSignIn.sharedInstance.currentUser.authentication != nil);
+  BOOL hasCurrentUser = (GIDSignIn.sharedInstance.currentUser != nil);
 
-  self.signInButton.enabled = !authenticated;
-  self.signOutButton.enabled = authenticated;
-  self.disconnectButton.enabled = authenticated;
-  self.credentialsButton.hidden = !authenticated;
+  self.signInButton.enabled = !hasCurrentUser;
+  self.signOutButton.enabled = hasCurrentUser;
+  self.disconnectButton.enabled = hasCurrentUser;
+  self.addScopesButton.enabled = hasCurrentUser;
+  self.credentialsButton.hidden = !hasCurrentUser;
 
-  if (authenticated) {
+  if (hasCurrentUser) {
     self.signInButton.alpha = 0.5;
-    self.signOutButton.alpha = self.disconnectButton.alpha = 1.0;
+    self.signOutButton.alpha = self.disconnectButton.alpha = self.addScopesButton.alpha = 1.0;
   } else {
     self.signInButton.alpha = 1.0;
-    self.signOutButton.alpha = self.disconnectButton.alpha = 0.5;
+    self.signOutButton.alpha = self.disconnectButton.alpha = self.addScopesButton.alpha = 0.5;
   }
 }
 
 #pragma mark - IBActions
 
+- (IBAction)signIn:(id)sender {
+  [GIDSignIn.sharedInstance signInWithConfiguration:_configuration
+                           presentingViewController:self
+                                           callback:^(GIDGoogleUser * _Nullable user,
+                                                      NSError * _Nullable error) {
+    if (error) {
+      self->_signInAuthStatus.text =
+          [NSString stringWithFormat:@"Status: Authentication error: %@", error];
+      return;
+    }
+    [self reportAuthStatus];
+    [self updateButtons];
+  }];
+}
+
 - (IBAction)signOut:(id)sender {
   [GIDSignIn.sharedInstance signOut];
   [self reportAuthStatus];
@@ -290,6 +290,21 @@ static NSString * const kClientID =
   }];
 }
 
+- (IBAction)addScopes:(id)sender {
+  [GIDSignIn.sharedInstance addScopes:@[ @"https://www.googleapis.com/auth/contacts.readonly" ]
+             presentingViewController:self
+                             callback:^(GIDGoogleUser * _Nullable user,
+                                        NSError * _Nullable error) {
+    if (error) {
+      self->_signInAuthStatus.text = [NSString stringWithFormat:@"Status: Failed to add scopes: %@",
+                                      error];
+    } else {
+      self->_signInAuthStatus.text = [NSString stringWithFormat:@"Status: Scopes added"];
+    }
+    [self refreshUserInfo];
+  }];
+}
+
 - (IBAction)showAuthInspector:(id)sender {
   AuthInspectorViewController *authInspector = [[AuthInspectorViewController alloc] init];
   [[self navigationController] pushViewController:authInspector animated:YES];

+ 29 - 17
Sample/Source/SignInViewController.xib

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
-    <device id="retina6_1" orientation="portrait" appearance="light"/>
+    <device id="retina6_0" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
@@ -9,6 +9,7 @@
     <objects>
         <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SignInViewController">
             <connections>
+                <outlet property="addScopesButton" destination="Cnr-Kk-7Cy" id="8ek-yS-f7u"/>
                 <outlet property="credentialsButton" destination="80" id="83"/>
                 <outlet property="disconnectButton" destination="65" id="71"/>
                 <outlet property="signInAuthStatus" destination="72" id="73"/>
@@ -22,12 +23,12 @@
         </placeholder>
         <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
         <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="55">
-            <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+            <rect key="frame" x="0.0" y="0.0" width="390" height="844"/>
             <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
             <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
             <nil key="simulatedStatusBarMetrics"/>
             <view key="tableHeaderView" contentMode="scaleToFill" id="57">
-                <rect key="frame" x="0.0" y="0.0" width="414" height="197"/>
+                <rect key="frame" x="0.0" y="0.0" width="390" height="197"/>
                 <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                 <subviews>
                     <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" fixedFrame="YES" text="Status: Authenticated" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="72">
@@ -55,19 +56,8 @@
                         <color key="textColor" systemColor="darkTextColor"/>
                         <nil key="highlightedColor"/>
                     </label>
-                    <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="63">
-                        <rect key="frame" x="50" y="160" width="77" height="35"/>
-                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
-                        <fontDescription key="fontDescription" type="boldSystem" pointSize="16"/>
-                        <state key="normal" title="Sign out">
-                            <color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
-                        </state>
-                        <connections>
-                            <action selector="signOut:" destination="-1" eventType="touchUpInside" id="64"/>
-                        </connections>
-                    </button>
                     <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="65">
-                        <rect key="frame" x="186" y="160" width="97" height="35"/>
+                        <rect key="frame" x="134" y="152" width="105" height="35"/>
                         <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                         <fontDescription key="fontDescription" type="boldSystem" pointSize="16"/>
                         <state key="normal" title="Disconnect">
@@ -92,13 +82,35 @@
                             <accessibilityTraits key="traits" button="YES"/>
                         </accessibility>
                         <connections>
-                            <action selector="signInPressed:" destination="-1" eventType="touchUpInside" id="6j4-hN-JQi"/>
+                            <action selector="signIn:" destination="-1" eventType="touchUpInside" id="6j4-hN-JQi"/>
                         </connections>
                     </view>
+                    <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="63">
+                        <rect key="frame" x="21" y="152" width="77" height="35"/>
+                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                        <fontDescription key="fontDescription" type="boldSystem" pointSize="16"/>
+                        <state key="normal" title="Sign out">
+                            <color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        </state>
+                        <connections>
+                            <action selector="signOut:" destination="-1" eventType="touchUpInside" id="64"/>
+                        </connections>
+                    </button>
+                    <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Cnr-Kk-7Cy">
+                        <rect key="frame" x="261" y="152" width="111" height="35"/>
+                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                        <fontDescription key="fontDescription" type="boldSystem" pointSize="16"/>
+                        <state key="normal" title="Add Scopes">
+                            <color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        </state>
+                        <connections>
+                            <action selector="addScopes:" destination="-1" eventType="touchUpInside" id="kRq-pg-Det"/>
+                        </connections>
+                    </button>
                 </subviews>
                 <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
             </view>
-            <point key="canvasLocation" x="139" y="153"/>
+            <point key="canvasLocation" x="332" y="87"/>
         </tableView>
     </objects>
     <resources>