GitHub - google/GTMAppAuth: Apple platforms SDK for using AppAuth with Google libraries. (original) (raw)
GTMAppAuth for Apple Platforms
GTMAppAuth enables you to use AppAuthwith theGoogle Toolbox for Mac - Session FetcherandGoogle APIs Client Library for Objective-C For RESTlibraries on iOS, macOS, tvOS, and watchOS by providing an implementation ofGTMFetcherAuthorizationProtocolfor authorizing requests with AppAuth.
GTMAppAuth is an alternative authorizer to GTMOAuth2. The key differentiator is the use of the user's default browser for the authorization, which is more secure, more usable (the user's session can be reused) and follows modern OAuth best practices for native apps. Compatibility methods for GTMOAuth2 are offered allowing you to migrate from GTMOAuth2 to GTMAppAuth preserving previously serialized authorizations (so users shouldn't need to re-authenticate).
Setup
If you use CocoaPods, simply add:
To your Podfile and run pod install.
Usage
Configuration
To configure GTMAppAuth with the OAuth endpoints for Google, you can use the convenience method:
OIDServiceConfiguration *configuration = [GTMAuthSession configurationForGoogle];
Alternatively, you can configure GTMAppAuth by specifying the endpoints directly:
NSURL *authorizationEndpoint = [NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"]; NSURL *tokenEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"];
OIDServiceConfiguration *configuration = [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpoint tokenEndpoint:tokenEndpoint];
// perform the auth request...
Or through discovery:
NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"];
[OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { if (!configuration) { NSLog(@"Error retrieving discovery document: %@", [error localizedDescription]); return; }
// perform the auth request... }];
Authorizing
First, you need to have a way for your UIApplicationDelegate to continue the authorization flow session from the incoming redirect URI. Typically you could store the in-progress OIDAuthorizationFlowSession instance in a property:
// property of the app's UIApplicationDelegate @property(nonatomic, nullable) id currentAuthorizationFlow;
And in a location accessible by all controllers that need authorization, a property to store the authorization state:
// property of the containing class @property(nonatomic, nullable) GTMAuthSession *authSession;
Then, initiate the authorization request. By using theauthStateByPresentingAuthorizationRequest method, the OAuth token exchange will be performed automatically, and everything will be protected with PKCE (if the server supports it).
// builds authentication request OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration clientId:kClientID clientSecret:kClientSecret scopes:@[OIDScopeOpenID, OIDScopeProfile] redirectURL:redirectURI responseType:OIDResponseTypeCode additionalParameters:nil]; // performs authentication request self.appDelegate.currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request callback:^(OIDAuthState *_Nullable authState, NSError *_Nullable error) { if (authState) { // Creates a GTMAuthSession from the OIDAuthState. self.authSession = [[GTMAuthSession alloc] initWithAuthState:authState]; NSLog(@"Got authorization tokens. Access token: %@", authState.lastTokenResponse.accessToken); } else { NSLog(@"Authorization error: %@", [error localizedDescription]); self.authSession = nil; } }];
Handling the Redirect
The authorization response URL is returned to the app via the platform-specific application delegate method, so you need to pipe this through to the current authorization session (created in the previous session).
macOS Custom URI Scheme Redirect Example
(void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Other app initialization code ...
// Register for GetURL events. NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; [appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
}
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; NSURL *URL = [NSURL URLWithString:URLString]; [_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:URL]; }
iOS Custom URI Scheme Redirect Example
(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *, id> *)options { // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { _currentAuthorizationFlow = nil; return YES; }
// Your additional URL handling (if any) goes here.
return NO;
}
Making API Calls
The goal of GTMAppAuth is to enable you to authorize HTTP requests with fresh tokens following the Session Fetcher pattern, which you can do like so:
// Creates a GTMSessionFetcherService with the authorization. // Normally you would save this service object and re-use it for all REST API calls. GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init]; fetcherService.authorizer = self.authSession;
// Creates a fetcher for the API call. NSURL *userinfoEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v3/userinfo"]; GTMSessionFetcher *fetcher = [fetcherService fetcherWithURL:userinfoEndpoint]; [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { // Checks for an error. if (error) { // OIDOAuthTokenErrorDomain indicates an issue with the authorization. if ([error.domain isEqual:OIDOAuthTokenErrorDomain]) { self.authSession = nil; NSLog(@"Authorization error during token refresh, clearing state. %@", error); // Other errors are assumed transient. } else { NSLog(@"Transient error during token refresh. %@", error); } return; }
// Parses the JSON response. NSError *jsonError = nil; id jsonDictionaryOrArray = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
// JSON error. if (jsonError) { NSLog(@"JSON decoding error %@", jsonError); return; }
// Success response! NSLog(@"Success: %@", jsonDictionaryOrArray); }];
Saving to the Keychain
You can easily save GTMAuthSession instances to the Keychain using the GTMKeychainStore class.
// Create a GIDKeychainStore instance, intializing it with the Keychain item name kKeychainItemName
// which will be used when saving, retrieving, and removing GTMAuthSession instances.
GIDKeychainStore *keychainStore = [[GIDKeychainStore alloc] initWithItemName:kKeychainItemName];
NSError *error;
// Save to the Keychain [keychainStore saveAuthSession:self.authSession error:&error]; if (error) { // Handle error }
// Retrieve from the Keychain self.authSession = [keychainStore retrieveAuthSessionWithError:&error]; if (error) { // Handle error }
// Remove from the Keychain [keychainStore removeAuthSessionWithError:&error]; if (error) { // Handle error }
Keychain Storage
With GTMKeychainStore, by default, GTMAuthSession instances are stored using Keychain items of thekSecClassGenericPasswordclass with a kSecAttrAccountvalue of "OAuth" and a developer supplied value for kSecAttrService. For this use of generic password items, the combination of account and service values acts as theprimary keyof the Keychain items. ThekSecAttrAccessiblekey is set tokSecAttrAccessibleAfterFirstUnlockThisDeviceOnlyin order to allow background access after initial device unlock following a restart. A keyed archiverepresentation of the relevant GTMAuthSession instance is supplied as the value forkSecValueDataand this is encrypted and stored byKeychain Services.
macOS
For macOS, two Keychain storage options are available: the traditional file-based Keychain storage which uses access control lists and the more modern data protection Keychain storagewhich uses Keychain access control groups. By default, GTMAppAuth follows Apple's adviceto use the data protection Keychain storage on macOS. You may opt into using file-based Keychain storage by including the GTMKeychainAttribute.useFileBasedKeychain attribute in thekeychainAttributes parameter of initWithItemName:keychainAttributes: when initializingGTMKeychainStore. Note that Keychain items stored via one storage type will not be available via the other. macOS apps that use the data protection Keychain without specifying an access group will need to be include $(AppIdentifierPrefix)$(CFBundleIdentifier) as a keychain access group in their apps entitlements for Keychain operations to succeed.
Migrating to data protected keychain storage
Prior to version 5.0.0 GTMAppAuth defaulted to using the file-based Keychain. It is recommended that developers migrate to the data protection Keychain.
Example migration:
// Create a keychain store with data protection. GTMKeychainStore dataProtectionKeychainStore = [[GTMKeychainStore alloc] initWithItemName:kKeychainName];
// Attempt to retrieve from Keychain. NSError *error; GTMAuthSession *authSession = [keychainStore retrieveAuthSessionWithError:&error];
// If no authSession found, try to retrieve from file-based Keychain. if (!authSession) { GTMKeychainAttribute *fileBased = [GTMKeychainAttribute useFileBasedKeychain]; NSSet *attributes = [NSSet setWithArray:@[fileBased]]; GTMKeychainStore fileBasedKeychainStore = [[GTMKeychainStore alloc] initWithItemName:kExampleAuthorizerKey keychainAttributes:attributes]; authSession =[keychainStore retrieveAuthSessionWithError:&error];
if (authSession) { // Remove previously stored session from file-based Keychain. [fileBasedKeychainStore removeAuthSessionWithError:&error]; // Save to the data protected Keychain. [dataProtectionKeychainStore saveAuthSession:authSession error:&error]; } }
Implementing Your Own Storage
If you'd like to use a backing store other than the Keychain to save your GTMAuthSessioninstances, you can create your own GTMAuthSessionStore conformance. Use GTMKeychainStore as an example of how to do this.
GTMOAuth2 Compatibility
To assist the migration from GTMOAuth2 to GTMAppAuth, GTMOAuth2-compatible Keychain methods are provided in GTMKeychainStore.
GTMKeychainStore keychainStore = [[GTMKeychainStore alloc] initWithItemName:kKeychainItemName];
// Retrieve from the Keychain NSError *error; GTMAuthSession *authSession = [keychainStore retrieveAuthSessionForGoogleInGTMOAuth2FormatWithClientID:clientID clientSecret:clientSecret error:&error];
// Remove from the Keychain [keychainStore removeAuthSessionWithError:&error];
You can also save to GTMOAuth2 format, though this is discouraged (you should save in GTMAppAuth format as described above).
// Save to the Keychain [keychainStore saveWithGTMOAuth2FormatForAuthSession:authSession error:&error];
Included Samples
Try out one of the included sample apps under Examples. In the apps folder run pod install, then open the resulting xcworkspace file.
Be sure to follow the instructions inExample-iOS/README.md orExample-macOS/README.md to configure your own OAuth client ID for use with the example.
Differences with GTMOAuth2
Authorization Method
GTMAppAuth uses the browser to present the authorization request, while GTMOAuth2 uses an embedded web-view. Migrating to GTMAppAuth will require you to change how you authorize the user. Follow the instructions above to get the authorization. You can then create a GTMAuthSession object with itsinitWithAuthState: initializer. Once you have a GTMAuthSession you can continue to make REST calls as before.
Error Handling
GTMAppAuth's error handling is also different. There are no notifications, instead you need to inspect NSError in the callback. If the error domain isOIDOAuthTokenErrorDomain, it indicates an authorization error, you should clear your authorization state and consider prompting the user to authorize again. Other errors are generally considered transient, meaning that you should retry the request after a delay.
Serialization
The serialization format is different between GTMOAuth2 and GTMAppAuth, though we have methods to help you migrate from one to the other without losing any data.
Migrating from GTMOAuth2
OAuth Client Registration
Typically, GTMOAuth2 clients are registered with Google as type "Other". Instead, Apple clients should be registered with the type "iOS".
If you're migrating an Apple client in the same project as your existing client,register a new iOS clientto be used with GTMAppAuth.
Changing your Authorization Flows
Both GTMOAuth2 and GTMAppAuth support the GTMFetcherAuthorizationProtocolallowing you to use the authorization with the session fetcher. Where you previously had a property like GTMOAuth2Authentication *authorization change the type to reference the protocol instead, i.e.:id<GTMFetcherAuthorizationProtocol> authorization. This allows you to switch the authorization implementation under the hood to GTMAppAuth.
Then, follow the instructions above to replace authorization request (where you ask the user to grant access) with the GTMAppAuth approach. If you created a new OAuth client, use that for these requests.
Serialization & Migrating Existing Grants
GTMAppAuth has a new data format and APIs for serialization. Unlike GTMOAuth2, GTMAppAuth serializes the configuration and history of the authorization, including the client id, and a record of the authorization request that resulted in the authorization grant.
The client ID used for GTMAppAuth is different to the one used for GTMOAuth2. In order to keep track of the different client ids used for new and old grants, it's recommended to migrate to the new serialization format, which will store that for you.GTMOAuth2-compatible serialization is also offered, but not fully supported.
Change how you serialize your authorization object by using GTMAuthSession and GTMKeychainStore as follows:
// Create an auth session from AppAuth's auth state object GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:authState];
// Create a keychain store GTMKeychainStore keychainStore = [[GTMKeychainStore alloc] initWithItemName:kNewKeychainName];
// Serialize to Keychain NSError *error; [keychainStore saveAuthSession:authSession error:&error];
Be sure to use a new name for the keychain. Don't reuse your old one!
For deserializing, we can preserve all existing grants (so users who authorized your app in GTMOAuth2 don't have to authorize it again). Remember that when deserializing the old data you need to use your old keychain name, and the old client id and client secret (if those changed), and that when serializing to the new format, use the new keychain name. Once again, pay particular care to use the old details when deserializing the GTMOAuth2 keychain, and the new details for all other GTMAppAuth calls.
Keychain migration example:
// Create a keychain store GTMKeychainStore keychainStore = [[GTMKeychainStore alloc] initWithItemName:kNewKeychainName];
// Attempt to deserialize from Keychain in GTMAppAuth format. NSError *error; GTMAuthSesion *authSession = [keychainStore retrieveAuthSessionWithError:&error];
// If no data found in the new format, try to deserialize data from GTMOAuth2 if (!authSession) { // Tries to load the data serialized by GTMOAuth2 using old keychain name. // If you created a new client id, be sure to use the previous client id and secret here. GTMKeychainStore oldKeychainStore = [[GTMKeychainStore alloc] initWithItemName:kPreviousKeychainName]; authSession = [oldKeychainStore retrieveAuthSessionInGTMOAuth2FormatWithClientID:kPreviousClientID clientSecret:kPreviousClientSecret error:&error]; if (authSession) { // Remove previously stored GTMOAuth2-formatted data. [oldKeychainStore removeAuthSessionWithError:&error]; // Serialize to Keychain in GTMAppAuth format. [keychainStore saveAuthSession:authSession error:&error]; } }