From efe957433c4c8e7310226d2385a0d60256046467 Mon Sep 17 00:00:00 2001 From: Matt Mathias Date: Wed, 10 Dec 2025 16:36:02 -0800 Subject: [PATCH 1/7] Remove keychain entries if fresh install --- GoogleSignIn/Sources/GIDSignIn.m | 8 ++++++-- GoogleSignIn/Sources/GIDSignIn_Private.h | 3 +++ GoogleSignIn/Tests/Unit/GIDSignInTest.m | 14 ++++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 138337c2..63b19c7e 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -120,8 +120,7 @@ // Error string for user cancelations. static NSString *const kUserCanceledError = @"The user canceled the sign-in flow."; -// User preference key to detect fresh install of the app. -static NSString *const kAppHasRunBeforeKey = @"GID_AppHasRunBefore"; +NSString *const kAppHasRunBeforeKey = @"GID_AppHasRunBefore"; // Maximum retry interval in seconds for the fetcher. static const NSTimeInterval kFetcherMaxRetryInterval = 15.0; @@ -672,6 +671,11 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore // Check to see if the 3P app is being run for the first time after a fresh install. BOOL isFreshInstall = [self isFreshInstall]; + + // If this is a fresh install, ensure that any pre-existing keychain data is purged. + if (isFreshInstall) { + [self removeAllKeychainEntries]; + } NSString *authorizationEnpointURL = [NSString stringWithFormat:kAuthorizationURLTemplate, [GIDSignInPreferences googleAuthorizationServer]]; diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index bb642cb7..803cc10e 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -32,6 +32,9 @@ NS_ASSUME_NONNULL_BEGIN @class GIDAppCheck; @class GIDAuthStateMigration; +/// User preference key to detect fresh install of the app. +extern NSString *const kAppHasRunBeforeKey; + /// Represents a completion block that takes a `GIDSignInResult` on success or an error if the /// operation was unsuccessful. typedef void (^GIDSignInCompletion)(GIDSignInResult *_Nullable signInResult, diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 99fe7ed3..b67b7a88 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -120,8 +120,6 @@ @"com.google.UnitTests:///emmcallback?action=unrecognized"; static NSString * const kDevicePolicyAppBundleID = @"com.google.DevicePolicy"; -static NSString * const kAppHasRunBeforeKey = @"GPP_AppHasRunBefore"; - static NSString * const kFingerprintKeychainName = @"fingerprint"; static NSString * const kVerifierKeychainName = @"verifier"; static NSString * const kVerifierKey = @"verifier"; @@ -1212,6 +1210,18 @@ - (void)testNotHandleWrongPath { XCTAssertFalse(_completionCalled, @"should not call delegate"); } +#pragma mark - Test Fresh Install + +- (void)testFreshInstall_removesKeychainEntries { + // Simulate that the app has been deleted and user defaults removed. + [NSUserDefaults.standardUserDefaults removeObjectForKey:kAppHasRunBeforeKey]; + // Initialization should check `isFreshInstall`. + GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore + authStateMigrationService:_authStateMigrationService]; + // If `isFreshInstall`, keychain entries should be removed. + XCTAssertTrue(self->_keychainRemoved); +} + #pragma mark - Tests - disconnectWithCallback: // Verifies disconnect calls callback with no errors if access token is present. From 332313c6953350ca8b70bc794bbd25f75a4831d1 Mon Sep 17 00:00:00 2001 From: Matt Mathias Date: Wed, 10 Dec 2025 17:17:33 -0800 Subject: [PATCH 2/7] Make sure to use signIn within test pod lib lint fails with a warning due to the unused variable --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 1 + 1 file changed, 1 insertion(+) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index b67b7a88..5a3183c1 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -1219,6 +1219,7 @@ - (void)testFreshInstall_removesKeychainEntries { GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore authStateMigrationService:_authStateMigrationService]; // If `isFreshInstall`, keychain entries should be removed. + XCTAssertNotNil(signIn); XCTAssertTrue(self->_keychainRemoved); } From 16e008f557e46fdae1277556cf0006957fa77029 Mon Sep 17 00:00:00 2001 From: Matt Mathias Date: Thu, 26 Feb 2026 17:32:38 -0800 Subject: [PATCH 3/7] Add comment to README --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 3376e1f4..49ac22ce 100644 --- a/README.md +++ b/README.md @@ -107,3 +107,30 @@ let signInButton = GoogleSignInButton { } let hostedButton = NSHostingView(rootView: signInButton) ``` + +## A Note iOS Keychain Access Groups + +On iOS, if you do not supply a custom Keychain access group, the system creates +a Keychain access group by prepending `$(AppIdentifierPrefix)` to your bundle +ID (e.g., `$(AppIdentifierPrefix).com.example.MyApp`), which becomes the +default access group for just your app ([Apple documentation](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps#Establish-your-apps-private-access-group)). + +If, however, you add a new Keychain access group (and add the entitlement to +your app), then Xcode will use whatever access group is listed first in the +list as the default. So, if the shared access group is first, then it becomes +the default Keychain for your app. + +The implication of this scenario is that credentials saved by GSI (via +[GTMAppAuth](https://github.com/google/GTMAppAuth)) on behalf of your app will +be stored in the shared keychain access group. + +You should make sure that you want this behavior because GSI [removes Keychain +items upon fresh install](https://github.com/google/GoogleSignIn-iOS/pull/567) +to ensure that stale credentials from previous installs of your app are not +mistakenly used. This behavior can lead new installs of apps sharing the same +Keychain access group to remove Keychain credentials for apps already installed. + +You can mitigate this by explicitly listing the typical default access group +(or whatever you prefer) in your list first. GSI, via GTMAppAuth, will then use +that default access group. Make sure that you also update your code that writes +to the Keychain to explicitly use the shared access group as needed. From affbd507d2837ebb0baf9425ed5412f97ee0b31d Mon Sep 17 00:00:00 2001 From: Matt Mathias Date: Fri, 27 Feb 2026 11:03:14 -0800 Subject: [PATCH 4/7] Update macOS keychain guidance --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49ac22ce..9f2778f5 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Google Sign-In allows your users to sign-in to your native macOS app using their and default browser. When building for macOS, the `signInWithConfiguration:` and `addScopes:` methods take a `presentingWindow:` parameter in place of `presentingViewController:`. Note that in order for your macOS app to store credentials via the Keychain on macOS, you will need to add -`$(AppIdentifierPrefix)$(CFBundleIdentifier)` to its keychain access group. +`$(AppIdentifierPrefix)$(CFBundleIdentifier)` as the first item in its keychain access group. ### Mac Catalyst From 3bfeb43d802b47947078ff1b544bc233638cdf7f Mon Sep 17 00:00:00 2001 From: Matt Mathias Date: Fri, 27 Feb 2026 12:19:43 -0800 Subject: [PATCH 5/7] Update keychain note --- README.md | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9f2778f5..1588cdf7 100644 --- a/README.md +++ b/README.md @@ -108,29 +108,18 @@ let signInButton = GoogleSignInButton { let hostedButton = NSHostingView(rootView: signInButton) ``` -## A Note iOS Keychain Access Groups +## A Note on iOS Keychain Access Groups -On iOS, if you do not supply a custom Keychain access group, the system creates -a Keychain access group by prepending `$(AppIdentifierPrefix)` to your bundle -ID (e.g., `$(AppIdentifierPrefix).com.example.MyApp`), which becomes the -default access group for just your app ([Apple documentation](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps#Establish-your-apps-private-access-group)). +GSI uses your default (first listed) keychain access group. If you don't add a +custom keychain access group, the default keychain access group is provided by +Xcode and looks like `$(AppIdentifierPrefix)$(CFBundleIdentifier)`. -If, however, you add a new Keychain access group (and add the entitlement to -your app), then Xcode will use whatever access group is listed first in the -list as the default. So, if the shared access group is first, then it becomes -the default Keychain for your app. - -The implication of this scenario is that credentials saved by GSI (via -[GTMAppAuth](https://github.com/google/GTMAppAuth)) on behalf of your app will -be stored in the shared keychain access group. - -You should make sure that you want this behavior because GSI [removes Keychain -items upon fresh install](https://github.com/google/GoogleSignIn-iOS/pull/567) +GSI [removes keychain items upon fresh install](https://github.com/google/GoogleSignIn-iOS/pull/567) to ensure that stale credentials from previous installs of your app are not -mistakenly used. This behavior can lead new installs of apps sharing the same -Keychain access group to remove Keychain credentials for apps already installed. +mistakenly used. If your app uses a shared access group by default this may +lead to new installs of apps sharing the same keychain access group to remove +keychain credentials for apps already installed. -You can mitigate this by explicitly listing the typical default access group -(or whatever you prefer) in your list first. GSI, via GTMAppAuth, will then use -that default access group. Make sure that you also update your code that writes -to the Keychain to explicitly use the shared access group as needed. +To prevent unintentional credential removal, you can explicitly list the +typical default access group (or whatever you prefer) in your list first. GSI, +will then use that default access group. From 931a97534c3686434f6842b0e49af8c80afcd0b7 Mon Sep 17 00:00:00 2001 From: Matt Mathias Date: Fri, 27 Feb 2026 13:47:08 -0800 Subject: [PATCH 6/7] Update Mac Catalyst section --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1588cdf7..db242668 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ in order for your macOS app to store credentials via the Keychain on macOS, you Google Sign-In also supports iOS apps that are built for macOS via [Mac Catalyst](https://developer.apple.com/mac-catalyst/). In order for your Mac Catalyst app to store credentials via the Keychain on macOS, you will need to add -`$(AppIdentifierPrefix)$(CFBundleIdentifier)` to its keychain access group. +`$(AppIdentifierPrefix)$(CFBundleIdentifier)` as the first item in the keychain +access group. ## Using the Google Sign-In Button From 293b68a04d39fa8429b60d698d124f58c1dba3e9 Mon Sep 17 00:00:00 2001 From: Matt Mathias Date: Fri, 27 Feb 2026 14:07:11 -0800 Subject: [PATCH 7/7] Clarify that 'whatever you prefer' should not be shared --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db242668..ade720e0 100644 --- a/README.md +++ b/README.md @@ -122,5 +122,5 @@ lead to new installs of apps sharing the same keychain access group to remove keychain credentials for apps already installed. To prevent unintentional credential removal, you can explicitly list the -typical default access group (or whatever you prefer) in your list first. GSI, -will then use that default access group. +typical default access group (or whatever you prefer so long as it is not +shared) in your list first. GSI, will then use that default access group.