Skip to content

Commit 9e8be62

Browse files
Add support for GTMAppAuth 5 on macOS (#522)
Co-authored-by: Brianna Morales <[email protected]>
1 parent 5861dad commit 9e8be62

File tree

7 files changed

+184
-66
lines changed

7 files changed

+184
-66
lines changed

GoogleSignIn/Sources/GIDAuthStateMigration.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
2727
/// Creates an instance of this migration type with the keychain storage wrapper it will use.
2828
- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore NS_DESIGNATED_INITIALIZER;
2929

30-
/// Perform a one-time migration for auth state saved by GPPSignIn 1.x or GIDSignIn 1.0 - 4.x to the
31-
/// GTMAppAuth storage introduced in GIDSignIn 5.0.
30+
/// Perform necessary migrations from legacy auth state storage to most recent GTMAppAuth version.
3231
- (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL
3332
callbackPath:(NSString *)callbackPath
3433
keychainName:(NSString *)keychainName

GoogleSignIn/Sources/GIDAuthStateMigration.m

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@
2626

2727
NS_ASSUME_NONNULL_BEGIN
2828

29-
// User preference key to detect whether or not the migration check has been performed.
30-
static NSString *const kMigrationCheckPerformedKey = @"GID_MigrationCheckPerformed";
29+
// User preference key to detect whether or not the migration to GTMAppAuth has been performed.
30+
static NSString *const kGTMAppAuthMigrationCheckPerformedKey = @"GID_MigrationCheckPerformed";
31+
32+
// User preference key to detect whether or not the data protected migration has been performed.
33+
static NSString *const kDataProtectedMigrationCheckPerformedKey =
34+
@"GID_DataProtectedMigrationCheckPerformed";
3135

3236
// Keychain account used to store additional state in SDKs previous to v5, including GPPSignIn.
3337
static NSString *const kOldKeychainAccount = @"GooglePlus";
@@ -63,32 +67,85 @@ - (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL
6367
callbackPath:(NSString *)callbackPath
6468
keychainName:(NSString *)keychainName
6569
isFreshInstall:(BOOL)isFreshInstall {
70+
// If this is a fresh install, take no action and mark the migration checks as having been
71+
// performed.
72+
if (isFreshInstall) {
73+
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
74+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
75+
[defaults setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey];
76+
#elif TARGET_OS_IOS
77+
[defaults setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey];
78+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
79+
return;
80+
}
81+
82+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
83+
[self performDataProtectedMigrationIfNeeded];
84+
#elif TARGET_OS_IOS
85+
[self performGIDMigrationIfNeededWithTokenURL:tokenURL
86+
callbackPath:callbackPath
87+
keychainName:keychainName];
88+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
89+
}
90+
91+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
92+
// Migrate from the fileBasedKeychain to dataProtectedKeychain with GTMAppAuth 5.0.
93+
- (void)performDataProtectedMigrationIfNeeded {
6694
// See if we've performed the migration check previously.
6795
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
68-
if ([defaults boolForKey:kMigrationCheckPerformedKey]) {
96+
if ([defaults boolForKey:kDataProtectedMigrationCheckPerformedKey]) {
6997
return;
7098
}
7199

72-
// If this is not a fresh install, attempt to migrate state. If this is a fresh install, take no
73-
// action and go on to mark the migration check as having been performed.
74-
if (!isFreshInstall) {
75-
// Attempt migration
76-
GTMAuthSession *authSession =
77-
[self extractAuthSessionWithTokenURL:tokenURL callbackPath:callbackPath];
78-
79-
// If migration was successful, save our migrated state to the keychain.
80-
if (authSession) {
81-
NSError *err;
82-
[self.keychainStore saveAuthSession:authSession error:&err];
83-
// If we're unable to save to the keychain, return without marking migration performed.
84-
if (err) {
85-
return;
86-
};
87-
}
100+
GTMKeychainAttribute *fileBasedKeychain = [GTMKeychainAttribute useFileBasedKeychain];
101+
NSSet *attributes = [NSSet setWithArray:@[fileBasedKeychain]];
102+
GTMKeychainStore *keychainStoreLegacy =
103+
[[GTMKeychainStore alloc] initWithItemName:self.keychainStore.itemName
104+
keychainAttributes:attributes];
105+
GTMAuthSession *authSession = [keychainStoreLegacy retrieveAuthSessionWithError:nil];
106+
// If migration was successful, save our migrated state to the keychain.
107+
if (authSession) {
108+
NSError *err;
109+
[self.keychainStore saveAuthSession:authSession error:&err];
110+
// If we're unable to save to the keychain, return without marking migration performed.
111+
if (err) {
112+
return;
113+
};
114+
[keychainStoreLegacy removeAuthSessionWithError:nil];
115+
}
116+
117+
// Mark the migration check as having been performed.
118+
[defaults setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey];
119+
}
120+
121+
#elif TARGET_OS_IOS
122+
// Migrate from GPPSignIn 1.x or GIDSignIn 1.0 - 4.x to the GTMAppAuth storage introduced in
123+
// GIDSignIn 5.0.
124+
- (void)performGIDMigrationIfNeededWithTokenURL:(NSURL *)tokenURL
125+
callbackPath:(NSString *)callbackPath
126+
keychainName:(NSString *)keychainName {
127+
// See if we've performed the migration check previously.
128+
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
129+
if ([defaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]) {
130+
return;
131+
}
132+
133+
// Attempt migration
134+
GTMAuthSession *authSession =
135+
[self extractAuthSessionWithTokenURL:tokenURL callbackPath:callbackPath];
136+
137+
// If migration was successful, save our migrated state to the keychain.
138+
if (authSession) {
139+
NSError *err;
140+
[self.keychainStore saveAuthSession:authSession error:&err];
141+
// If we're unable to save to the keychain, return without marking migration performed.
142+
if (err) {
143+
return;
144+
};
88145
}
89146

90147
// Mark the migration check as having been performed.
91-
[defaults setBool:YES forKey:kMigrationCheckPerformedKey];
148+
[defaults setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey];
92149
}
93150

94151
// Returns a |GTMAuthSession| object containing any old auth state or |nil| if none
@@ -189,6 +246,7 @@ + (nullable NSString *)passwordForService:(NSString *)service {
189246
NSString *password = [[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding];
190247
return password;
191248
}
249+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
192250

193251
@end
194252

GoogleSignIn/Sources/GIDSignIn.m

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
2222
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h"
2323

24+
#import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
2425
#import "GoogleSignIn/Sources/GIDEMMSupport.h"
2526
#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
2627
#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
@@ -31,7 +32,6 @@
3132
#import <AppCheckCore/GACAppCheckToken.h>
3233
#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h"
3334
#import "GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h"
34-
#import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
3535
#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
3636
#import "GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h"
3737
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
@@ -560,15 +560,13 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
560560
initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEnpointURL]
561561
tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]];
562562
_keychainStore = keychainStore;
563-
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
564-
// Perform migration of auth state from old (before 5.0) versions of the SDK if needed.
563+
// Perform migration of auth state from old versions of the SDK if needed.
565564
GIDAuthStateMigration *migration =
566565
[[GIDAuthStateMigration alloc] initWithKeychainStore:_keychainStore];
567566
[migration migrateIfNeededWithTokenURL:_appAuthConfiguration.tokenEndpoint
568567
callbackPath:kBrowserCallbackPath
569568
keychainName:kGTMAppAuthKeychainName
570569
isFreshInstall:isFreshInstall];
571-
#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
572570
}
573571
return self;
574572
}

GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
#import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
1818
#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
19+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
20+
#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
21+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
1922

2023
@import GTMAppAuth;
2124

@@ -49,7 +52,9 @@
4952
static NSString *const kRedirectURI =
5053
@"com.googleusercontent.apps.223520599684-kg64hfn0h950oureqacja2fltg00msv3:/callback/path";
5154

52-
static NSString *const kMigrationCheckPerformedKey = @"GID_MigrationCheckPerformed";
55+
static NSString *const kGTMAppAuthMigrationCheckPerformedKey = @"GID_MigrationCheckPerformed";
56+
static NSString *const kDataProtectedMigrationCheckPerformedKey =
57+
@"GID_DataProtectedMigrationCheckPerformed";
5358
static NSString *const kFingerprintService = @"fingerprint";
5459

5560
NS_ASSUME_NONNULL_BEGIN
@@ -79,6 +84,9 @@ @implementation GIDAuthStateMigrationTest {
7984
id _mockNSBundle;
8085
id _mockGIDSignInCallbackSchemes;
8186
id _mockGTMOAuth2Compatibility;
87+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
88+
id _realLegacyGTMKeychainStore;
89+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
8290
}
8391

8492
- (void)setUp {
@@ -92,6 +100,12 @@ - (void)setUp {
92100
_mockNSBundle = OCMStrictClassMock([NSBundle class]);
93101
_mockGIDSignInCallbackSchemes = OCMStrictClassMock([GIDSignInCallbackSchemes class]);
94102
_mockGTMOAuth2Compatibility = OCMStrictClassMock([GTMOAuth2Compatibility class]);
103+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
104+
GTMKeychainAttribute *fileBasedKeychain = [GTMKeychainAttribute useFileBasedKeychain];
105+
NSSet *attributes = [NSSet setWithArray:@[fileBasedKeychain]];
106+
_realLegacyGTMKeychainStore = [[GTMKeychainStore alloc] initWithItemName:kKeychainName
107+
keychainAttributes:attributes];
108+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
95109
}
96110

97111
- (void)tearDown {
@@ -111,48 +125,72 @@ - (void)tearDown {
111125
[_mockGIDSignInCallbackSchemes stopMocking];
112126
[_mockGTMOAuth2Compatibility verify];
113127
[_mockGTMOAuth2Compatibility stopMocking];
128+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
129+
[_realLegacyGTMKeychainStore removeAuthSessionWithError:nil];
130+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
114131

115132
[super tearDown];
116133
}
117134

118135
#pragma mark - Tests
119136

120-
- (void)testMigrateIfNeeded_NoPreviousMigration {
137+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
138+
- (void)testMigrateIfNeeded_NoPreviousMigration_DataProtectedMigration {
121139
[[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
122-
[[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kMigrationCheckPerformedKey];
123-
[[_mockUserDefaults expect] setBool:YES forKey:kMigrationCheckPerformedKey];
140+
[[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kDataProtectedMigrationCheckPerformedKey];
141+
[[_mockUserDefaults expect] setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey];
124142

125143
[[_mockGTMKeychainStore expect] saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef];
144+
[[[_mockGTMKeychainStore expect] andReturn:kKeychainName] itemName];
126145

127-
[self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint];
146+
// set the auth session that will be migrated
147+
OIDAuthState *authState = [OIDAuthState testInstance];
148+
GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:authState];
149+
NSError *err;
150+
[_realLegacyGTMKeychainStore saveAuthSession:authSession error:&err];
151+
XCTAssertNil(err);
128152

129153
GIDAuthStateMigration *migration =
130154
[[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
131155
[migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
132156
callbackPath:kCallbackPath
133157
keychainName:kKeychainName
134158
isFreshInstall:NO];
159+
160+
// verify that the auth session was removed during migration
161+
XCTAssertNil([_realLegacyGTMKeychainStore retrieveAuthSessionWithError:nil]);
135162
}
136163

137-
- (void)testMigrateIfNeeded_HasPreviousMigration {
164+
- (void)testMigrateIfNeeded_KeychainFailure_DataProtectedMigration {
138165
[[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
139-
[[[_mockUserDefaults expect] andReturnValue:@YES] boolForKey:kMigrationCheckPerformedKey];
140-
[[_mockUserDefaults reject] setBool:YES forKey:kMigrationCheckPerformedKey];
166+
[[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kDataProtectedMigrationCheckPerformedKey];
167+
168+
NSError *keychainSaveError = [NSError new];
169+
OCMStub([_mockGTMKeychainStore saveAuthSession:OCMOCK_ANY error:[OCMArg setTo:keychainSaveError]]);
170+
171+
[[[_mockGTMKeychainStore expect] andReturn:kKeychainName] itemName];
172+
OIDAuthState *authState = [OIDAuthState testInstance];
173+
GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:authState];
174+
NSError *err;
175+
[_realLegacyGTMKeychainStore saveAuthSession:authSession error:&err];
176+
XCTAssertNil(err);
141177

142178
GIDAuthStateMigration *migration =
143179
[[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
144180
[migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
145181
callbackPath:kCallbackPath
146182
keychainName:kKeychainName
147183
isFreshInstall:NO];
184+
XCTAssertNotNil([_realLegacyGTMKeychainStore retrieveAuthSessionWithError:nil]);
148185
}
149186

150-
- (void)testMigrateIfNeeded_KeychainFailure {
187+
#else
188+
- (void)testMigrateIfNeeded_NoPreviousMigration_GTMAppAuthMigration {
151189
[[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
152-
[[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kMigrationCheckPerformedKey];
190+
[[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kGTMAppAuthMigrationCheckPerformedKey];
191+
[[_mockUserDefaults expect] setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey];
153192

154-
NSError *keychainSaveError = [NSError new];
155-
OCMStub([_mockGTMKeychainStore saveAuthSession:OCMOCK_ANY error:[OCMArg setTo:keychainSaveError]]);
193+
[[_mockGTMKeychainStore expect] saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef];
156194

157195
[self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint];
158196

@@ -164,18 +202,21 @@ - (void)testMigrateIfNeeded_KeychainFailure {
164202
isFreshInstall:NO];
165203
}
166204

167-
- (void)testMigrateIfNeeded_isFreshInstall {
205+
- (void)testMigrateIfNeeded_KeychainFailure_GTMAppAuthMigration {
168206
[[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
169-
[[[_mockUserDefaults expect] andReturnValue:@NO]
170-
boolForKey:kMigrationCheckPerformedKey];
171-
[[_mockUserDefaults expect] setBool:YES forKey:kMigrationCheckPerformedKey];
207+
[[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kGTMAppAuthMigrationCheckPerformedKey];
208+
209+
NSError *keychainSaveError = [NSError new];
210+
OCMStub([_mockGTMKeychainStore saveAuthSession:OCMOCK_ANY error:[OCMArg setTo:keychainSaveError]]);
211+
212+
[self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint];
172213

173214
GIDAuthStateMigration *migration =
174215
[[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
175216
[migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
176217
callbackPath:kCallbackPath
177218
keychainName:kKeychainName
178-
isFreshInstall:YES];
219+
isFreshInstall:NO];
179220
}
180221

181222
- (void)testExtractAuthorization {
@@ -201,6 +242,43 @@ - (void)testExtractAuthorization_HostedDomain {
201242

202243
XCTAssertNotNil(authorization);
203244
}
245+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
246+
247+
- (void)testMigrateIfNeeded_HasPreviousMigration {
248+
[[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
249+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
250+
[[[_mockUserDefaults expect] andReturnValue:@YES] boolForKey:kDataProtectedMigrationCheckPerformedKey];
251+
[[_mockUserDefaults reject] setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey];
252+
#else
253+
[[[_mockUserDefaults expect] andReturnValue:@YES] boolForKey:kGTMAppAuthMigrationCheckPerformedKey];
254+
[[_mockUserDefaults reject] setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey];
255+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
256+
257+
GIDAuthStateMigration *migration =
258+
[[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
259+
[migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
260+
callbackPath:kCallbackPath
261+
keychainName:kKeychainName
262+
isFreshInstall:NO];
263+
}
264+
265+
- (void)testMigrateIfNeeded_isFreshInstall {
266+
[[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
267+
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
268+
[[_mockUserDefaults expect] setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey];
269+
#else
270+
[[_mockUserDefaults expect] setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey];
271+
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
272+
273+
GIDAuthStateMigration *migration =
274+
[[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
275+
[migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
276+
callbackPath:kCallbackPath
277+
keychainName:kKeychainName
278+
isFreshInstall:YES];
279+
}
280+
281+
204282

205283
#pragma mark - Helpers
206284

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ If you would like to see a Swift example, take a look at
4747
Google Sign-In allows your users to sign-in to your native macOS app using their Google account
4848
and default browser. When building for macOS, the `signInWithConfiguration:` and `addScopes:`
4949
methods take a `presentingWindow:` parameter in place of `presentingViewController:`. Note that
50-
in order for your macOS app to store credientials via the Keychain on macOS, you will need to
51-
[sign your app](https://developer.apple.com/support/code-signing/).
50+
in order for your macOS app to store credentials via the Keychain on macOS, you will need to add
51+
`$(AppIdentifierPrefix)$(CFBundleIdentifier)` to its keychain access group.
5252

5353
### Mac Catalyst
5454

5555
Google Sign-In also supports iOS apps that are built for macOS via
5656
[Mac Catalyst](https://developer.apple.com/mac-catalyst/). In order for your Mac Catalyst app
57-
to store credientials via the Keychain on macOS, you will need to
58-
[sign your app](https://developer.apple.com/support/code-signing/).
57+
to store credentials via the Keychain on macOS, you will need to add
58+
`$(AppIdentifierPrefix)$(CFBundleIdentifier)` to its keychain access group.
5959

6060
## Using the Google Sign-In Button
6161

0 commit comments

Comments
 (0)