This tutorial will dive into implementing Google Services using the OAuth 2.0 protocol. Read on!
OAuth 2.0 Overview
The OAuth 2.0 protocol provides a simple and secure standard that allows third-party applications to access major service providers like Facebook, G+, and Twitter without compromising user passwords. The whole idea revolves around the existence of an access token, something like a unique key that can identify a user in place of a password. Access tokens are obtained by third-party applications after the user successfully authenticates with a web service. The whole process, known as authorization flow, begins when a user enters his credentials into a login window and finishes when the access token is acquired. The access token is usually refreshed from time to time. With this token, there is no need for any of the user’s personal data or password to be transmitted over the web each time that a client application asks for access on behalf of the user.
If you are not familiar with the OAuth 2.0 protocol you should do some background reading now before continuing this tutorial. Specifically, review the following links:
In general, the OAuth 2.0 authorization flow adheres to the following pattern:
- Let users connect to their online account.
- Get an authorization code (i.e. authorization token).
- Exchange the authorization code for an access token and a refresh token.
- Use the access token to interact with a web service or an API.
- Use the refresh token to update the access token when needed.
Google and OAuth 2.0
Google is one of the many third-party web service providers that have adopted the OAuth 2.0 protocol. It provides many APIs for accessing almost all of its services (like Calendar, Blogger, etc.) through client applications and provides guidance on how to implement the authorization flow with various programming languages and platforms. Every application that needs to use any of the Google web services must first register with the Google Developer Console, an administration panel where all the client applications developed by a user are managed. When registering an app in the Console, a client id and a client secret are created specifically for this app. These values, along with some others, are used by OAuth to authorize an app and to obtain the access token. After having registered an application, there are a number of available services that can be integrated into projects. These services are accessed through APIs that are provided for each one. Some of them are free of charge while others require a fee before Google will let you use them beyond a courtesy/trial usage limit.
From this point forward, I'll assume that you are familiar with OAuth 2.0 principles, so it’s time to visit the Using OAuth 2.0 for Installed Applications documentation from Google. This documentation presents the authorization flow supported by Google for client applications implemented for mobile or desktop platforms and is going to be our guide on the project of this tutorial. Also, I would recommend that you surf around a little using the menu options at the top-left side of the window and the links inside the text in order to gain a deeper understanding on Google services and the way the OAuth 2.0 protocol works.
Project Overview
This project will teach you how to create a class that will contain the OAuth 2.0 client-side implementation for accessing Google's API and services. This class will be part of a simple demo application. The GUI will be really simple, as we only need to test the class, so we are going to spend a great deal of time actually coding. In order to test the OAuth protocol implementation, we will ask for the user profile information from Google, which will be displayed in our app. Also, in order for our work to be as complete as possible, we will add extra checks and error handling messages. After this class is ready, it can be used in real projects for accessing Google services. Of course, it can be improved or be modified by anyone at any time.
Before we begin writing one line of code, we need to register the demo application in the Google Developers Console. To do this, we'll need the app’s name. For the sake of our project, I'm using the name GoogleOAuthDemo. If you prefer some other name, make sure to use it instead of mine in the steps that follow.
For now, here is a screenshot of the final product:
1. Register the iOS Application
Step 1
Our first step is to register the iOS application in the Google Developer Console because we need to acquire a Client ID and a Client Secret value pair. So, inside a new browser tab/window, visit the Google Developers website. At the top-right side of the window, a Sign In button exists. Click it and the next window will show up.
If you have an account in any Google service (like gmail, Google+, Drive, etc.) then you can login using the credentials from this account. If you don’t have one already, or you just want to create a new one, now is the time to do it, simply by clicking on the Sign Up button.
In case you are making a new account, enter any required information, create it, and login to the Google Developers website.
Step 2
Once you get connected with the Google Developers account, you might become quite curious about all the stuff this page contains, so go ahead and take a look if you wish. Surely you’ll find some pretty interesting topics. Scroll to the bottom side of the page, until you see the Developer Tools area. Click on the API Console link.
Step 3
When you create an application in the Developers Console, the information regarding it is grouped all together and it’s called a project. Through it, you can manage all the APIs, users, authorization, and more stuff that you use in your applications. I would advise to look at the Help section (at the top-right side of the webpage) if you want to find detailed information about the console.
Let’s get back on track. When you first look at this webpage, you notice a drop-down menu at the top-left side of the webpage, named API Project. If you expand it, you’ll find some extra options, where by using them you can create new projects, delete them, rename them, and more. We don’t need anything from it, so if you have expanded it, click on it to collapse it. Beneath the drop-down menu, there are four options:
- Overview
- Services
- Team
- API Access
If you click on the Services option, you’ll see all the services that Google offers with APIs. Next to each service there are some notes indicating which of them require a fee. To use any of them in an application, first you must register your application, then implement the OAuth protocol and finally ask for the appropriate APIs.
Click on the API Access option now. This is the place where we’ll register our new application. Click on the big blue button with the title "Create an OAuth 2.0 client ID...".
In the popup window, add as the Product Name the Google OAuth 2.0 iOS Implementation Demo value and click on Next.
In the next step, set the following:
- Application Type: Installed application
- Installed Application Type: iOS
- Bundle ID: com.yourdomain.googleoauthdemo (for example, com.gabrieltheodoropoulos.googleoauthdemo)
Click on the Create client ID button now and you are ready. As you can see at the next screenshot, you can easily find the client ID and the client secret values.
You can find out for yourself that you can edit all the information we just entered using the provided links and buttons in the webpage. The data we are going to use in our demo application is:
- client ID
- client secret
- Redirect URI
At this point, the first part of our mission is over. Our app has been registered in the Google Developers Console and we have all the necessary information we want. The hard part though is coming next, where we are going to actually implement the OAuth 2.0 protocol in code.
2. Create the Demo Project
Time to begin building our app. Launch Xcode and create a new project. At the first step, select the Single View Application option from the provided templates.
Next, specify the GoogleOAuthDemo in the Product Name field of the project's options window.
Lastly, select a place to save your project and click on the Create button.
3. Create the Interface
Step 1
Click on the ViewController.xib
file to launch Interface Builder. Drag-and-drop the next subviews on the default view:
- A Table View.
- A Toolbar.
Also, apart from the default Bar Button Item that the toolbar contains, add:
- A Flexible Space Bar Button Item and
- A Bar Button Item
Make sure that the one button is at the left side of toolbar, while the second is at the right side of it. The flexible space should be between them.
Step 2
Let's now do a couple of settings.
- Select the View and set its Size to None so it works on 3.5" iPhone screens as well.
- Open the Utilities pane.
- Show the Attributes Inspector tab.
- In the Simulated Metrics section set the Size to None.
- Set the frame of the Table View to: X: 0, Y: 0, Width: 320, Height: 416.
- Set the Title of the left Bar Button Item to My Profile.
- Set the Title of the right Bar Button Item to Revoke access.
That's all we want from our interface and this is how it should look now:
4. Setup IBOutlets & IBActions
Step 1
Our interface is prepared, but we need an IBOutlet property to connect the Table View. We also need two IBAction methods if we want our buttons to work. So, while we are in Interface Builder, Click on the middle button of the Editor section on the Xcode toolbar to make the Assistant Editor appear.
Step 2
With the goal of creating the necessary IBOutlet connection, open the Document Outline and perform the following steps:
- Control + Click or Right + Click on the Table View.
- On the popup menu, click on the New Referencing Outlet.
- Drag-and-drop into the Assistant Editor window, just like you see in the next image.
Give a name for the Table View and click on the Connect button. I simply named it table.
Step 3
Let's create the two IBAction methods now. Apply the following procedure in both the Bar Button Items.
- Control + Click or Right + Click on the left Bar Button item.
- Click on the Selector option in the Sent Actions section of the popup menu.
- Drag-and-drop into the Assistant Editor window.
Set the showProfile as the name for the method and create it.
Give the name revokeAccess to the next IBAction method when you create it. Your ViewController.h
file after the IBOutlet property and the IBAction methods should look like this:
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (weak, nonatomic) IBOutlet UITableView *table; - (IBAction)showProfile:(id)sender; - (IBAction)revokeAccess:(id)sender; @end
5. Create the OAuth 2.0 Protocol Implementation File
Step 1
Even though we added a Table View in the project, we won't implement its delegate methods for the time being. This is something that we are going to do when the OAuth 2.0 implementation is over. What we are going to do in this step is to create a new file where we will build the OAuth protocol.
On the Project Navigator, do the following:
- Control + Click or Right + Click on the GoogleOAuthDemo group.
- Select the New File... option from the menu.
Step 2
In the window that appears, select the Objective-C class option as the template for the new file:
Next, at the Subclass of field, select the UIWebView option. Our new class is going to be a subclass of the UIWebView
class, because we want to show a web view to the users for letting them sign in with their Google account and grant them access to our application. At the Class field, add the GoogleOAuth value:
On the next step, just click on the Create button to get the new files made.
6. The OAuth 2.0 Flow
In this part, I will shortly describe how the OAuth 2.0 flow will be implemented and how all the necessary checking for the existence of the tokens and the validity of the access token is going to take place. First of all, after the access token has been obtained, it should be stored permanently along with any other info that comes with it. It's up to the developer to choose the way that all this information will be saved. You may store everything into a database, save it to the user defaults, write everything to files, or use more secure methods in order to keep things safe. In this tutorial, I prefer to save all in plain files, inside the Documents directory of the app.
We are going to create two different files. The access token and any other information that comes with it from Google is going to be stored into the first one, except for the refresh token. The refresh token is going to be saved in another file, because we need to keep it for a longer period than everything else and use it when the access token has expired. Every time that the access token is being refreshed, the contents of the first file are overwritten. Besides that, the refresh token itself is not refreshed that often.
So, having said all that, here is a preview of the protocol's flow and how it is going to be implemented.
Even though an image equals a thousand words, I would better describe the whole procedure to make it more clear. To elaborate on the authorization process:
- In case the file that contains the access token information does not exist...
- The URL that we will require an access token from is being formed. All the necessary parameters to authenticate the user are provided. The client ID and client secret values are needed here.
- Next, we display a web view inside the app (we don't call Safari) to let the user sign in to Google. After a successful login, a message from Google with the permissions we ask for is displayed to the user, who should consent if he/she wants to go any further.
- An authorization code comes back from Google, which will be exchanged with the access token right after.
- A new URL is formed with new parameters. The aim is to exchange the authorization code with the access token.
- If the access token is successfully obtained, the caller class is informed via a delegate method. Otherwise, using another delegate method we let the caller class know that the access token was not obtained, so further actions should be taken as needed.
- The access token info is loaded from the file.
- The access token is checked to see whether it has expired or not.
- If the access token is valid, then the caller class simply gets notified through a delegate method that is valid and these steps end here. Otherwise, a refresh is required (next steps).
- If the file that contains the refresh token is not found in the Documents directory, then the whole OAuth flow starts over. If it exists, then the refresh token is loaded.
- A new access token is requested. If the refresh token is not valid, a kind of error message will be returned from Google and the user is guided to the beginning again. Otherwise the access token is obtained.
So, let's see everything step-by-step.
7. Start building
Step 1
At first, open the GoogleOAuth.h
file and adopt the next two protocols, simply by declaring them in the @interface
header line:
@interface OAuth : UIWebView <UIWebViewDelegate, NSURLConnectionDataDelegate> @end
I am pretty sure that it is quite obvious what these delegates are for.
Step 2
Now let's create an enum
type, with the acceptable HTTP method values required for various API calls. For the time being, we'll create the enum type, but we'll use it when we'll implement the API call method.
So, right above the @interface
line at the GoogleOAuth.h
file, add the following:
typedef enum { httpMethod_GET, httpMethod_POST, httpMethod_DELETE, httpMethod_PUT } HTTP_Method;
Step 3
It's always useful to use constants for things that we don't want to make any mistakes about, and that is true for the URL endpoints we want to access during the OAuth flow. For that reason, open the GoogleOAuth.m
file and declare the following two constants at the top of the file:
#define authorizationTokenEndpoint @"https://accounts.google.com/o/oauth2/auth" #define accessTokenEndpoint @"https://accounts.google.com/o/oauth2/token"
The first URL will be used to access the endpoint for getting the authorization code, which we will exchange then with the access token using the second endpoint. Let's move on now and let's declare some private properties that are going to hold some essential data.
Step 4
While working in the GoogleOAuth.m
file, inside the private part of the interface, add the following properties. There are comments above each property that explain their purpose:
// The client ID from the Google Developers Console. @property (nonatomic, strong) NSString *clientID; // The client secret value from the Google Developers Console. @property (nonatomic, strong) NSString *clientSecret; // The redirect URI after the authorization code gets fetched. For mobile applications it is a standard value. @property (nonatomic, strong) NSString *redirectUri; // The authorization code that will be exchanged with the access token. @property (nonatomic, strong) NSString *authorizationCode; // The refresh token. @property (nonatomic, strong) NSString *refreshToken; // An array for storing all the scopes we want authorization for. @property (nonatomic, strong) NSMutableArray *scopes; // A NSURLConnection object. @property (nonatomic, strong) NSURLConnection *urlConnection; // The mutable data object that is used for storing incoming data in each connection. @property (nonatomic, strong) NSMutableData *receivedData; // The file name of the access token information. @property (nonatomic, strong) NSString *accessTokenInfoFile; // The file name of the refresh token. @property (nonatomic, strong) NSString *refreshTokenFile; // A dictionary for keeping all the access token information together. @property (nonatomic, strong) NSMutableDictionary *accessTokenInfoDictionary; // A flag indicating whether an access token refresh is on the way or not. @property (nonatomic) BOOL isRefreshing; // The parent view where the webview will be shown on. @property (nonatomic, strong) UIView *parentView;
Note that for the access token there is not a specific NSString property, but we have the accessTokenInfoDictionary
NSMutableDictionary for storing all the access token related data in it.
Step 5
Time to do some initializations. Go inside the - (id)initWithFrame:(CGRect)frame
and add the following code:
- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Set the access token and the refresh token file paths. NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docDirectory = [paths objectAtIndex:0]; _accessTokenInfoFile = [[NSString alloc] initWithFormat:@"%@/acctok", docDirectory]; _refreshTokenFile = [[NSString alloc] initWithFormat:@"%@/reftok", docDirectory]; // Set the redirect URI. // This is taken from the Google Developers Console. _redirectUri = @"urn:ietf:wg:oauth:2.0:oob"; // Make any other required initializations. _receivedData = [[NSMutableData alloc] init]; _urlConnection = [[NSURLConnection alloc] init]; _refreshToken = nil; _isRefreshing = NO; } return self; }
The rest of the initializations will take place inside the methods that they will be used, so for now we are just fine.
8. Define a protocol
Defining a protocol inside a new class is a task that is done usually after most of the class' functionality has been completed, but in our case it would be nice if we have had the protocol definition prepared before we go any further. What we will actually do is to declare the protocol methods that will be implemented later by the classes that will adopt the protocol. So, inside the GoogleOAuth.h
file and above the @interface
line, add the next few lines:
@protocol GoogleOAuthDelegate -(void)authorizationWasSuccessful; -(void)accessTokenWasRevoked; -(void)responseFromServiceWasReceived:(NSString *)responseJSONAsString andResponseJSONAsData:(NSData *)responseJSONAsData; -(void)errorOccuredWithShortDescription:(NSString *)errorShortDescription andErrorDetails:(NSString *)errorDetails; -(void)errorInResponseWithBody:(NSString *)errorMessage; @end
Here is a short description of the above methods:
-
authorizationWasSuccessful
: It will be used after a successful authorization, meaning after having obtained a valid access token. -
accessTokenWasRevoked
: This delegate method will be used when the user revokes all the granted permissions. -
responseFromServiceWasReceived:andResponseJSONAsData:
: This method will be called every time that a response to an API call is received. -
errorOccuredWithShortDescription:andErrorDetails:
: Called when a general error occurs. -
errorInResponseWithBody:
: This delegate method will be called when an error in the HTTP response exists.
Next, inside the @interface
, add the next line:
@interface OAuth : UIWebView <UIWebViewDelegate, NSURLConnectionDataDelegate> @property (nonatomic, strong) id<GoogleOAuthDelegate> gOAuthDelegate; @end
9. Authorization Flow Setup
Step 1
Until now, we declared all the private member variables we are going to need and initialized some of them, we defined constants, delegates, and protocol, so it's now time to begin implementing the actual authorization flow. The best place to begin from is the "entry" point, a public method that the user will call every time that should be authorized or use an API.
Go to the GoogleOAuth.h
file and add the next method declaration:
@interface GoogleOAuth : UIWebView <UIWebViewDelegate, NSURLConnectionDataDelegate> @property (nonatomic, strong) id<GoogleOAuthDelegate> gOAuthDelegate; -(void)authorizeUserWithClienID:(NSString *)client_ID andClientSecret:(NSString *)client_Secret andParentView:(UIView *)parent_View andScopes:(NSArray *)scopes; @end
Step 2
Let's go to the implementation now. I think it would be better to have the method built first and then discuss it a little bit. Inside the GoogleOAuth.m
file add the next snippet:
-(void)authorizeUserWithClienID:(NSString *)client_ID andClientSecret:(NSString *)client_Secret andParentView:(UIView *)parent_View andScopes:(NSArray *)scopes{ // Store into the local private properties all the parameter values. _clientID = [[NSString alloc] initWithString:client_ID]; _clientSecret = [[NSString alloc] initWithString:client_Secret]; _scopes = [[NSMutableArray alloc] initWithArray:scopes copyItems:YES]; _parentView = parent_View; // Check if the access token info file exists or not. if ([self checkIfAccessTokenInfoFileExists]) { // In case it exists load the access token info and check if the access token is valid. [self loadAccessTokenInfo]; if ([self checkIfShouldRefreshAccessToken]) { // If the access token is not valid then refresh it. [self refreshAccessToken]; } else{ // Otherwise tell the caller through the delegate class that the authorization is successful. [self.gOAuthDelegate authorizationWasSuccessful]; } } else{ // In case that the access token info file is not found then show the // webview to let user sign in and allow access to the app. [self showWebviewForUserLogin]; } }
Let's talk a little about the way the method that we just wrote works. As you can see, the first thing we do is to store all the values being passed into the method as parameters to the local properties. These are the client ID, the client secret, the scopes array and the UIView
that the login webview shall appear within.
A scope indicates an API that the app requests access for.
Next, we check if the access token info file exists or not. In case it exists, we load it using the loadAccessTokenInfo
method. We check if it's valid using the checkIfShouldRefreshAccessToken
method and if it's not, we refresh it by using the refreshAccessToken
method. In case the access token is valid and it has not yet expired, we just inform the caller class through the authorizationWasSuccessful
delegate method. Finally, if the access token info file doesn't exist, we simply call the showWebviewForUserLogin
method to force the webview to appear and let the user sign in.
All of the methods above will be implemented one by one.
10. Method Preview & Declaration
As you can see for yourself in the snippet above, the method almost in its entirety works using other, private methods. Before going on to the next steps, it is a good idea to declare all the private methods that we are going to implement inside the private @interface
section.
In order to make the whole idea easier to understand, I prefer to separate the private methods in two categories:
- The first one includes those methods which are part of the authorization flow itself. These are:
@interface OAuth() ... ... -(void)showWebviewForUserLogin; -(void)exchangeAuthorizationCodeForAccessToken; -(void)refreshAccessToken; @end
- The second category includes auxiliary methods whose role is to support the authorization flow by implementing other necessary operations in order the whole procedure properly works. These include:
@interface OAuth() ... ... -(NSString *)urlEncodeString:(NSString *)stringToURLEncode; -(void)storeAccessTokenInfo; -(void)loadAccessTokenInfo; -(void)loadRefreshToken; -(BOOL)checkIfAccessTokenInfoFileExists; -(BOOL)checkIfRefreshTokenFileExists; -(BOOL)checkIfShouldRefreshAccessToken; -(void)makeRequest:(NSMutableURLRequest *)request; @end
Besides all the above, we will implement the following delegate method for the webview:
-(void)webViewDidFinishLoad:(UIWebView *)webView;
Using it we will "read" the authorization code.
Additionally, we will implement the next NSURLConnection
delegate methods:
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; -(void)connectionDidFinishLoading:(NSURLConnection *)connection; -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; -(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
Let's move on now and let's see how all these bind together.
11. Implementing the Authorization Flow
Step 1
Let's continue by writing the -(void)showWebviewForUserLogin
method. This method serves two purposes. The first one is to form the URL (with all the required parameters) that our app will use to get the authorization code. The second is to show a webview to the users, through which they will sign in their Google account and grant access permissions to the app. Let's see the method first.
-(void)showWebviewForUserLogin{ // Create a string to concatenate all scopes existing in the _scopes array. NSString *scope = @""; for (int i=0; i<[_scopes count]; i++) { scope = [scope stringByAppendingString:[self urlEncodeString:[_scopes objectAtIndex:i]]]; // If the current scope is other than the last one, then add the "+" sign to the string to separate the scopes. if (i < [_scopes count] - 1) { scope = [scope stringByAppendingString:@"+"]; } } // Form the URL string. NSString *targetURLString = [NSString stringWithFormat:@"%@?scope=%@&redirect_uri=%@&client_id=%@&response_type=code", authorizationTokenEndpoint, scope, _redirectUri, _clientID]; // Do some basic webview setup. [self setDelegate:self]; [self setScalesPageToFit:YES]; [self setAutoresizingMask:_parentView.autoresizingMask]; // Make the request and add self (webview) to the parent view. [self loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:targetURLString]]]; [_parentView addSubview:self]; }
A couple of observations now. At first, all the scopes existing in the _scopes
array are getting concatenated into one NSString
, after having been URL encoded. This is a must, as every special character that might exist in a scope should be replaced with the equivalent URL encoded character (more on this later). A temporary string is being used for concatenation purposes. Inside the concatenated string the scopes are separated using the plus "+" sign.
Next, the URL string is formed. Pay special attention to the parameters it accepts.
We provide:
- The authorization code endpoint
- The scopes
- The redirect URI which is a constant value for installed applications
- The client ID that we got from the Google Developers Console
- The
response_type
value which is set to the standard value ofcode
In the second half of the method, we are simply setting up the webview that will appear to the user. There is nothing particular I should point out here, the code is easy enough. Just note that after setting everything up and calling the loadRequest
method the webview is added as a subview to the parent view.
Step 2
Once the user has signed in and has agreed to provide access to our app, the authorization code is returned from Google. Actually, there are two ways to get it. The first one involves the use of cookies, where the authorization code is stored in a cookie file. Google provides a sample on how to use this way. The second one, which is the way I prefer to use, is to get the authorization code from the web view's page title. Once we acquire it, we are ready to proceed in obtaining the access token.
This is what we are going to do in the only one webview delegate method that we are going to implement. Let's see this in action.
-(void)webViewDidFinishLoad:(UIWebView *)webView{ // Get the webpage title. NSString *webviewTitle = [webView stringByEvaluatingJavaScriptFromString:@"document.title"]; //NSLog(@"Webview Title = %@", webviewTitle); // Check for the "Success token" literal in title. if ([webviewTitle rangeOfString:@"Success code"].location != NSNotFound) { // The oauth code has been retrieved. // Break the title based on the equal sign (=). NSArray *titleParts = [webviewTitle componentsSeparatedByString:@"="]; // The second part is the oauth token. _authorizationCode = [[NSString alloc] initWithString:[titleParts objectAtIndex:1]]; // Show a "Please wait..." message to the webview. NSString *html = @"<html><head><title>Please wait</title></head><body><h1>Please wait...</h1></body></html>"; [self loadHTMLString:html baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]]; // Exchange the authorization code for an access code. [self exchangeAuthorizationCodeForAccessToken]; } else{ if ([webviewTitle rangeOfString:@"access_denied"].location != NSNotFound) { // In case that the user tapped on the Cancel button instead of the Accept, then just // remove the webview from the superview. [webView removeFromSuperview]; } } }
The authorization code that is returned is in a form similar to code=4/a5F4r45...
. That't why in the above snippet the title is being broken into parts based on the equal sign "=". After that, we are ready to exchange it for the access token.
Step 3
Now that the authorization token has been obtained, the webview is no longer needed. We will remove it though at the end of the whole process (next step). The NSURLConnection
and NSURLRequest
classes will be in use from now on. What we have to do in the exchangeAuthorizationCodeForAccessToken
method is just to setup all the required parameters that are needed to get the access token and to make a request to the access token endpoint. The most important thing I should point out here is that the POST HTTP method is being used. Here is the method.
-(void)exchangeAuthorizationCodeForAccessToken{ // Create a string containing all the post parameters required to exchange the authorization code // with the access token. NSString *postParams = [NSString stringWithFormat:@"code=%@&client_id=%@&client_secret=%@&redirect_uri=%@&grant_type=authorization_code", _authorizationCode, _clientID, _clientSecret, _redirectUri]; // Create a mutable request object and set its properties. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:accessTokenEndpoint]]; [request setHTTPMethod:@"POST"]; [request setHTTPBody:[postParams dataUsingEncoding:NSUTF8StringEncoding]]; [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; // Make the request. [self makeRequest:request]; }
Notice the use of the authorization code that we acquired in the previous step.
Step 4
When making a request and using a NSURLConnection, two things may happen. Either the connection will successfully finish, or it will fail. In our case, when a request fails we only want to inform the caller class through the delegate and that's all. So, let's write the first NSURLConnection
delegate method.
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{ [self.gOAuthDelegate errorOccuredWithShortDescription:@"Connection failed." andErrorDetails:[error localizedDescription]]; }
You see how our delegate methods become handy. Let's go now to the case where the connection is successfully finished and the access token has been received.
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{ // This object will be used to store the converted received JSON data to string. NSString *responseJSON; // This flag indicates whether the response was received after an API call and out of the // following cases. BOOL isAPIResponse = YES; // Convert the received data in NSString format. responseJSON = [[NSString alloc] initWithData:(NSData *)_receivedData encoding:NSUTF8StringEncoding]; // Check for access token. if ([responseJSON rangeOfString:@"access_token"].location != NSNotFound) { // This is the case where the access token has been fetched. [self storeAccessTokenInfo]; // Remove the webview from the superview. [self removeFromSuperview]; if (_isRefreshing) { _isRefreshing = NO; } // Notify the caller class that the authorization was successful. [self.gOAuthDelegate authorizationWasSuccessful]; isAPIResponse = NO; } }
Don't fool yourself by believing that this is going to be the only content of this delegate method. Actually, it's going to become somehow big, but you shouldn't mind about that right now.
Note: Do you see the isAPIResponse
flag? Well, this is useful for knowing when the response regards an API or the authorization process itself. When it comes under any case that has to do with the authorization process, it becomes NO, as you can see in the access token's case. You'll completely understand its significance along the way.
Google returns a JSON object which contains the access token along with other data. The first thing we do is to convert the received data into a string value, so we can easily distinguish what kind of data has been received and properly handle it. By doing so at the beginning of the method, we check whether the access token was received indeed or not, simply by looking for it inside the response string. If it's there, then...wow, we just obtained the access token! Let's take care of it now. First, we save it into the file, and we remove the webview from the superview. In case that the access token was obtained during a refresh process, we set the respective flag to NO
. Finally, we use our delegate to inform the caller class that the access token has been successfully acquired.
The JSON that is returned is something similar to this one:
{ "access_token" : "ya29.AHES6ZSpPmQZwz30EcyhWE9mvi_lgvh4DX8gSQ9GpIbkLL5qhP9sYQ", "token_type" : "Bearer", "expires_in" : 3600, "id_token" : "eyJhbGciOiJSUzI1NiIsImtpZCI6ImUxYzdhM2NkZGFmMzcxOWFlMGNlZGUzOTI4ZmZlZDI1MGFmMmQyODMifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwidG9rZW5faGFzaCI6ImNfWngxMzBaWGQySFlCT0lEWlhiVUEiLCJhdF9oYXNoIjoiY19aeDEzMFpYZDJIWUJPSURaWGJVQSIsImNpZCI6Ijg4ODk5MTE4OTQ4OS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF6cCI6Ijg4ODk5MTE4OTQ4OS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF1ZCI6Ijg4ODk5MTE4OTQ4OS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImlkIjoiMTE0MzE1NTUzNzc0OTY1NTIwNTIzIiwic3ViIjoiMTE0MzE1NTUzNzc0OTY1NTIwNTIzIiwiaWF0IjoxMzczMjE5NDYwLCJleHAiOjEzNzMyMjMzNjB9.jM8_m8aqwW56V2YHob5d-Xlb69yTRBrd_FgagJlSSOxEgRcxpn3D6uDDBmbkiI7S_UgvCc07CAis9wmzTGrKvjpp8JR04ka_LlzdvlaWlFlMvCZgs13GNP8fpi_o4jTgpMLeUM47ZZbBtF0C2uU8XCaVAzTW-VOYZkNPT2SjDY4", "refresh_token" : "1/xZWny-TMV0jZvDRuHxwMl5tTZSiN8yCGP7gaILbPPxk" }
At this point, add the following code segment above the if ([_responseJSON rangeOfString:@"access_token"].location != NSNotFound) {
line. It is required for dealing with any invalid request that may occur. If you wonder why could this happen, just try to play around with the whole authorization or refresh process and you will easily find out.
if ([responseJSON rangeOfString:@"invalid_request"].location != NSNotFound) { NSLog(@"General error occured."); // If a refresh was on the way then set the respective flag to NO. if (_isRefreshing) { _isRefreshing = NO; } // Notify the caller class through the delegate. [self.gOAuthDelegate errorInResponseWithBody:responseJSON]; isAPIResponse = NO; }
Quite important is the next NSURLConnection
delegate method too, if you want the above two to properly work.
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ // Append any new data to the _receivedData object. [_receivedData appendData:data]; }
The authorization flow is now over. However, there is still a lot of work to be done before we will be ready to say that we have a fully implemented class.
12. Refreshing the Access Token
One piece of information that is received along with the access token is the time in seconds that the access token is valid. While the access token is valid, it can be used without any problem when calling APIs. However, when the token expires, a refresh is necessary to take place in the background and a new access token needs to be obtained.
To check if an access token is still valid or not, we will use the checkIfShouldRefreshAccessToken
method. We will implement it a little bit later, along with the rest of the auxiliary private methods. For now, let's focus on the refresh process.
The way we refresh an access token is quite similar to the way we exchange the authorization code for the access token. We set the POST
parameters and we perform a NSURLRequest
so as to get a new access token. Here is the method:
-(void)refreshAccessToken{ // Load the refrest token if it's not loaded alredy. if (_refreshToken == nil) { [self loadRefreshToken]; } // Set the HTTP POST parameters required for refreshing the access token. NSString *refreshPostParams = [NSString stringWithFormat:@"refresh_token=%@&client_id=%@&client_secret=%@&grant_type=refresh_token", _refreshToken, _clientID, _clientSecret ]; // Indicate that an access token refresh process is on the way. _isRefreshing = YES; // Create the request object and set its properties. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:accessTokenEndpoint]]; [request setHTTPMethod:@"POST"]; [request setHTTPBody:[refreshPostParams dataUsingEncoding:NSUTF8StringEncoding]]; [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; // Make the request. [self makeRequest:request]; }
As you can see, we need only the refresh token that was obtained with the access token, the client ID, the client secret, and the standard value grant_type
parameters. Nothing hard or new here.
There are a couple of things I should underline at this point. First of all, a refresh token lasts a lot longer than an access token. That means that a refresh token is not being received every time a refresh procedure is on the way. A refresh token can be used many times and that fact leads to the need for a more permanent way of storage, while the saving place/way of it should be different from the access token's. The last one may be re-written many times and if the refresh token resides with it, it will be deleted after the first refresh.
Secondly, a refresh token can become invalid as well. In that case, and if the access token needs to be updated, the user must be led to enter login information and provide access permissions once again. This is a case we should predict in our code.
Inside the -(void)connectionDidFinishLoading:(NSURLConnection *)connection
delegate method, the case of obtaining an access token from the response already exists. What we also need to add is a new code segment that will check if Google responded that we have an invalid refresh token. So, right before the if ([_responseJSON rangeOfString:@"access_token"].location != NSNotFound)
line, add the following:
// Check for invalid refresh token. // In that case guide the user to enter the credentials again. if ([responseJSON rangeOfString:@"invalid_grant"].location != NSNotFound) { if (_isRefreshing) { _isRefreshing = NO; } [self showWebviewForUserLogin]; isAPIResponse = NO; }
By calling the showWebviewForUserLogin
method, we start the whole procedure over again.
13. Auxiliary Private Methods
Time to implement the rest of the auxiliary, private methods that we have already declared.
Step 1
Let's begin with the -(NSString *)urlEncodeString:(NSString *)stringToURLEncode
method, which is simply used to transform URL related strings to a URL-encoded form. When sending data over the web, not every character can exist in the URL. It should be replaced by other, special characters that are URL-friendly, and that's exactly what the next method does:
-(NSString *)urlEncodeString:(NSString *)stringToURLEncode{ // URL-encode the parameter string and return it. CFStringRef encodedURL = CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef) stringToURLEncode, NULL, (CFStringRef)@"!@#$%&*'();:=+,/?[]", kCFStringEncodingUTF8); return (NSString *)CFBridgingRelease(encodedURL); }
As you can see, the CFURLCreateStringByAddingPercentEscapes
function, which I would advise to read more about it, returns a CFStringRef
type object, which is then converted to NSString
when it's returned. It's not hard to understand the parameters this function accepts. Note that we provide a "list" of characters that should be replaced with percent characters. For example, the ! is converted to %21, the * to %2A, and so on. The last parameter relates to the desired encoding.
Step 2
We keep going with the next one:
-(void)storeAccessTokenInfo{ NSError *error; // Keep the access token info into a dictionary. _accessTokenInfoDictionary = [NSJSONSerialization JSONObjectWithData:_receivedData options:NSJSONReadingMutableContainers error:&error]; // Check if any error occured while converting NSData data to NSDictionary. if (error) { [self.gOAuthDelegate errorOccuredWithShortDescription:@"An error occured while saving access token info into a NSDictionary." andErrorDetails:[error localizedDescription]]; } // Save the dictionary to a file. [_accessTokenInfoDictionary writeToFile:_accessTokenInfoFile atomically:YES]; // If a refresh token is found inside the access token info dictionary then save it separately. if ([_accessTokenInfoDictionary objectForKey:@"refresh_token"] != nil) { // Extract the refresh token. _refreshToken = [[NSString alloc] initWithString:[_accessTokenInfoDictionary objectForKey:@"refresh_token"]]; // Save the refresh token as data. [_refreshToken writeToFile:_refreshTokenFile atomically:YES encoding:NSUTF8StringEncoding error:&error]; // If an error occurs while saving the refresh token notify the caller class. if (error) { [self.gOAuthDelegate errorOccuredWithShortDescription:@"An error occured while saving the refresh token." andErrorDetails:[error localizedDescription]]; } } }
Things are simple here. At first, the received data containing the access token information is stored in the accessTokenInfoDictionary
mutable dictionary, which, in turn, is written to a file. Then, whether a refresh token exists is checked in the returned information. If found, it is extracted and saved to another file. Remember that we need to keep the refresh token separated from the access token file.
Step 3
Let's see the loadAccessTokenInfo
method now and then talk about it:
-(void)loadAccessTokenInfo{ // Check if the access token info file exists. if ([self checkIfAccessTokenInfoFileExists]) { // Load the access token info from the file into the dictionary. _accessTokenInfoDictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:_accessTokenInfoFile]; } else{ // If the access token info file doesn't exist then inform the caller class through the delegate. [self.gOAuthDelegate errorOccuredWithShortDescription:@"Access token info file was not found." andErrorDetails:@""]; } }
First of all, we check if the access token information file exists or not. If it exists, then it's loaded into the accessTokenInfoDictionary
. If the file for some reason is not found, then the errorOccuredWithShortDescription:andErrorDetails:
delegate method is called to inform the caller class.
Step 4
Next, let's see how to load the refresh token as well:
-(void)loadRefreshToken{ // Check if the refresh token file exists. if ([self checkIfRefreshTokenFileExists]) { NSError *error; _refreshToken = [[NSString alloc] initWithContentsOfFile:_refreshTokenFile encoding:NSUTF8StringEncoding error:&error]; // If an error occurs while saving the refresh token notify the caller class. if (error) { [self.gOAuthDelegate errorOccuredWithShortDescription:@"An error occured while loading the refresh token." andErrorDetails:[error localizedDescription]]; } } }
The approach here is the same as previously implemented. We check if the file with the refresh code exists, and if it does we load it into the refreshToken
string. Otherwise, we report an error to the caller class through the delegate method.
Step 5
The next task is to check if the file with the access token information exists or not.
-(BOOL)checkIfAccessTokenInfoFileExists { // If the access token info file exists, return YES, otherwise return NO. return (![[NSFileManager defaultManager] fileExistsAtPath:_accessTokenInfoFile]) ? NO : YES; }
Step 6
Do exactly the same for the refresh token file.
-(BOOL)checkIfRefreshTokenFileExists { // If the refresh token file exists then return YES, otherwise return NO. return (![[NSFileManager defaultManager] fileExistsAtPath:_refreshTokenFile]) ? NO : YES; }
Pretty simple, right?
Step 7
Let's see now how to check if an access token is valid or not. When talking for validity, we mean whether the access token has expired or not. Google returns the time that an access token may keep alive, along with the rest of the returned data. That time is expressed in seconds and it usually is 3,600 seconds or one hour. If an access token expires and we want to exchange data from a Google service, we simply need to refresh it in the background and obtain a new one.
Our strategy is going to be really simple. We will read the creation time of the file containing the access token info, as well as the time the access token should stay valid. If the time elapsed since the file creation is greater than the time to live, then a refresh is required, otherwise the access token is still valid. Let's see everything in action:
-(BOOL)checkIfShouldRefreshAccessToken{ NSError *error = nil; // Get the time-to-live (in seconds) value regarding the access token. int accessTokenTTL = [[_accessTokenInfoDictionary objectForKey:@"expires_in"] intValue]; // Get the date that the access token file was created. NSDate *accessTokenInfoFileCreated = [[[NSFileManager defaultManager] attributesOfItemAtPath:_accessTokenInfoFile error:&error] fileCreationDate]; // Check if any error occured. if (error != nil) { [self.gOAuthDelegate errorOccuredWithShortDescription:@"Cannot read access token file's creation date." andErrorDetails:[error localizedDescription]]; return YES; } else{ // Get the time difference between the file creation date and now. NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:accessTokenInfoFileCreated]; // Check if the interval value is equal or greater than the accessTokenTTL value. // If that's the case then the access token should be refreshed. if (interval >= accessTokenTTL) { // In this case the access token should be refreshed. return YES; } else{ // Otherwise the access token is valid. return NO; } } }
So, in case the access token should be refreshed we return YES, otherwise we return NO. Everything is self-explained, so I comment no further here.
Step 8
There is only one method that has been left out of these steps, the makeRequest
. This method has nothing particular or special that I should point out. It exists just to avoid writing the same code every time we want to make NSURLRequest requests. So, as you will see right away, what we do is to empty the receivedData
object by setting its length to zero and to make a new request.
-(void)makeRequest:(NSMutableURLRequest *)request{ // Set the length of the _receivedData mutableData object to zero. [_receivedData setLength:0]; // Make the request. _urlConnection = [NSURLConnection connectionWithRequest:request delegate:self]; }
14. NSURLConnection Additions
Step 1
There are a few more things that we should add inside the connectionDidFinishLoading:
method to make this class more complete. Until now we have added support for dealing with responses regarding:
- Invalid requests
- Invalid grants (e.g. an expired refresh token)
- Access token obtainment
What we should also add support for is the case of invalid credentials and for any other response that may contain the error word inside it. A response containing an Invalid credentials message arrives when an API is called and the access token is invalid. In our implementation, we always check if the access token is valid or not before doing any requests. However, this remains an edge case that we should predict. For any other general case that the response describes an error, we will call the errorInResponseWithBody
delegate method. So, at the end of the connectionDidFinishLoading:
, add the next code section:
// Check for invalid credentials. // This checking is useful when an API is called without prior checking whether the // access token is valid or not. if ([responseJSON rangeOfString:@"Invalid Credentials"].location != NSNotFound || [responseJSON rangeOfString:@"401"].location != NSNotFound) { [self refreshAccessToken]; isAPIResponse = NO; } // This is the case where any other error message exists in the response. if ([responseJSON rangeOfString:@"error"].location != NSNotFound) { [self.gOAuthDelegate errorInResponseWithBody:responseJSON]; isAPIResponse = NO; }
If you look now at the connectionDidFinishLoading:
method content, you'll see all the cases that we should take care of regarding the Google responses. For normal usage and for the purpose of this tutorial, almost nothing else needs to be added, except for one thing. The call to the responseFromServiceWasReceived:andResponseJSONAsData:
delegate method. This method will be called every time that is retrieved a response about an API call that the caller class should handle it and it doesn't come under any other of the previous cases. Here it is:
// If execution successfully arrives here then notify the caller class that a response was received. if (isAPIResponse) { [self.gOAuthDelegate responseFromServiceWasReceived:responseJSON andResponseJSONAsData:_receivedData]; }
Step 2
It would be useful (especially when debugging) to have the next method implemented as well:
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSLog(@"%d", [httpResponse statusCode]); }
The httpResponse
object contains the returned HTTP status code of the request that was made. If everything rolls normally, the value of this code is 200. Search around the web for more information about the HTTP status codes if you'd like.
15. Access Token Revocation
There are cases you may want to revoke the access you gave in an app. Google provides a URL in which a request can be made and when access is being revoked.
Open the GoogleOAuth.h
class to declare a new public method, as highlighted next:
@interface GoogleOAuth : UIWebView <UIWebViewDelegate, NSURLConnectionDataDelegate> @property (nonatomic, strong) id<GoogleOAuthDelegate> gOAuthDelegate; -(void)authorizeUserWithClienID:(NSString *)client_ID andClientSecret:(NSString *)client_Secret andParentView:(UIView *)parent_View andScopes:(NSArray *)scopes; -(void)revokeAccessToken; @end
Now go to the GoogleOAuth.m
file to implement it:
-(void)revokeAccessToken{ // Set the revoke URL string. NSString *revokeURLString = [NSString stringWithFormat:@"https://accounts.google.com/o/oauth2/revoke?token=%@", [_accessTokenInfoDictionary objectForKey:@"access_token"] ]; // Create and make a request based on the URL string. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:revokeURLString]]; [self makeRequest:request]; // Now that the request for revoking the access in Google has been made, // all local files regarding the access token should be removed as well. NSError *error = nil; // If the access token info file exists then delete it. if ([self checkIfAccessTokenInfoFileExists]) { [[NSFileManager defaultManager] removeItemAtPath:_accessTokenInfoFile error:&error]; if (error != nil) { // If an error occurs while removing the access token info file then notify the caller class through the // next delegate method. [self.gOAuthDelegate errorOccuredWithShortDescription:@"Unable to delete access token info file." andErrorDetails:[error localizedDescription]]; } } // Check now if the refresh token file exists and then remove it. if ([self checkIfRefreshTokenFileExists]) { [[NSFileManager defaultManager] removeItemAtPath:_refreshTokenFile error:&error]; if (error != nil) { // In case of an error while removing the file then notify the caller class through the delegate method. [self.gOAuthDelegate errorOccuredWithShortDescription:@"Unable to delete refresh token info file." andErrorDetails:[error localizedDescription]]; } } if (error == nil) { // If no error occured during file removals then use the next delegate method // to notify the caller class that the access has been revoked. [self.gOAuthDelegate accessTokenWasRevoked]; } }
At first, we prepare the revoke URL string and make the request. Then, any file related to the access token is deleted, and, finally, the accessTokenWasRevoked
delegate method is called.
16. Calling an API
Our class is ready and fully functional and if you use it now you will be able to obtain an access token from Google. But, without having implemented a way to call APIs, the only data you can get is your own personal info. Therefore, let's implement our last, public method, a really important and necessary one.
Go to the GoogleOAuth.h
file and declare the next method just like you see it:
-(void)callAPI:(NSString *)apiURL withHttpMethod:(HTTP_Method)httpMethod postParameterNames:(NSArray *)params postParameterValues:(NSArray *)values;
Note that any API that requires data to be posted in Google must be in a key-value pair. The parameters for this are:
-
apiURL
: The URL string required to make the API call. -
httpMethod
: The desired HTTP method for calling an API. This is where theenum
type we created at the beginning is used. -
params
: A NSArray object containing the keys of the key-value pair form parameters. -
values
: A NSArray object containing the values of the key-value pair form parameters.
Now turn on the GoogleOAuth.m
file and implement the method:
-(void)callAPI:(NSString *)apiURL withHttpMethod:(HTTP_Method)httpMethod postParameterNames:(NSArray *)params postParameterValues:(NSArray *)values{ // Check if the httpMethod value is valid. // If not then notify for error. if (httpMethod != httpMethod_GET && httpMethod != httpMethod_POST && httpMethod != httpMethod_DELETE && httpMethod != httpMethod_PUT) { [self.gOAuthDelegate errorOccuredWithShortDescription:@"Invalid HTTP Method in API call" andErrorDetails:@""]; } else{ // Create a string containing the API URL along with the access token. NSString *urlString = [NSString stringWithFormat:@"%@?access_token=%@", apiURL, [_accessTokenInfoDictionary objectForKey:@"access_token"]]; // Create a mutable request. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]]; // Depending on the httpMethod value set the respective property of the request object. switch (httpMethod) { case httpMethod_GET: [request setHTTPMethod:@"GET"]; break; case httpMethod_POST: [request setHTTPMethod:@"POST"]; break; case httpMethod_DELETE: [request setHTTPMethod:@"DELETE"]; break; case httpMethod_PUT: [request setHTTPMethod:@"PUT"]; break; default: break; } // In case of POST httpMethod value, set the parameters and any other necessary properties. if (httpMethod == httpMethod_POST) { // A string with the POST parameters should be built. // Create an empty string. NSString *postParams = @""; // Iterrate through all parameters and append every POST parameter to the postParams string. for (int i=0; i<[params count]; i++) { postParams = [postParams stringByAppendingString:[NSString stringWithFormat:@"%@=%@", [params objectAtIndex:i], [values objectAtIndex:i]]]; // If the current parameter is not the last one then add the "&" symbol to separate post parameters. if (i < [params count] - 1) { postParams = [postParams stringByAppendingString:@"&"]; } } // Set any other necessary options. [request setHTTPBody:[postParams dataUsingEncoding:NSUTF8StringEncoding]]; [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; } // Make the request. [self makeRequest:request]; } }
There is nothing difficult in this method that needs to be discussed.
Our class is now ready to be put into action. Let's go forward to our demo app to try it out!
17. Configure the Demo App
Step 1
At the beginning of the implementation of this project, while still working from Interface Builder, we added a table view to our project and now it's time to set it up. So, at first, go to the ViewController.h
file and adopt the next protocols, as you see in the highlighted code:
@interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> @property (weak, nonatomic) IBOutlet UITableView *table; - (IBAction)showProfile:(id)sender; - (IBAction)revokeAccess:(id)sender; @end
Besides the above two protocols, we also need to adopt our own protocol regarding our class. So, at the top of the file, import the GoogleOAuth.h
file and also adopt the GoogleOAuthDelegate
protocol:
#import <UIKit/UIKit.h> #import "GoogleOAuth.h" @interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, GoogleOAuthDelegate> @property (weak, nonatomic) IBOutlet UITableView *table; - (IBAction)showProfile:(id)sender; - (IBAction)revokeAccess:(id)sender; @end
Step 2
Now turn to the ViewController.h
file. At the private section of the interface, add these two mutable arrays, as we need them to store the user profile info that will be shown later on within the table view:
@interface ViewController () @property (nonatomic, strong) NSMutableArray *arrProfileInfo; @property (nonatomic, strong) NSMutableArray *arrProfileInfoLabel; @end
Actually, the arrProfileInfoLabel
will contain the data that describe each field of the profile information returned by Google. This data will be placed into the detailTextLabel of each cell.
We also need an object of our newly created class. So, right below the array declarations, add this:
@interface ViewController () @property (nonatomic, strong) NSMutableArray *arrProfileInfo; @property (nonatomic, strong) NSMutableArray *arrProfileInfoLabel; @property (nonatomic, strong) GoogleOAuth *googleOAuth; @end
Step 3
Let's initialize our objects and set our delegates. Go inside the viewDidLoad
method to initialize both the two arrays and the googleOAuth
object, as well as to set the delegates, including the table view's delegate.
- (void)viewDidLoad { [super viewDidLoad]; [_table setDelegate:self]; [_table setDataSource:self]; _arrProfileInfo = [[NSMutableArray alloc] init]; _arrProfileInfoLabel = [[NSMutableArray alloc] init]; _googleOAuth = [[GoogleOAuth alloc] initWithFrame:self.view.frame]; [_googleOAuth setGOAuthDelegate:self]; }
Step 4
Now we must create the table view methods. We will put in action the two arrays we declared previously, even though there is still no data on them. In the next code section I provide all the methods you need completed.
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ return 1; } -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return [_arrProfileInfo count]; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; [cell setAccessoryType:UITableViewCellAccessoryNone]; [[cell textLabel] setFont:[UIFont fontWithName:@"Trebuchet MS" size:15.0]]; [[cell textLabel] setShadowOffset:CGSizeMake(1.0, 1.0)]; [[cell textLabel] setShadowColor:[UIColor whiteColor]]; [[cell detailTextLabel] setFont:[UIFont fontWithName:@"Trebuchet MS" size:13.0]]; [[cell detailTextLabel] setTextColor:[UIColor grayColor]]; } [[cell textLabel] setText:[_arrProfileInfo objectAtIndex:[indexPath row]]]; [[cell detailTextLabel] setText:[_arrProfileInfoLabel objectAtIndex:[indexPath row]]]; return cell; } -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 60.0; }
Step 5
Time to use what we've built by implementing the two IBAction methods we created at the beginning of the project. Starting from the showProfile:
method, we will call the authorizeUserWithClienID: andClientSecret: andParentView: andScopes:
of the GoogleOAuth class, which will fire the authorization flow. Let's implement it:
- (IBAction)showProfile:(id)sender { [_googleOAuth authorizeUserWithClienID:@"YOUR_CLIENT_ID" andClientSecret:@"YOUR_CLIENT_SECRET" andParentView:self.view andScopes:[NSArray arrayWithObjects:@"https://www.googleapis.com/auth/userinfo.profile", nil] ]; }
Make sure to set your own client ID and client secret values.
Let's implement the revokeAccess:
IBAction method now. It's just a single line of code:
- (IBAction)revokeAccess:(id)sender { [_googleOAuth revokeAccessToken]; }
Step 6
Finally, the only thing we have left is the delegate methods. Using the first IBAction method, we force the authorization flow to begin. Using the delegate methods we will control everything and we will retrieve the data we want. So, let's implement them one by one.
We begin from the authorizationWasSuccessful:
method. This method is called after a new access token is successfully obtained or an existing token is still valid. So, we are ready to proceed with the API call. This one is pretty simple as we just ask for the user profile and there is no data to post.
-(void)authorizationWasSuccessful{ [_googleOAuth callAPI:@"https://www.googleapis.com/oauth2/v1/userinfo" withHttpMethod:httpMethod_GET postParameterNames:nil postParameterValues:nil]; }
Continue with the accessTokenWasRevoked:
method. We will simply show an alert view to the user and we will empty the arrays and the table contents.
-(void)accessTokenWasRevoked{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"Your access was revoked!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; [_arrProfileInfo removeAllObjects]; [_arrProfileInfoLabel removeAllObjects]; [_table reloadData]; }
For the error handling methods we will simply log the error messages and that's all. Here are both of them:
-(void)errorOccuredWithShortDescription:(NSString *)errorShortDescription andErrorDetails:(NSString *)errorDetails{ NSLog(@"%@", errorShortDescription); NSLog(@"%@", errorDetails); } -(void)errorInResponseWithBody:(NSString *)errorMessage{ NSLog(@"%@", errorMessage); }
Finally, the most important delegate method, in which we'll get the Google response with the user profile information and we'll fill our arrays. We check if the family_name field exists in the JSON response to be sure that the user profile info exists in the response.
-(void)responseFromServiceWasReceived:(NSString *)responseJSONAsString andResponseJSONAsData:(NSData *)responseJSONAsData{ if ([responseJSONAsString rangeOfString:@"family_name"].location != NSNotFound) { NSError *error; NSMutableDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:responseJSONAsData options:NSJSONReadingMutableContainers error:&error]; if (error) { NSLog(@"An error occured while converting JSON data to dictionary."); return; } else{ if (_arrProfileInfoLabel != nil) { _arrProfileInfoLabel = nil; _arrProfileInfo = nil; _arrProfileInfo = [[NSMutableArray alloc] init]; } _arrProfileInfoLabel = [[NSMutableArray alloc] initWithArray:[dictionary allKeys] copyItems:YES]; for (int i=0; i<[_arrProfileInfoLabel count]; i++) { [_arrProfileInfo addObject:[dictionary objectForKey:[_arrProfileInfoLabel objectAtIndex:i]]]; } [_table reloadData]; } } }
Now we're ready. Let's try it out!
18. OAuth 2.0 in Action
Run the project and let the iPhone Simulator appear. Initially, the table view is empty, as no data exists. Click on the My Profile button and enter your credentials to sign into your Google account. In the next window, allow the app to access your profile information and proceed. Once you get back to the table view, you'll see your personal data appear on-screen!
Stop running the app and start over again. Click again on the My Profile button and notice that no credentials will be asked for this time. Instead, your personal info is being retrieved and will be shown in the table view.
Next, try to revoke your access and then go to your profile once again. This time, you'll be asked for permissions once more.
Conclusion
Through this tutorial we managed to see how the OAuth 2.0 protocol actually works. A simple, but also fully working custom class has been implemented, which consists of a reusable tool. Hopefully this tool is useful for every programmer's toolkit who wants to explore and use the services that Google provides. Of course, new features and functionalities can be added at any time, and you are most welcome to improve it or change it as you wish.
In this tutorial, I tried to cover every aspect required to implement Google services with OAuth 2.0. In a future lesson, we will use this class in a real-world example while working with Google calendar. Until then, play around and get a great taste of the OAuth 2.0 protocol philosophy!
Comments