Просмотр исходного кода

Merge branch 'main' into briannamorales/vwg-flow

brianna 1 год назад
Родитель
Сommit
325a706530

+ 6 - 5
GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.m

@@ -141,10 +141,10 @@
 
   // TODO: Clean up callback flow (#427).
   [authFlow wait];
-  [OIDAuthorizationService
-      performTokenRequest:tokenRequest
-                 callback:^(OIDTokenResponse *_Nullable tokenResponse,
-                            NSError *_Nullable error) {
+  [OIDAuthorizationService performTokenRequest:tokenRequest
+                 originalAuthorizationResponse:authFlow.authState.lastAuthorizationResponse
+                                      callback:^(OIDTokenResponse *_Nullable tokenResponse,
+                                                 NSError *_Nullable error) {
     [authState updateWithTokenResponse:tokenResponse error:error];
     authFlow.error = error;
 
@@ -211,7 +211,8 @@
   switch (_flowName) {
     case GIDFlowNameSignIn: {
       GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
-      if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) {
+      if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow ||
+          error.code == OIDErrorCodeProgramCanceledAuthorizationFlow) {
         // The user has canceled the flow at the iOS modal dialog.
         errorString = kUserCanceledSignInError;
         errorCode = kGIDSignInErrorCodeCanceled;

+ 46 - 11
GoogleSignIn/Sources/GIDSignIn.m

@@ -234,12 +234,25 @@ static NSString *const kClientAssertionTypeParameterValue =
                                       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];
 }
@@ -315,12 +328,25 @@ static NSString *const kClientAssertionTypeParameterValue =
                               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];
 }
@@ -539,7 +565,7 @@ static NSString *const kClientAssertionTypeParameterValue =
     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;
     }
 
@@ -633,7 +659,6 @@ static NSString *const kClientAssertionTypeParameterValue =
       [_timedLoader startTiming];
       [self->_appCheck getLimitedUseTokenWithCompletion:^(GACAppCheckToken * _Nullable token,
                                                           NSError * _Nullable error) {
-        OIDAuthorizationRequest *request = nil;
         if (token) {
           additionalParameters[kClientAssertionTypeParameter] = kClientAssertionTypeParameterValue;
           additionalParameters[kClientAssertionParameter] = token.token;
@@ -643,7 +668,7 @@ static NSString *const kClientAssertionTypeParameterValue =
           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:^{
@@ -667,13 +692,23 @@ static NSString *const kClientAssertionTypeParameterValue =
 - (OIDAuthorizationRequest *)
     authorizationRequestWithOptions:(GIDSignInInternalOptions *)options
                additionalParameters:(NSDictionary<NSString *, NSString *> *)additionalParameters {
-  OIDAuthorizationRequest *request =
-      [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
-                                                    clientId:options.configuration.clientID
-                                                      scopes:options.scopes
-                                                 redirectURL:[self redirectURLWithOptions:options]
-                                                responseType:OIDResponseTypeCode
-                                        additionalParameters:additionalParameters];
+  OIDAuthorizationRequest *request;
+  if (options.nonce) {
+    request = [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
+                                                            clientId:options.configuration.clientID
+                                                              scopes:options.scopes
+                                                         redirectURL:[self redirectURLWithOptions:options]
+                                                        responseType:OIDResponseTypeCode
+                                                               nonce:options.nonce
+                                                additionalParameters:additionalParameters];
+  } else {
+    request = [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
+                                                            clientId:options.configuration.clientID
+                                                              scopes:options.scopes
+                                                         redirectURL:[self redirectURLWithOptions:options]
+                                                        responseType:OIDResponseTypeCode
+                                                additionalParameters:additionalParameters];
+  }
   return request;
 }
 
@@ -724,7 +759,7 @@ static NSString *const kClientAssertionTypeParameterValue =
 
 - (void)processAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse
                                error:(nullable 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

@@ -78,6 +78,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
@@ -100,6 +104,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
@@ -113,6 +118,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

@@ -54,6 +54,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
@@ -61,6 +62,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];
@@ -77,6 +79,7 @@ NS_ASSUME_NONNULL_BEGIN
     options->_loginHint = loginHint;
     options->_completion = completion;
     options->_scopes = [GIDScopes scopesWithBasicProfile:scopes];
+    options->_nonce = nonce;
   }
   return options;
 }
@@ -103,6 +106,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

+ 48 - 9
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -329,7 +329,8 @@ static NSString *const kNewScope = @"newScope";
                          callback:COPY_TO_ARG_BLOCK(self->_savedAuthorizationCallback)]);
   OCMStub([self->_oidAuthorizationService
       performTokenRequest:SAVE_TO_ARG_BLOCK(self->_savedTokenRequest)
-                 callback:COPY_TO_ARG_BLOCK(self->_savedTokenCallback)]);
+      originalAuthorizationResponse:[OCMArg any]
+      callback:COPY_TO_ARG_BLOCK(self->_savedTokenCallback)]);
 
   // Fakes
   _fetcherService = [[GIDFakeFetcherService alloc] init];
