Channels ▼
RSS

Web Development

Getting Started With the Cloud: Logging On With Google OAuth


Convert the Private Key File to PKCS8

There's one final step that's not in the Google documentation anywhere, but is critical. The .pem format created earlier is required for the certificate, but the Java code that will use the private key needs that key specified in a different format called "PKCS8." Convert the .pem version to PKCS8 by executing the following openssl command, which creates a .pk8 file that holds the private key in the PKCS8 format:

openssl pkcs8 -in myDomain-rsakey.pem -topk8 -nocrypt -out myDomain-rsakey.pk8

That .pk8 file (myDomain-rsakey.pk8) has to be accessible to your servlet. I just put the file on the classpath (in the WEB-INF/classes directory of the war file), so that I can access it as a normal Java resource using

private static final String PRIVATE_KEY_FILE_NAME = "myDomain-rsakey.pk8";
//...
InputStream in = getClass().getResourceAsStream(PRIVATE_KEY_FILE_NAME);
if( in == null )
	in = ClassLoader.getSystemResourceAsStream(PRIVATE_KEY_FILE_NAME);

The second getResourceAsStream(...) call shouldn't be necessary, but the first call doesn't seem to work under the Google debugger; so I fall back to the system class loader if the first call fails.

In a standard Eclipse configuration, put the file on the classpath by copying it to your project's /src directory. Eclipse will move it to a classpath directory (war/WEB-INF/classes) when you deploy.

Get the Request Token

