Google Services Authentication in App Engine, Part 1
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:
- User requests access to listFiles.jsp (actually any of the JSP pages can be used).
- 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.
- 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.
- 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).
- 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:
- 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.
- 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.
- 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.
<?xml version='1.0' encoding='UTF-8'?> <web-app xmlns:xsi='http:www.w3.org2001XMLSchema-instance' xsi:schemaLocation='http:java.sun.comxmlnsjavaee http:java.sun.comxmlnsjavaeeweb-app_2_5.xsd' version='2.5' xmlns='http:java.sun.comxmlnsjavaee'> <!-- Filters --> <filter> <filter-name>AuthorizationFilter<filter-name> <filter-class>com.zazarie.server.servlet.filter.AuthorizationFilter<filter-class> <filter> <filter-mapping> <filter-name>AuthorizationFilter<filter-name> <url-pattern>html*<url-pattern> <filter-mapping> <!-- Servlets --> <servlet> <servlet-name>Step2<servlet-name> <servlet-class>com.zazarie.server.servlet.RequestTokenCallbackServlet<servlet-class> <servlet> <servlet-mapping> <servlet-name>Step2<servlet-name> <url-pattern>authSub<url-pattern> <servlet-mapping> <!-- Default page to serve --> <welcome-file-list> <welcome-file>htmllistFiles.jsp<welcome-file> <welcome-file-list> <web-app>
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:
@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; HttpSession session = request.getSession(); LOGGER.info('Invoking Authorization Filter'); LOGGER.info('Destination URL is: ' + request.getRequestURI()); if (filterConfig == null) return; get the Google user AppUser appUser = LoginService.login(request, response); if (appUser != null) { session.setAttribute(Constant.AUTH_USER, appUser); } identify if user has an OAuth accessToken - it not, will set in motion oauth procedure if (appUser.getCredentials() == null) { need to save the target URI in session so we can forward to it when oauth is completed session.setAttribute(Constant.TARGET_URI, request.getRequestURI()); OAuthRequestService.requestOAuth(request, response, session); return; } else store DocService in the session so it can be reused session.setAttribute(Constant.DOC_SESSION_ID, LoginService.docServiceFactory(appUser)); chain.doFilter(request, response); }
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.
public static AppUser login(HttpServletRequest req, HttpServletResponse res) { LOGGER.setLevel(Constant.LOG_LEVEL); LOGGER.info('Initializing LoginService'); String URI = req.getRequestURI(); UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); if (user != null) { LOGGER.info('User id is: '' + userService.getCurrentUser().getUserId() + '''); String userEmail = userService.getCurrentUser().getEmail(); AppUser appUser = (AppUser) req.getSession().getAttribute( Constant.AUTH_USER); if (appUser == null) { LOGGER.info('appUser not found in session'); see if it is a new user appUser = findUser(userEmail); if (appUser == null) { LOGGER.info('User not found in datastore...creating'); appUser = addUser(userEmail); } else { LOGGER.info('User found in datastore...updating'); appUser = updateUserTimeStamp(appUser); } } else { appUser = updateUserTimeStamp(appUser); } return appUser; } else { LOGGER.info('Redirecting user to login page'); try { res.sendRedirect(userService.createLoginURL(URI)); } catch (IOException e) { e.printStackTrace(); } } return null; }
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:
public static void requestOAuth(HttpServletRequest req, HttpServletResponse res, HttpSession session) { LOGGER.setLevel(Constant.LOG_LEVEL); LOGGER.info('Initializing OAuthRequestService'); GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters(); oauthParameters.setOAuthConsumerKey(Constant.CONSUMER_KEY); oauthParameters.setOAuthConsumerSecret(Constant.CONSUMER_SECRET); Set the scope. oauthParameters.setScope(Constant.GOOGLE_RESOURCE); Sets the callback URL. oauthParameters.setOAuthCallback(Constant.OATH_CALLBACK); GoogleOAuthHelper oauthHelper = new GoogleOAuthHelper( new OAuthHmacSha1Signer()); try { Request is still unauthorized at this point oauthHelper.getUnauthorizedRequestToken(oauthParameters); Generate the authorization URL String approvalPageUrl = oauthHelper .createUserAuthorizationUrl(oauthParameters); session.setAttribute(Constant.SESSION_OAUTH_TOKEN, oauthParameters.getOAuthTokenSecret()); LOGGER.info('Session attributes are: ' + session.getAttributeNames().hasMoreElements()); res.getWriter().print( '<a href='' + approvalPageUrl + ''>Request token for the Google Documents Scope'); } catch (OAuthException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
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:
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Create an instance of GoogleOAuthParameters GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters(); oauthParameters.setOAuthConsumerKey(Constant.CONSUMER_KEY); oauthParameters.setOAuthConsumerSecret(Constant.CONSUMER_SECRET); GoogleOAuthHelper oauthHelper = new GoogleOAuthHelper( new OAuthHmacSha1Signer()); String oauthTokenSecret = (String) req.getSession().getAttribute( Constant.SESSION_OAUTH_TOKEN); AppUser appUser = (AppUser) req.getSession().getAttribute( Constant.AUTH_USER); oauthParameters.setOAuthTokenSecret(oauthTokenSecret); oauthHelper.getOAuthParametersFromCallback(req.getQueryString(), oauthParameters); try { String accessToken = oauthHelper.getAccessToken(oauthParameters); String accessTokenSecret = oauthParameters.getOAuthTokenSecret(); appUser = LoginService.getById(appUser.getId()); appUser = LoginService.updateUserCredentials(appUser, new OauthCredentials(accessToken, accessTokenSecret)); req.getSession().setAttribute(Constant.DOC_SESSION_ID, LoginService.docServiceFactory(appUser)); RequestDispatcher dispatcher = req.getRequestDispatcher((String) req .getSession().getAttribute(Constant.TARGET_URI)); if (dispatcher != null) dispatcher.forward(req, resp); } catch (OAuthException e) { e.printStackTrace(); } }
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!
Continue to the second part of this tutorial.
Reference: Authenticating for Google Services in Google App Engine from our JCG partner Jeff Davis at the Jeff’s SOA Ruminations blog.
hey,just what i needed, im getting frustrated about this Oauth2 thing. thx so much!!! I have one problem though, I am trying it in the local, I got the consumer key and secret, but how about the SESSION_OAUTH_TOKEN, because when I try to log in, I am getting “com.google.gdata.client.authn.oauth.OAuthException: oauth_token_secret does not exist” error. What should I do? thanks!!
Any particular reason that you have two objectify.jar’s in the build path?