@@ -502,8 +503,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);
@@ -678,7 +678,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);
@@ -692,7 +693,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);
@@ -706,7 +708,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);
@@ -803,6 +806,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;
 
@@ -1408,7 +1442,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.
@@ -1421,11 +1456,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];
   }
 
@@ -1433,6 +1469,7 @@ static NSString *const kNewScope = @"newScope";
       @{ @"emm_passcode_info_required" : @"1" } : nil;
   OIDAuthorizationResponse *authResponse =
       [OIDAuthorizationResponse testInstanceWithAdditionalParameters:additionalParameters
+                                                               nonce:nonce
                                                          errorString:authError];
 
   OIDTokenResponse *tokenResponse =
@@ -1508,6 +1545,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;
 
 + (instancetype)testInstanceNoAuthCodeWithAdditionalParameters:

+ 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

@@ -94,8 +94,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) {

+ 55 - 2
Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift

@@ -35,12 +35,28 @@ final class GoogleSignInAuthenticator: ObservableObject {
       print("There is no root view controller!")
       return
     }
-
-    GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { signInResult, error in
+    let manualNonce = UUID().uuidString
+
+    GIDSignIn.sharedInstance.signIn(
+      withPresenting: rootViewController,
+      hint: nil,
+      additionalScopes: nil,
+      nonce: manualNonce
+    ) { signInResult, error in
       guard let signInResult = signInResult else {
         print("Error! \(String(describing: error))")
         return
       }
+
+      // Per OpenID Connect Core section 3.1.3.7, rule #11, compare returned nonce to manual
+      guard let idToken = signInResult.user.idToken?.tokenString,
+            let returnedNonce = self.decodeNonce(fromJWT: idToken),
+            returnedNonce == manualNonce else {
+        // Assert a failure for convenience so that integration tests with this sample app fail upon
+        // `nonce` mismatch
+        assertionFailure("ERROR: Returned nonce doesn't match manual nonce!")
+        return
+      }
       self.authViewModel.state = .signedIn(signInResult.user)
     }
 
@@ -125,3 +141,40 @@ final class GoogleSignInAuthenticator: ObservableObject {
   }
 
 }
+
+// MARK: Parse nonce from JWT ID Token
+
+private extension GoogleSignInAuthenticator {
+  func decodeNonce(fromJWT jwt: String) -> String? {
+    let segments = jwt.components(separatedBy: ".")
+    guard let parts = decodeJWTSegment(segments[1]),
+          let nonce = parts["nonce"] as? String else {
+      return nil
+    }
+    return nonce
+  }
+
+  func decodeJWTSegment(_ segment: String) -> [String: Any]? {
+    guard let segmentData = base64UrlDecode(segment),
+          let segmentJSON = try? JSONSerialization.jsonObject(with: segmentData, options: []),
+          let payload = segmentJSON as? [String: Any] else {
+      return nil
+    }
+    return payload
+  }
+
+  func base64UrlDecode(_ value: String) -> Data? {
+    var base64 = value
+      .replacingOccurrences(of: "-", with: "+")
+      .replacingOccurrences(of: "_", with: "/")
+
+    let length = Double(base64.lengthOfBytes(using: String.Encoding.utf8))
+    let requiredLength = 4 * ceil(length / 4.0)
+    let paddingLength = requiredLength - length
+    if paddingLength > 0 {
+      let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
+      base64 = base64 + padding
+    }
+    return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
+  }
+}