Wednesday, May 23, 2012

Authenticating for Google Services in Google App Engine


(NOTE: This tutorial uses the older, and now deprecated Oauth 1.0 approach -- See my new blog entry for an OAuth 2.0 tutorial).

Introduction


This post will illustrate how to build a simple Google App Engine (GAE) Java application that authenticates against Google as well as leverages Google's OAuth for authorizing access to Google's API services such as Google Docs.  In addition, building on some of the examples already provided by Google, it will also illustrate how to persist data using the App Engine Datastore and Objectify.

Project Source Code

The motivation behind this post is that I struggled to previously find any examples that really tied these technologies together. Yet, these technologies really represent the building-blocks for many web applications that want to leverage the vast array of Google API services. 

To keep things simple, the demo will simply allow the user to login via a Google domain; authorize access to the user's Google Docs services; and display a list of the user's Google Docs Word and Spreadsheet documents. Throughout this tutorial I do make several assumptions about the reader's expertise, such as a pretty deep familiarity with Java.

Overview of the Flow


Before we jump right into the tutorial/demo, let's take a brief look at the navigation flow. 
While it may look rather complicated, the main flow can be summarized as:
  1. User requests access to listFiles.jsp (actually any of the JSP pages can be used).
  2. A check is make to see if the user is logged into Google. If not, they are re-directed to a Google login page -- once logged in, they are returned back. 
  3. A check is then made to determine whether the user is stored in the local datastore. If not, the user is added along with the user's Google domain email address.
  4. Next, we check to see if the user has granted OAuth credentials to the Google Docs API service. If not, the OAuth authentication process is initiated. Once the OAuth credentials are granted, they are stored in the local user table (so we don't have to ask each time the user attempts to access the services).
  5. Finally, a list of Google Docs Spreadsheet or Word docs is displayed.
This same approach could be used to access other Google services, such as YouTube (you might display a list of the user's favorite videos, for example).

Environment Setup


For this tutorial, I am using the following:
  • Eclipse Indigo Service Release 2 along with the Google Plugin for Eclipse (see setup instructions).
  • Google GData Java SDK Eclipse plugin version 1.47.1 (see setup instructions). 
  • Google App Engine release 1.6.5. Some problems exist with earlier versions, so I'd recommend making sure you are using it. It should install automatically as part of the Google Plugin for Eclipse.
  • Objectify version 3.1. The required library is installed already in the project's war/WEB-INF/lib directory.
 After you have imported the project into Eclipse, your build path should resemble:


The App Engine settings should resemble:


You will need to setup your own GAE application, along with specifying your own Application ID (see the Google GAE developer docs). 

The best tutorial I've seen that describes how to use OAuth to access Google API services can be found here. One of the more confusing aspects I found was how to acquire the necessary consumer key and consumer secret values that are required when placing the OAuth request. The way I accomplished this was:
  1. Create the GAE application using the GAE Admin Console. You will need to create your own Application ID (just a name for your webapp). Once you have it, you will update your Application ID in the Eclipse App Engine settings panel that is shown above.
  2. Create a new Domain for the application. For example, since my Application ID was specified above as "tennis-coachrx", I configured the target URL path prefix as: http://tennis-coachrx.appspot.com/authSub. You will see how we configure that servlet to receive the credentials shortly. 
  3. To complete the domain registration, Google will provide you an HTML file that you can upload. Include that file the root path under the /src/war directory and upload the application to GAE. This way, when Google runs it's check, the file will be present and it will generate the necessary consumer credentials. Here's a screenshot of what the setup looks like after it is completed:

Once you have the OAuth Consumer Key and OAuth Consumer Secret, you will then replace the following values in the com.zazarie.shared.Constant file:

final static String CONSUMER_KEY = "";
final static String CONSUMER_SECRET = "";

Whew, that seemed like a lot of work! However, it's a one-time deal, and you shouldn't have to fuss with it again.

Code Walkthrough


Now that we got that OAuth configuration/setup out of the way, we can dig into the code. Let's begin by looking at the structure of the war directory, where your web assets reside:

The listFiles.jsp is the default JSP page that is displayed when your first enter the webapp. Let's now look at the web.xml file to see how this is configured, along with the servlet filter which is central to everything.

The servlet filter called AuthorizationFilter is invoked whenever a JSP file located in the html directory is requested. The filter, as we'll look at in a moment, is responsible for ensuring that the user is logged into Google, and if so, then ensures that the OAuth credentials have been granted for that user (i.e., it will kick off the OAuth credentialing process, if required).

The servlet name of Step2 represents the servlet that is invoked by Google when the OAuth credentials have been granted -- think of it as a callback. We will look at this in more detail in a bit.

Let's take a more detailed look at the AuthorizationFilter.

AuthorizationFilter Deep Dive

The doFilter method is where the work takes place in a servlet filter.  Here's the implementation:


Besides the usual housekeeping stuff, the main logic begins with the line:

AppUser appUser = LoginService.login(request, response);

As we will see in a moment, the LoginService is responsible for logging the user into Google, and also will create the user in the local BigTable datastore. By storing the user locally, we can then store the user's OAuth credentials, eliminating the need for the user to have to grant permissions every time they access a restricted/filtered page.

After LoginService has returned the user (AppUser object), we then store that user object into the session (NOTE: to enable sessions, you must set sessions-enabled in the appengine-web.xml file):

session.setAttribute(Constant.AUTH_USER, appUser);

We then check to see whether the OAuth credentials are associated with that user:

if (appUser.getCredentials() == null) {

   session.setAttribute(Constant.TARGET_URI, request.getRequestURI());

   OAuthRequestService.requestOAuth(request, response, session);
   return;
} else 
   session.setAttribute(Constant.DOC_SESSION_ID,LoginService.docServiceFactory(appUser));

If getCredentials() returns a null, the OAuth credentials have not already been assigned for the user. This means that the OAuth process needs to be kicked off. Since this involves a two-step process of posting the request to Google and then retrieving back the results via the callback (Step2 servlet mentioned above), we need to store the destination URL so that we can later redirect the user to it once the authorization process is completed. This is done by storing the URL requested into the session using the setAttribute method.

We then kick off the OAuth process by calling the OAuthRequestService.requestOAuth() method (details discussed below).

In the event that if getCredentials() returns a non-null value, this indicates that we already have the user's OAuth credentials from their local AppUser entry in the datastore, and we simply add it to the session so that we can use it later.

LoginService Deep Dive

The LoginService class has one main method called login, followed by a bunch of JPA helper methods for saving or updating the local user in the datastore. We will focus on login(), since that is where most of the business logic resides.


The first substantive thing we do is use Google UserService class to determine whether the user is logged into Google:

UserService userService = UserServiceFactory.getUserService();

User user = userService.getCurrentUser();

If the User object returned by Google's call is null, the user isn't logged into Google, and they are redirected to a login page using:

res.sendRedirect(userService.createLoginURL(URI));

If the user is logged (i.e., not null), the next thing we do is determine whether that user exists in the local datastore. This is done by looking up the user with their logged-in Google email address with appUser = findUser(userEmail). Since JPA/Objectify isn't the primary discussion point for this tutorial, I won't go into how that method works. However, the Objectify web site has some great tutorials/documentation.

If the user doesn't exist locally, the object is populated with Google's email address and created using appUser = addUser(userEmail). If the user does exist, we simply update the login timestamp for logging purposes.

OAuthRequestService Deep Dive

As you may recall from earlier, once the user is setup locally, the AuthorizationFilter will then check to see whether the OAuth credentials have been granted by the user. If not, the OAuthRequestService.requestOAuth() method is invoked. It is shown below:


To simplify working with OAuth, Google has a set of Java helper classes that we are utilizing. The first thing we need to do is setup the consumer credentials (acquiring those was discussed earlier):

GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters();
oauthParameters.setOAuthConsumerKey(Constant.CONSUMER_KEY);
oauthParameters.setOAuthConsumerSecret(Constant.CONSUMER_SECRET);

Then, we set the scope of the OAuth request using:
oauthParameters.setScope(Constant.GOOGLE_RESOURCE);

Where Constant.GOOGLE_RESOURCE resolves to https://docs.google.com/feeds/. When you make an OAuth request, you specify the scope of what resources you are attempting to gain access. In this case, we are trying to access Google Docs (the GData API's for each service have the scope URL provided).  Next, we establish where we want Google to return the reply.

oauthParameters.setOAuthCallback(Constant.OATH_CALLBACK);

This value changes whether we are running locally in dev mode, or deployed to the Google App Engine. Here's how the values are defined in the the Constant interface:

// Use for running on GAE
//final static String OATH_CALLBACK = "http://tennis-coachrx.appspot.com/authSub";

// Use for local testing
final static String OATH_CALLBACK = "http://127.0.0.1:8888/authSub";

When then sign the request using Google's helper:

GoogleOAuthHelper oauthHelper = new GoogleOAuthHelper(new OAuthHmacSha1Signer());

We then generate the URL that the user will navigate to in order to authorize access to the resource. This is generated dynamically using:

String approvalPageUrl = oauthHelper.createUserAuthorizationUrl(oauthParameters);

The last step is to provide a link to the user so that they can navigate to that URL to approve the request. This is done by constructing some simple HTML that is output using res.getWriter().print().

Once the user has granted access, Google calls back to the servlet identified by the URL parameter /authSub, which corresponds to the servlet class RequestTokenCallbackServlet. We will examine this next.

RequestTokenCallbackServlet Deep Dive

The servlet uses the Google OAuth helper classes to generate the required access token and secret access token's that will be required on subsequent calls to to the Google API docs service.  Here is the doGet method that receives the call back response from Google:


The Google GoogleOAuthHelper is used to perform the housekeeping tasks required to populate the two values we are interested in:

String accessToken = oauthHelper.getAccessToken(oauthParameters);
String accessTokenSecret = oauthParameters.getOAuthTokenSecret();

Once we have these values, we then requery the user object from the datastore, and save those values into the AppUser.OauthCredentials subclass:

appUser = LoginService.getById(appUser.getId());
appUser = LoginService.updateUserCredentials(appUser,
          new OauthCredentials(accessToken, accessTokenSecret));
req.getSession().setAttribute(Constant.DOC_SESSION_ID,
LoginService.docServiceFactory(appUser));

In addition, you'll see they are also stored into the session so we have them readily available when the API request to Google Docs is placed.

Now that we've got everything we need, we simply redirect the user back to the resource they had originally requested:

RequestDispatcher dispatcher = req.getRequestDispatcher((String) req
.getSession().getAttribute(Constant.TARGET_URI));
dispatcher.forward(req, resp);

Now, when they access the JSP page listing their documents, everything should work!

Here's a screencast demo of the final product:



Hope you enjoyed the tutorial and demo -- look forward to your comments!


2 comments:

sofwarewiki said...

Nice to see a full article explaining in detail. However, I am slowly stopping the use of app engine due to recent price changes, I am afraid it will not be free anymore.

Unknown said...

wow, great detail in the post.. veryyy great contibution!!