That's all the preliminaries. Now for some code. I've created a small GWT application that prompts the user to "request authorization." Clicking on that link performs the entire OAuth dance, resulting in an persistent access token that your program uses to talk to Google Calendar. (You don't need to know how GWT works to follow along with my discussion — I've explained everything that's weird)

You need two servlets to handle OAuth, one that creates the URL that your user clicks to authorize access, and a second that handles the Auth token that Google sends when your user grants access. I've put all of the OAuth-related code into the first of these classes so that it will all be in one place, which reduces the working part of the token-handling Servlet (Listing One) to a single line that just calls a static method in the link-creation servlet. Note that I'm passing that static method both the query string (in which Google puts the token) and the "path info," which holds the part of the URI that follows the actual servlet name. For example, The actual address of the servlet is http://myDomain.com/calendar/AuthTokenRegistrar, but I tell Google to send the token to the servlet address http://myDomain.com/calendar/AuthTokenRegistrar/userID, where userID is the key that identifies the user who is requesting access. When I get the token back, I'll extract the userID from the "path info" and use it to put the Auth token into the database entry for that user.

The web.xml entry that makes this scheme possible looks like this:

  <servlet>
  	<servlet-name>AuthTokenRegistrar</servlet-name>
  	<servlet-class>com.holub.calendar.server.AuthTokenRegistrar</servlet-class>
  </servlet>
  
  <servlet-mapping>
  	<servlet-name>AuthTokenRegistrar</servlet-name>
  	<url-pattern><b>/calendar/AuthTokenRegistrar/*</b></url-pattern>
  </servlet-mapping>

The star in the <url-pattern> element is important. Without it, Tomcat won't recognize the longer URL when I append the user ID.

Listing One: AuthTokenRegistrar.java
package com.holub.calendar.server;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/** This servlet handles the OAuth callback that provides the Auth token
 * <span style="font-size:8pt; font-style:italic;">©May 19, 2011, Allen I. Holub. All rights reserved.</span><br>
 * @author Allen Holub (www.holub.com)
 */

public class AuthTokenRegistrar extends HttpServlet
{
	private static final long serialVersionUID = -3588807760494492604L;
	
	@Override protected void doGet( HttpServletRequest request, HttpServletResponse response )
	{	AuthAgentImpl.processAuthToken( 
						request.getPathInfo(),		// holds the userID
						request.getQueryString()  );	// holds the returned OAuth token
	}
}

All the real work goes on in AuthAgentImpl.java (Listing Two). Though you wouldn't know it by looking at it, this class is a servlet also. The base class, RemoteServiceServlet is a GWT class that extends HttpServlet. What's going on here is that GWT supports a remote-procedure-call mechanism that makes it vastly easier to implement a REST interface to the server. I defined the following interface to specify the method to call:

public interface AuthAgent extends RemoteService
{   public String getAuthorizationURL();     // this is the RPC method
    //...
}

You'll notice that the AuthAgentImpl class in Listing Two implements that interface. On the client side, I do some magic GWT stuff (that's not particularly relevant to the subject at hand, so I'm not going to describe it) to get an instance of an RPC proxy for AuthAgent, and then send that proxy a getAuthorizationURL() message. The proxy marshals up the request into an HTTP packet and sends it over wire to the server. The packet is received and parsed by the RemoteServiceServlet base class, which calls the version of getAuthorizationURL() that you'll find in Listing Two at about line 70.

Listing Two: AuthAgentImpl.java
package com.holub.calendar.server;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.holub.calendar.shared.AuthAgent;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.gdata.client.authn.oauth.GoogleOAuthHelper;
import com.google.gdata.client.authn.oauth.GoogleOAuthParameters;
import com.google.gdata.client.authn.oauth.OAuthException;
import com.google.gdata.client.authn.oauth.OAuthRsaSha1Signer;
import com.google.gdata.util.common.util.Base64;

/* This class handles Google OAuth authentication. It exposes a simple RPC interface
 * to the client (defined in AuthAgent) and also implements a static method that's
 * called from the AuthTokenRegistrar servlet (which receives the authorized-request
 * token from Google). This static method updates the database entry for a given
 * User to hold the token.
 * <span style="font-size:8pt; font-style:italic;">©May 19, 2011, Allen I. Holub. All rights reserved.</span><br>
 * @author Allen Holub (www.holub.com)
 *
 * (c) 2011 Allen I. Holub. All rights reserved.
 */

public class AuthAgentImpl extends RemoteServiceServlet implements AuthAgent
{
	private static final long serialVersionUID = 1L;
	
	private static Logger log = LoggerFactory.getILoggerFactory().getLogger(AuthAgentImpl.class.getName());
	
	private static final String 	CONSUMER_KEY 	 = "timezer.com";
	private static final String 	PRIVATE_KEY_FILE = "timezer-rsakey.pk8";
	private static final String 	SCOPE 			 = "https://www.google.com/calendar/feeds/";
	private static final String 	CALLBACK_URL	 = "http://timezer.com:8888/calendar/AuthTokenRegistrar";
	
	private static 		 PrivateKey privateKey;
	
	@Override public void init( ServletConfig config ) throws ServletException
	{
		try
		{
			super.init( config );
			InputStream in = getClass().getResourceAsStream(PRIVATE_KEY_FILE);
			if( in == null )
				in = ClassLoader.getSystemResourceAsStream(PRIVATE_KEY_FILE);
			privateKey = getPrivateKey( in );
			
		}
		catch( ServletException e )
		{	throw e;
		}
		catch( Exception e )
		{	throw new ServletException( e.getMessage() );
		}
	}
	
	/** This is the RPC method that's called from the client in order to get the
	 *  URL at Google at which the end user will authorize access. Note that there's
	 *  no notion of a "user" in this URL because that information is suppled as part
	 *  of the Google login process.
	 */
	@Override public String getAuthorizationURL()
	{	
		String userID = "12345"; // TODO: get the userID from the session data.
		
		try
		{
			String callbackURL = CALLBACK_URL + "/" + userID ;
			GoogleOAuthParameters parameters = new GoogleOAuthParameters();
			parameters.setOAuthConsumerKey	(CONSUMER_KEY);
			parameters.setScope				(SCOPE);
			parameters.setOAuthCallback		(callbackURL);
			
			// On error, getUnauthorizedRequestToken() throws an excpetion with a singularly
			// uninformative message string. Probably, the problem is that the SCOPE
			// string is malformed.
			//
			GoogleOAuthHelper helper = new GoogleOAuthHelper(new OAuthRsaSha1Signer(privateKey));
			helper.getUnauthorizedRequestToken(parameters);
			String authorizationURL = helper.createUserAuthorizationUrl(parameters);
			
			log.info("Got OAuth URL: " + authorizationURL );
			return authorizationURL;
		}
		catch (OAuthException e)
		{
			log.error( e.getMessage() );
			// fall through to error processing
		}
		return null;
	}
	
	/** This method is called from he AuthTokenRegistr to get the OAuth token returned
	 *  in the URL. It's located in the AuthAgentImpl class because the private key, etc., is here.
	 */
	static public void processAuthToken( String extraPathInfo, String queryString )
	{
		try
		{
			GoogleOAuthParameters parameters = new GoogleOAuthParameters();
			parameters.setOAuthConsumerKey(CONSUMER_KEY);
			
			GoogleOAuthHelper helper = new GoogleOAuthHelper(new OAuthRsaSha1Signer(privateKey));
			helper.getOAuthParametersFromCallback( queryString, parameters);	
			
			// Send an HTTP request to Google to convert the authorized-request token
			// to a persistent "access token" The token is returned in the response
			// payload, and is extracted by the library method.code.
			
			String accessToken = helper.getAccessToken( parameters );
			log.info("Got Access Token: " + accessToken );
			
			// TODO: put the access token into the database! The User ID is in
			// the extraPathInfo argument.
		}
		catch(OAuthException e)
		{	log.error( e.getMessage() );
		}
	}
	
	/**
	 * Covert the private key (stored in a file on the classpath) into a Java
	 * PrivateKey object.
	 * 
	 * @param keyFileIn
	 * @return
	 * @throws Exception
	 */
	static private PrivateKey getPrivateKey(InputStream keyFileIn) throws Exception
	{
		BufferedReader in = new BufferedReader( new InputStreamReader(keyFileIn) );
	  
		String BEGIN = "-----BEGIN PRIVATE KEY-----";
		String END = "-----END PRIVATE KEY-----";
		
		StringBuffer keyAsString = new StringBuffer();
		boolean 	 ignoreInput = true;
		
		for( String line; (line = in.readLine()) != null ; )
		{	if( line.matches(BEGIN) )
			{	ignoreInput = false;
				continue;				// ignore the BEGIN line
			}
			else if( ignoreInput )
				continue;
			else if( line.matches( END ) )
				break;

			keyAsString.append( line );
		}
			
		in.close();
	
		KeyFactory		factory	= KeyFactory.getInstance("RSA");
		EncodedKeySpec	keySpec = new PKCS8EncodedKeySpec(Base64.decode( keyAsString.toString() ));
		return factory.generatePrivate(keySpec);
	}
}

I'll come back to that method in a moment, but lets start off at the top of Listing Two with the setup code that you need to run before you can actually talk to Google.

At the top of the class definition you'll find several important constants:

CONSUMER_KEY This is the consumer key (that's "Consumer" in the OAuth sense, as described last month) that was created by Google when you registered your cert. I got it from Google during the registration process (See Figure 5, above). The "OAuth Consumer Secret" isn't necessary when you use RSA signing to authenticate the request, which is what I'm doing in the current example, so you can ignore that.
PRIVATE_KEY_FILE This is the name of the PKCS8 (.pk8) file that holds the private key. This file should be located somewhere on your classpath, as I discussed earlier
SCOPE The "scope" argument specifies which Google services you want to access. For example, https://www.google.com/calendar/feeds/ gives you access to Google Calendar; use https://docs.google.com/feeds/ to access Google Documents. You can specify several services in this string as a space-separated (not comma-separated) list. Here's the complete list of scopes for all the Google services.

In theory, the scopes are just feed-data URLs, and you can narrow the scope of your request by providing a more restrictive feed URL. I haven't been able to get that to work, however, and the documentation for how to narrow the scope is either nonexistent or so well hidden that I haven't been able to find it. If you want to play around with that featrue in your copious spare time, the entire feed-url system for the Calendar API is described in the Data API Atom Reference.

CALLBACK_URL This argument is the base URL to which Google sends the unauthorized request token. This is the URL of the AuthTokenRegistrar servlet we looked at earlier. Obviously, you should use a domain of your own, here. You don't have my private-key file, so attempts to ask for permission in the name of my domain (timezer.com) won't work. The odd port assignment (:8888) is the port that's used by the GWT Jetty sever, which is running when you're debugging a GWT application inside Eclipse. Omit the port if your servlet is hosted by a web server that's running on the standard port 80.

Moving into the init(...) method that is called when the Servlet is activated, I'm setting up for future work by creating an object to represent the private key. I get the file, then call getPrivateKey(...) to convert it to a java.security.PrivateKey object. That class is just part of the standard Java library.

The getPrivateKey method is down at the bottom of the current class definition. It just reads in the file, stripping out header and footer information, then calls a few java-security methods to create the key. The details are unimportant, but note that this is just Java-security stuff, not anything that that's special to Google. For those of you who've bothered to figure out how that works, the standard out-of-the-box "provider" works just fine here (if you're not in that exulted category, forget that you've ever seen this sentence).

Display The Authorization-Page URL

Now the client side gets involved. I'm running an AJAX client, so some client-side JavaScript to assemble a link to the Google-authorization page. It gets the URL for the href in that link by calling getAuthorizationURL(). If I had used a non-AJAX approach, the servlet or jsp that would have created the page that contains the "get authorization" link would call the same method. Note that an AJAX application can not do this work on the client side, because you really don't want your private key to be accessible to the client, otherwise any random hacker could pretend they're you when they talk to Google.

The method creates the link using google-supplied methods, which are a little strange. Rather than just passing arguments to method, you start out by creating a GoogleOAuthParameters object and put the arguments in there. Note that, at the very top of the try block, I'm appending the userID to the callback URL so that the token-processing servlet we discussed earlier can figure out which user is making the request.

I then create a GoogleOAuthHelper object to do the actual work, passing it another Google object (an OAuthRsaSha1Signer) that, mercifully, encapsulates the overcomplicated process of digitally signing the request using the javax.crypo APIs. I'm pretty sure that this call is thread safe since it's using a unique signer object, but the underlying java.crypto classes are not thread safe, so I may need to do some more work here. If anybody who works for Google is reading this article, call me.

The call to getUnauthorizedRequestToken(...) now makes the request to the Google server, using Google's HTTP/REST protocol. Since we're going out to the network, this call could take a while. If everything works, we'll now have a URL for a Google authorization page that looks like the one in Figure 5, and we'll embed that URL in a link or a button in the UI.

Authorize The Request Token

Whether we continue from this point depends on the user. If he or she clicks on the link, they'll get to the authorization page. If they decide to grant access, Google will respond to the URL specified in the CALLBACK_URL constant (that was passed to Google as part of requesting the URL to begin with). Assuming that all that happens, Google posts an HTTP reply back to us, thereby invoking the AuthTokenRegistrar servlet I discussed a moment ago. This servlet calls the static processAuthToken method that's defined just beneath the method we were just looking at (in Listing 2).

The processAuthToken(...) method extracts the unauthorized token from the query sting, then signs it (to prove that the entity that requested the token has actually received it) and passes it back up to Google with a call to getAccessToken(...), which makes another HTTP request to the Google server to get the token.

Note that processAuthToken(...) does not recycle the GoogleOAuthParameters or GoogleAOuthHelper object that was used by getAuthorizationURL(). This is a good thing. In the real world, where multiple instances of this servlet will be running on multiple threads (maybe in multiple servers) to service multiple clients, it would very difficult to keep track of a specific parameters or helper object. You can't just squirrel them away in fields of the class, because those fields could be overwritten by other threads. It's a serious bug if you attempt to keep the earlier objects somewhere and then reuse them, thinking that you'll somehow make the program more efficient.

So, finally, if everything works right, getAccessToken returns an authorized, persistent token, which when passed to one of the other Google APIs (such as the ones that access Calendar), will permit that method to do its work. In fact, I'll show you how to do that in a future installment of this series. The access token remains valid until the user logs on to their Google account and revokes the permission that he or she granted earlier, so you don't have to do this whole dance again, and you should store the access token in the database for later use. Use the user ID, passed in the extraPathInfo argument, to store the token in the right place.

Conclusion

So that's it. It takes lot longer just to set everything up than it does to actually write the code, but that's the life of programmer. You'll need to use this process for every Google service that you intend to access, however, so this code is central to every Google GData API. I'll continue with this series in future months with examples of how to use those APIs.

Getting the Code

The Eclipse project that holds the entire project from which the code in this article was extracted is available at http://www.holub.com/software/ddj.

Related Articles

Getting Started with The Cloud: The Ecosystem

Getting Started with Google Apps and OAuth


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV