Răsfoiți Sursa

feat: allow providing custom nonce (#402)

Vojtech Novak 1 an în urmă
părinte
comite
b34f5a924a

+ 30 - 4
GoogleSignIn/Sources/GIDSignIn.m

@@ -272,12 +272,25 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                                       hint:(nullable NSString *)hint
                           additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
                                 completion:(nullable GIDSignInCompletion)completion {
+  [self signInWithPresentingViewController:presentingViewController 
+                                      hint:hint
+                          additionalScopes:additionalScopes
+                                     nonce:nil
+                                completion:completion];
+}
+
+- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
+                                      hint:(nullable NSString *)hint
+                          additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
+                                     nonce:(nullable NSString *)nonce
+                                completion:(nullable GIDSignInCompletion)completion {
   GIDSignInInternalOptions *options =
     [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
                                      presentingViewController:presentingViewController
                                                     loginHint:hint
                                                 addScopesFlow:NO
                                                        scopes:additionalScopes
+                                                        nonce:nonce
                                                    completion:completion];
   [self signInWithOptions:options];
 }
@@ -350,12 +363,25 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                               hint:(nullable NSString *)hint
                   additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
                         completion:(nullable GIDSignInCompletion)completion {
+  [self signInWithPresentingWindow:presentingWindow
+                              hint:hint
+                  additionalScopes:additionalScopes
+                             nonce:nil
+                        completion:completion];
+}
+
+- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
+                              hint:(nullable NSString *)hint
+                  additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
+                             nonce:(nullable NSString *)nonce
+                        completion:(nullable GIDSignInCompletion)completion {
   GIDSignInInternalOptions *options =
     [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
                                              presentingWindow:presentingWindow
                                                     loginHint:hint
                                                 addScopesFlow:NO
                                                        scopes:additionalScopes
+                                                        nonce:nonce
                                                    completion:completion];
   [self signInWithOptions:options];
 }
@@ -573,7 +599,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     if (!_configuration) {
       // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
       [NSException raise:NSInvalidArgumentException
-                  format:@"No active configuration.  Make sure GIDClientID is set in Info.plist."];
+                  format:@"No active configuration. Make sure GIDClientID is set in Info.plist."];
       return;
     }
 
@@ -667,7 +693,6 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
       [_timedLoader startTiming];
       [self->_appCheck getLimitedUseTokenWithCompletion:^(GACAppCheckToken * _Nullable token,
                                                           NSError * _Nullable error) {
-        OIDAuthorizationRequest *request = nil;
         if (token) {
           additionalParameters[kClientAssertionTypeParameter] = kClientAssertionTypeParameterValue;
           additionalParameters[kClientAssertionParameter] = token.token;
@@ -677,7 +702,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
           NSLog(@"[Google Sign-In iOS]: Error retrieving App Check limited use token: %@", error);
         }
         #endif
-        request = [self authorizationRequestWithOptions:options
+        OIDAuthorizationRequest *request = [self authorizationRequestWithOptions:options
                                    additionalParameters:additionalParameters];
         if (self->_timedLoader.animationStatus == GIDTimedLoaderAnimationStatusAnimating) {
           [self->_timedLoader stopTimingWithCompletion:^{
@@ -707,6 +732,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                                                       scopes:options.scopes
                                                  redirectURL:[self redirectURLWithOptions:options]
                                                 responseType:OIDResponseTypeCode
+                                                       nonce:options.nonce
                                         additionalParameters:additionalParameters];
   return request;
 }
@@ -758,7 +784,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 - (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse
                                error:(NSError *)error
-                          emmSupport:(NSString *)emmSupport{
+                          emmSupport:(NSString *)emmSupport {
   if (_restarting) {
     // The auth flow is restarting, so the work here would be performed in the next round.
     _restarting = NO;

+ 6 - 0
GoogleSignIn/Sources/GIDSignInInternalOptions.h

@@ -64,6 +64,10 @@ NS_ASSUME_NONNULL_BEGIN
 /// The login hint to be used during the flow.
 @property(nonatomic, copy, nullable) NSString *loginHint;
 
+/// A cryptographically random value used to associate a Client session with an ID Token,
+/// and to mitigate replay attacks.
+@property(nonatomic, readonly, copy, nullable) NSString *nonce;
+
 /// Creates the default options.
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
@@ -77,6 +81,7 @@ NS_ASSUME_NONNULL_BEGIN
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
+                                          nonce:(nullable NSString *)nonce
                                      completion:(nullable GIDSignInCompletion)completion;
 
 #elif TARGET_OS_OSX
@@ -91,6 +96,7 @@ NS_ASSUME_NONNULL_BEGIN
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
+                                          nonce:(nullable NSString *)nonce
                                      completion:(nullable GIDSignInCompletion)completion;
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
 

+ 4 - 0
GoogleSignIn/Sources/GIDSignInInternalOptions.m

@@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
+                                          nonce:(nullable NSString *)nonce
                                      completion:(nullable GIDSignInCompletion)completion {
 #elif TARGET_OS_OSX
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
@@ -38,6 +39,7 @@ NS_ASSUME_NONNULL_BEGIN
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
+                                          nonce:(nullable NSString *)nonce
                                      completion:(nullable GIDSignInCompletion)completion {
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
   GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
@@ -54,6 +56,7 @@ NS_ASSUME_NONNULL_BEGIN
     options->_loginHint = loginHint;
     options->_completion = completion;
     options->_scopes = [GIDScopes scopesWithBasicProfile:scopes];
+    options->_nonce = nonce;
   }
   return options;
 }
@@ -80,6 +83,7 @@ NS_ASSUME_NONNULL_BEGIN
                                                                     loginHint:loginHint
                                                                 addScopesFlow:addScopesFlow
                                                                        scopes:@[]
+                                                                        nonce:nil
                                                                    completion:completion];
   return options;
 }

+ 48 - 1
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h

@@ -196,6 +196,31 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.")
                    NSError *_Nullable error))completion
 NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
 
+
+/// Starts an interactive sign-in flow on iOS using the provided hint, additional scopes, and nonce.
+///
+/// The completion will be called at the end of this process.  Any saved sign-in state will be
+/// replaced by the result of this flow.  Note that this method should not be called when the app is
+/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the
+/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in.
+///
+/// @param presentingViewController The view controller used to present `SFSafariViewController` on
+///     iOS 9 and 10.
+/// @param hint An optional hint for the authorization server, for example the user's ID or email
+///     address, to be prefilled if possible.
+/// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes.
+/// @param nonce A custom nonce.
+/// @param completion The optional block that is called on completion.  This block will
+///     be called asynchronously on the main queue.
+- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
+                                      hint:(nullable NSString *)hint
+                          additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
+                                     nonce:(nullable NSString *)nonce
+                                completion:
+    (nullable void (^)(GIDSignInResult *_Nullable signInResult,
+                       NSError *_Nullable error))completion
+    NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
+
 #elif TARGET_OS_OSX
 
 /// Starts an interactive sign-in flow on macOS.
@@ -229,7 +254,7 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.")
                         completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult,
                                                       NSError *_Nullable error))completion;
 
-/// Starts an interactive sign-in flow on macOS using the provided hint.
+/// Starts an interactive sign-in flow on macOS using the provided hint and additional scopes.
 ///
 /// The completion will be called at the end of this process.  Any saved sign-in state will be
 /// replaced by the result of this flow.  Note that this method should not be called when the app is
@@ -248,6 +273,28 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.")
                         completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult,
                                                       NSError *_Nullable error))completion;
 
