-
Notifications
You must be signed in to change notification settings - Fork 627
Implementing OAuth 2.0
NEW/UPDATED Easy Option: Using GTMAppAuth
Google deprecated GTMOAuth2, so you shouldn't add it to projects.
Here are the high-levels steps:
- Integrate GTMAppAuth into your project using Google's instructions (ie. cocoapod)
- Create a Google API app for your app if you don't already have one: https://console.developers.google.com/apis/credentials
- Create a credential set for your Google API App: you need an "OAuth 2.0 client ID" type credential
- Update your code as below, including Info.plist change to add the URL type
In AppDelegate.m:
(in the openURL app delegate method your app is using)
....
} else if ([url.absoluteString hasPrefix:@"com.googleusercontent.apps.MADE_IN_GOOGLE_APP_CONSOLE"]) {
if ([[EmailHelper singleton].currentAuthorizationFlow resumeAuthorizationFlowWithURL:url]) {
[EmailHelper singleton].currentAuthorizationFlow = nil;
return true;
}
}
....
In your Info.plist:
- Add the URL type ie. com.googleusercontent.apps.MADE_IN_GOOGLE_APP_CONSOLE
Here's a singleton class to do the work. This is not well-tested code, use at your own risk:
//
// EmailHelper.h
//
#import <Foundation/Foundation.h>
#import <MailCore/MailCore.h>
#import <AppAuth/AppAuth.h>
#import <GTMAppAuth/GTMAppAuth.h>
@interface EmailHelper : NSObject
+ (EmailHelper *)singleton;
@property(nonatomic, strong, nullable) id<OIDAuthorizationFlowSession> currentAuthorizationFlow;
@property(nonatomic, nullable) GTMAppAuthFetcherAuthorization *authorization;
- (void)doEmailLoginIfRequiredOnVC:(UIViewController*)vc completionBlock:(dispatch_block_t)completionBlock;
@end
//
// EmailHelper.m
//
#import "EmailHelper.h"
#import <GTMSessionFetcher/GTMSessionFetcherService.h>
#import <GTMSessionFetcher/GTMSessionFetcher.h>
/*! @brief The OIDC issuer from which the configuration will be discovered.
*/
static NSString *const kIssuer = @"https://accounts.google.com";
/*! @brief The OAuth client ID.
@discussion For Google, register your client at
https://console.developers.google.com/apis/credentials?project=_
The client should be registered with the "iOS" type.
*/
static NSString *const kClientID = @"MADE_IN_GOOGLE_API_CONSOLE.apps.googleusercontent.com";
/*! @brief The OAuth redirect URI for the client @c kClientID.
@discussion With Google, the scheme of the redirect URI is the reverse DNS notation of the
client ID. This scheme must be registered as a scheme in the project's Info
property list ("CFBundleURLTypes" plist key). Any path component will work, we use
'oauthredirect' here to help disambiguate from any other use of this scheme.
*/
static NSString *const kRedirectURI =
@"com.googleusercontent.apps.MADE_IN_GOOGLE_API_CONSOLE:/oauthredirect";
/*! @brief @c NSCoding key for the authState property. You don't need to change this value.
*/
static NSString *const kExampleAuthorizerKey = @"googleOAuthCodingKey";
@implementation EmailHelper
static dispatch_once_t pred;
static EmailHelper *shared = nil;
+ (EmailHelper *)singleton {
dispatch_once(&pred, ^{ shared = [[EmailHelper alloc] init]; });
return shared;
}
- (instancetype)init {
if (self = [super init]) {
[self loadState];
}
return self;
}
#pragma mark -
// CALL THIS TO START
- (void)doEmailLoginIfRequiredOnVC:(UIViewController*)vc completionBlock:(dispatch_block_t)completionBlock {
// Optional: if no internet connectivity, do nothing
if (your reachability code says there is internet connectivity) {
dispatch_async(dispatch_get_main_queue(), ^{
// first see if we already have authorization
[self checkIfAuthorizationIsValid:^(BOOL authorized) {
NSAssert([NSThread currentThread].isMainThread, @"ERROR MAIN THREAD NEEDED");
if (authorized) {
if (completionBlock)
completionBlock();
} else {
[self doInitialAuthorizationWithVC:vc completionBlock:completionBlock];
}
}];
});
}];
}
/*! @brief Saves the @c GTMAppAuthFetcherAuthorization to @c NSUSerDefaults.
*/
- (void)saveState {
if (_authorization.canAuthorize) {
[GTMAppAuthFetcherAuthorization saveAuthorization:_authorization toKeychainForName:kExampleAuthorizerKey];
} else {
NSLog(@"EmailHelper: WARNING, attempt to save a google authorization which cannot authorize, discarding");
[GTMAppAuthFetcherAuthorization removeAuthorizationFromKeychainForName:kExampleAuthorizerKey];
}
}
/*! @brief Loads the @c GTMAppAuthFetcherAuthorization from @c NSUSerDefaults.
*/
- (void)loadState {
GTMAppAuthFetcherAuthorization* authorization =
[GTMAppAuthFetcherAuthorization authorizationFromKeychainForName:kExampleAuthorizerKey];
if (authorization.canAuthorize) {
self.authorization = authorization;
} else {
NSLog(@"EmailHelper: WARNING, loaded google authorization cannot authorize, discarding");
[GTMAppAuthFetcherAuthorization removeAuthorizationFromKeychainForName:kExampleAuthorizerKey];
}
}
- (void)doInitialAuthorizationWithVC:(UIViewController*)vc completionBlock:(dispatch_block_t)completionBlock {
NSURL *issuer = [NSURL URLWithString:kIssuer];
NSURL *redirectURI = [NSURL URLWithString:kRedirectURI];
NSLog(@"EmailHelper: Fetching configuration for issuer: %@", issuer);
// discovers endpoints
[OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) {
if (!configuration) {
NSLog(@"EmailHelper: Error retrieving discovery document: %@", [error localizedDescription]);
self.authorization = nil;
return;
}
NSLog(@"EmailHelper: Got configuration: %@", configuration);
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientID
scopes:@[OIDScopeOpenID, OIDScopeProfile, @"https://mail.google.com/"]
redirectURL:redirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
NSLog(@"EmailHelper: Initiating authorization request with scope: %@", request.scope);
self.currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request presentingViewController:vc callback:^(OIDAuthState *_Nullable authState, NSError *_Nullable error) {
if (authState) {
self.authorization = [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
NSLog(@"EmailHelper: Got authorization tokens. Access token: %@", authState.lastTokenResponse.accessToken);
[self saveState];
} else {
self.authorization = nil;
NSLog(@"EmailHelper: Authorization error: %@", [error localizedDescription]);
}
if (completionBlock)
dispatch_async(dispatch_get_main_queue(), completionBlock);
}];
}];
}
// Performs a UserInfo request to the account to see if the token works
- (void)checkIfAuthorizationIsValid:(void (^)(BOOL authorized))completionBlock {
NSLog(@"EmailHelper: Performing userinfo request");
// 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.authorization;
// 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]) {
[GTMAppAuthFetcherAuthorization removeAuthorizationFromKeychainForName:kExampleAuthorizerKey];
self.authorization = nil;
NSLog(@"EmailHelper: Authorization error during token refresh, cleared state. %@", error);
if (completionBlock)
completionBlock(NO);
} else {
// Other errors are assumed transient.
NSLog(@"EmailHelper: Transient error during token refresh. %@", error);
if (completionBlock)
completionBlock(NO);
}
return;
}
NSLog(@"EmailHelper: authorization is valid");
if (completionBlock)
completionBlock(YES);
}];
}
@end
Finally, here's the code to use the token, when you are using MailCore to send email (should work for IMAP too):
EmailHelper* eh = [EmailHelper singleton];
smtpSession.username = eh.smtpUsername;
smtpSession.hostname = eh.smtpHostname;
smtpSession.port = (int)eh.smtpPort;
if (eh.authorization.canAuthorize) {
smtpSession.authType = MCOAuthTypeXOAuth2;
smtpSession.OAuth2Token = eh.authorization.authState.lastTokenResponse.accessToken;
} else {
smtpSession.password = eh.smtpPassword;
}
Add GTMOAuth2 to your project.
Only the following files are required:
- GTMHTTPFetcher.[hm]
- GTMHTTPFetchHistory.[hm]
- GTMOAuth2Authentication.[hm]
- GTMOAuth2SignIn.[hm]
- GTMOAuth2WindowController.[hm]
- GTMOAuth2Window.xib
Make sure to comply with the license.
Here's an example of how to use it
- (void) startOAuth2
{
GTMOAuth2Authentication * auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:KEYCHAIN_ITEM_NAME
clientID:CLIENT_ID
clientSecret:CLIENT_SECRET];
if ([auth refreshToken] == nil) {
GTMOAuth2WindowController *windowController =
[[GTMOAuth2WindowController alloc] initWithScope:@"https://mail.google.com/"
clientID:CLIENT_ID
clientSecret:CLIENT_SECRET
keychainItemName:KEYCHAIN_ITEM_NAME
resourceBundle:[NSBundle bundleForClass:[GTMOAuth2WindowController class]]];
[windowController autorelease];
[windowController signInSheetModalForWindow:nil
delegate:self
finishedSelector:@selector(windowController:finishedWithAuth:error:)];
}
else {
[auth beginTokenFetchWithDelegate:self
didFinishSelector:@selector(auth:finishedRefreshWithFetcher:error:)];
}
}
- (void)auth:(GTMOAuth2Authentication *)auth
finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher
error:(NSError *)error {
[self windowController:nil finishedWithAuth:auth error:error];
}
- (void)windowController:(GTMOAuth2WindowController *)viewController
finishedWithAuth:(GTMOAuth2Authentication *)auth
error:(NSError *)error
{
if (error != nil) {
// Authentication failed
return
}
NSString * email = [auth userEmail];
NSString * accessToken = [auth accessToken];
MCOIMAPSession * imapSession = [[MCOIMAPSession alloc] init];
[imapSession setAuthType:MCOAuthTypeXOAuth2];
[imapSession setOAuth2Token:accessToken];
[imapSession setUsername:email];
// Use a different hostname if you oauth authenticate against a different provider
[imapSession setHostname:@"imap.gmail.com"];
[imapSession setPort:993];
MCOSMTPSession * smtpSession = [[MCOSMTPSession alloc] init];
[smtpSession setAuthType:MCOAuthTypeXOAuth2];
[smtpSession setOAuth2Token:accessToken];
[smtpSession setUsername:email];
}
On iOS substitute GTMOAuth2WindowController
for GTMOAuth2ViewControllerTouch
. You will also need to initialize GTMOAuth2ViewControllerTouch
slightly differently:
[[GTMOAuth2ViewControllerTouch alloc] initWithScope:@"https://mail.google.com/"
clientID:CLIENT_ID
clientSecret:CLIENT_SECRET
keychainItemName:KEYCHAIN_NAME
delegate:self
finishedSelector:@selector(windowController:finishedWithAuth:error:)];
Finally, on iOS you need to dismiss GTMOAuth2ViewControllerTouch
in your finished selector.
To gather a deeper understanding of the OAuth2 authentication process, refer to: Gmail XOAUTH2 API
For a quick start you may follow this brief set of steps:
-
Set up a profile for your app in the Google API Console
-
With your recently obtained
client_id
andsecret
load the following URL (everything goes in a single line):
https://accounts.google.com/o/oauth2/auth?client_id=[YOUR_CLIENT_ID]&
redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&
response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&
&access_type=offline
-
The user most follow instructions to authorize application access to Gmail.
-
After the user hits the "Accept" button it will be redirected to another page where the access token will be issued.
-
Now from the app we need and authorization token, to get one we issue a POST request the following URL:
https://accounts.google.com/o/oauth2/token
using these parameters:
-
client_id
: This is the client id we got from step 1 -
client_secret
: Client secret as we got it from step 1 -
code
: This is the code we received in step 4 -
redirect_uri
: This is a redirect URI where the access token will be sent, for non-web applications this is usuallyurn:ietf:wg:oauth:2.0:oob
(as we got from step 1) -
grant_type
: Always use the authorization_code parameter to retrieve an access and refresh tokens
- After step 5 completes we receive a JSON object similar to:
{
"access_token":"1/fFAGRNJru1FTz70BzhT3Zg",
"refresh_token":"1/fFAGRNJrufoiWEGIWEFJFJF",
"expires_in":3920,
"token_type":"Bearer"
}
The above output gives us the access_token, now we need to also retrieve the user's e-mail,
to do that we need to perform an HTTP GET request to Google's UserInfo API using this URL: https://www.googleapis.com/oauth2/v1/userinfo?access_token=[YOUR_ACCESS_TOKEN]
this will return the following JSON output:
{
"id": "00000000000002222220000000",
"email": "email@example.com",
"verified_email": true
}
- Use the "email field of the UserInfo request and "access_token" of the token request and call the following APIs:
MCOIMAPSession * imapSession = [[MCOIMAPSession alloc] init];
[imapSession setAuthType:MCOAuthTypeXOAuth2];
[imapSession setOAuth2Token:accessToken];
[imapSession setUsername:email];
MCOSMTPSession * smtpSession = [[MCOSMTPSession alloc] init];
[smtpSession setAuthType:MCOAuthTypeXOAuth2];
[smtpSession setOAuth2Token:accessToken];
[smtpSession setUsername:email];
Mac and iOS examples implements OAuth 2.0 access.