Implementing OAuth 2.0

Tim Carr edited this page Jan 16, 2017 · 14 revisions

NEW/UPDATED Easy Option: Using GTMAppAuth

Google deprecated GTMOAuth2, so you shouldn't add it to projects.

Here are the high-levels steps:

  1. Integrate GTMAppAuth into your project using Google's instructions (ie. cocoapod)
  2. Create a Google API app for your app if you don't already have one: https://console.developers.google.com/apis/credentials
  3. Create a credential set for your Google API App: you need an "OAuth 2.0 client ID" type credential
  4. 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;
    }

OLD/DEPRECATED Easy Option: Using GTMOAuth2

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.

Implement manually

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:

  1. Set up a profile for your app in the Google API Console

  2. With your recently obtained client_id and secret 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
  1. The user most follow instructions to authorize application access to Gmail.

  2. After the user hits the "Accept" button it will be redirected to another page where the access token will be issued.

  3. 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 usually urn: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
  1. 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
}
  1. 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];

Examples

Mac and iOS examples implements OAuth 2.0 access.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.