+/// Starts an interactive sign-in flow on macOS using the provided hint, additional scopes, and nonce.
+///
+/// The completion will be called at the end of this process.  Any saved sign-in state will be
+/// replaced by the result of this flow.  Note that this method should not be called when the app is
+/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the
+/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in.
+///
+/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`.
+/// @param hint An optional hint for the authorization server, for example the user's ID or email
+///     address, to be prefilled if possible.
+/// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes.
+/// @param nonce A custom nonce.
+/// @param completion The optional block that is called on completion.  This block will
+///     be called asynchronously on the main queue.
+- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
+                              hint:(nullable NSString *)hint
+                  additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
+                             nonce:(nullable NSString *)nonce
+                        completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult,
+                                                      NSError *_Nullable error))completion;
+
+
 #endif
 
 @end

+ 46 - 8
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -500,8 +500,7 @@ static NSString *const kNewScope = @"newScope";
   
   // Mock generating a GIDConfiguration when initializing GIDGoogleUser.
   OIDAuthorizationResponse *authResponse =
-      [OIDAuthorizationResponse testInstanceWithAdditionalParameters:nil
-                                                         errorString:nil];
+      [OIDAuthorizationResponse testInstance];
   
   OCMStub([_authState lastAuthorizationResponse]).andReturn(authResponse);
   OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken);
@@ -676,7 +675,8 @@ static NSString *const kNewScope = @"newScope";
                      oldAccessToken:NO
                         modalCancel:NO
                 useAdditionalScopes:YES
-                   additionalScopes:nil];
+                   additionalScopes:nil
+                        manualNonce:nil];
 
   expectedScopeString = [@[ @"email", @"profile" ] componentsJoinedByString:@" "];
   XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
@@ -690,7 +690,8 @@ static NSString *const kNewScope = @"newScope";
                      oldAccessToken:NO
                         modalCancel:NO
                 useAdditionalScopes:YES
-                   additionalScopes:@[ kScope ]];
+                   additionalScopes:@[ kScope ]
+                        manualNonce:nil];
 
   expectedScopeString = [@[ kScope, @"email", @"profile" ] componentsJoinedByString:@" "];
   XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
@@ -704,7 +705,8 @@ static NSString *const kNewScope = @"newScope";
                      oldAccessToken:NO
                         modalCancel:NO
                 useAdditionalScopes:YES
-                   additionalScopes:@[ kScope, kScope2 ]];
+                   additionalScopes:@[ kScope, kScope2 ]
+                        manualNonce:nil];
 
   expectedScopeString = [@[ kScope, kScope2, @"email", @"profile" ] componentsJoinedByString:@" "];
   XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
@@ -796,6 +798,37 @@ static NSString *const kNewScope = @"newScope";
   XCTAssertEqual(params[kOpenIDRealmKey], kOpenIDRealm, @"OpenID Realm should match.");
 }
 
+- (void)testManualNonce {
+  _signIn.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
+                                                      serverClientID:nil
+                                                        hostedDomain:nil
+                                                         openIDRealm:kOpenIDRealm];
+
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
+  NSString* manualNonce = @"manual_nonce";
+  
+  [self OAuthLoginWithAddScopesFlow:NO
+                          authError:nil
+                         tokenError:nil
+            emmPasscodeInfoRequired:NO
+                      keychainError:NO
+                     restoredSignIn:NO
+                     oldAccessToken:NO
+                        modalCancel:NO
+                useAdditionalScopes:NO
+                   additionalScopes:@[]
+                        manualNonce:manualNonce];
+
+  XCTAssertEqualObjects(_savedAuthorizationRequest.nonce,
+                        manualNonce,
+                        @"Provided nonce should match nonce in authorization request.");
+}
+
 - (void)testOAuthLogin_LoginHint {
   _hint = kUserEmail;
 
@@ -1375,7 +1408,8 @@ static NSString *const kNewScope = @"newScope";
                      oldAccessToken:oldAccessToken
                         modalCancel:modalCancel
                 useAdditionalScopes:NO
-                   additionalScopes:nil];
+                   additionalScopes:nil
+                        manualNonce:nil];
 }
 
 // The authorization flow with parameters to control which branches to take.
@@ -1388,11 +1422,12 @@ static NSString *const kNewScope = @"newScope";
                      oldAccessToken:(BOOL)oldAccessToken
                         modalCancel:(BOOL)modalCancel
                 useAdditionalScopes:(BOOL)useAdditionalScopes
-                   additionalScopes:(NSArray *)additionalScopes {
+                   additionalScopes:(NSArray *)additionalScopes 
+                        manualNonce:(NSString *)nonce {
   if (restoredSignIn) {
     // clearAndAuthenticateWithOptions
     [[[_authorization expect] andReturn:_authState] authState];
-    BOOL isAuthorized = restoredSignIn ? YES : NO;
+    BOOL isAuthorized = restoredSignIn;
     [[[_authState expect] andReturnValue:[NSNumber numberWithBool:isAuthorized]] isAuthorized];
   }
 
@@ -1400,6 +1435,7 @@ static NSString *const kNewScope = @"newScope";
       @{ @"emm_passcode_info_required" : @"1" } : nil;
   OIDAuthorizationResponse *authResponse =
       [OIDAuthorizationResponse testInstanceWithAdditionalParameters:additionalParameters
+                                                               nonce:nonce
                                                          errorString:authError];
 
   OIDTokenResponse *tokenResponse =
@@ -1475,6 +1511,8 @@ static NSString *const kNewScope = @"newScope";
         [_signIn signInWithPresentingWindow:_presentingWindow
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
                                        hint:_hint
+                           additionalScopes:nil
+                                      nonce:nonce
                                  completion:completion];
       }
     }

+ 7 - 5
GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h

@@ -20,13 +20,15 @@
 #import <AppAuth/OIDAuthorizationRequest.h>
 #endif
 
-extern NSString *const OIDAuthorizationRequestTestingClientID;
-extern NSString *const OIDAuthorizationRequestTestingScope;
-extern NSString *const OIDAuthorizationRequestTestingScope2;
-extern NSString *const OIDAuthorizationRequestTestingCodeVerifier;
+extern NSString * _Nonnull const OIDAuthorizationRequestTestingClientID;
+extern NSString * _Nonnull const OIDAuthorizationRequestTestingScope;
+extern NSString * _Nonnull const OIDAuthorizationRequestTestingScope2;
+extern NSString * _Nonnull const OIDAuthorizationRequestTestingCodeVerifier;
 
 @interface OIDAuthorizationRequest (Testing)
 
-+ (instancetype)testInstance;
++ (instancetype _Nonnull)testInstance;
+
++ (instancetype _Nonnull)testInstanceWithNonce:(nullable NSString *)nonce;
 
 @end

+ 5 - 0
GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.m

@@ -32,6 +32,10 @@ NSString *const OIDAuthorizationRequestTestingCodeVerifier = @"codeVerifier";
 @implementation OIDAuthorizationRequest (Testing)
 
 + (instancetype)testInstance {
+  return [self testInstanceWithNonce:nil];
+}
+
++ (instancetype)testInstanceWithNonce:(nullable NSString *)nonce {
   return [[OIDAuthorizationRequest alloc]
       initWithConfiguration:[OIDServiceConfiguration testInstance]
                    clientId:OIDAuthorizationRequestTestingClientID
@@ -39,6 +43,7 @@ NSString *const OIDAuthorizationRequestTestingCodeVerifier = @"codeVerifier";
                                OIDAuthorizationRequestTestingScope2 ]
                 redirectURL:[NSURL URLWithString:@"http://test.com"]
                responseType:OIDResponseTypeCode
+                      nonce:nonce
        additionalParameters:nil];
 }
 

+ 1 - 0
GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h

@@ -26,6 +26,7 @@
 
 + (instancetype)testInstanceWithAdditionalParameters:
     (NSDictionary<NSString *, NSString *> *)additionalParameters
+                                               nonce:(NSString *)nonce
                                          errorString:(NSString *)errorString;
 
 @end

+ 3 - 2
GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m

@@ -25,11 +25,12 @@
 @implementation OIDAuthorizationResponse (Testing)
 
 + (instancetype)testInstance {
-  return [self testInstanceWithAdditionalParameters:nil errorString:nil];
+  return [self testInstanceWithAdditionalParameters:nil nonce:nil errorString:nil];
 }
 
 + (instancetype)testInstanceWithAdditionalParameters:
     (NSDictionary<NSString *, NSString *> *)additionalParameters
+                                               nonce:(NSString *)nonce
                                          errorString:(NSString *)errorString {
 
   NSMutableDictionary<NSString *, NSString *> *parameters;
@@ -45,7 +46,7 @@
       [parameters addEntriesFromDictionary:additionalParameters];
     }
   }
-  return [[OIDAuthorizationResponse alloc] initWithRequest:[OIDAuthorizationRequest testInstance]
+  return [[OIDAuthorizationResponse alloc] initWithRequest:[OIDAuthorizationRequest testInstanceWithNonce:nonce]
                                                 parameters:parameters];
 }
 

+ 1 - 2
GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m

@@ -70,8 +70,7 @@ NSString * const kFatPictureURL = @"fake_user_picture_url";
                               expiresIn:(NSNumber *)expiresIn
                            refreshToken:(NSString *)refreshToken
                            tokenRequest:(OIDTokenRequest *)tokenRequest {
-  NSMutableDictionary<NSString *, NSString *> *parameters;
-  parameters = [[NSMutableDictionary alloc] initWithDictionary:@{
+  NSMutableDictionary<NSString *, NSString *> *parameters = [[NSMutableDictionary alloc] initWithDictionary:@{
     @"access_token" : accessToken ?: kAccessToken,
     @"expires_in" : expiresIn ?: @(kAccessTokenExpiresIn),
     @"token_type" : @"example_token_type",

+ 1 - 1
Samples/ObjC/SignInSample/Podfile

@@ -1,4 +1,4 @@
-platform :ios, '11.0'
+platform :ios, '12.0'
 use_frameworks!
 
 target 'SampleForPod' do

+ 1 - 1
Samples/ObjC/SignInSample/README.md

@@ -18,7 +18,7 @@ open SignInSampleForPod.xcworkspace
 
 3. Run the 'SampleForPod' target.
 
-## Swift Pacakage Manager
+## Swift Package Manager
 
 1. In the `../Sample/ObjC/SignInSample/` folder, open the
 [Swift Package Manager](https://swift.org/package-manager/) project:

+ 5 - 0
Samples/ObjC/SignInSample/Source/SignInViewController.m

@@ -21,6 +21,7 @@
 #import "AuthInspectorViewController.h"
 #import "DataPickerState.h"
 #import "DataPickerViewController.h"
+#import <AppAuth/OIDTokenUtilities.h>
 
 static NSString *const kSignInViewTitle = @"Sign-In Sample";
 static NSString *const kPlaceholderUserName = @"<Name>";
@@ -247,7 +248,11 @@ static NSString *const kCredentialsButtonAccessibilityIdentifier = @"Credentials
 #pragma mark - IBActions
 
 - (IBAction)signIn:(id)sender {
+  NSString* nonce = [OIDTokenUtilities randomURLSafeStringWithSize:32];
   [GIDSignIn.sharedInstance signInWithPresentingViewController:self
+                                                          hint:nil
+                                              additionalScopes:nil
+                                                         nonce:nonce
                                                     completion:^(GIDSignInResult *signInResult,
                                                                  NSError *error) {
     if (error) {