Wednesday, January 14, 2009

Simple OAuth Client and Server Examples

The OAuth Google code (http://code.google.com/p/oauth/) is woefully undocumented, so I put together below a zeroeth order approximation to how things actually work. To use this, you need both the Open Social Java implementation (for the client) and the OAuth Java implementation. Check them both out from the Google Code SVNs as described in earlier posts.

In the examples below, we will use OAuth's two-legged authentication. This is the server-to-server authentication used by OpenSocial gadgets. It is appropriate when no human intervention is needed (or is possible) in the authentication process. Also, we are going to just look at shared secret key-style authentication, using the HMAC-SH1 algorithm and symmetric key encryption. The OAuth code will also support public/private asymmetric keys.

Finally, note that the steps for generating the OAuth consumer key and consumer secret are out of scope. We will just use the key and secret from the earlier OpenSocial gadget experiments. Generally, shared secrets will need to be communicated through a separate registration process. Presumably, the server would use the consumer key sent by the client to look up the secret key on the server side from a database, but check the OAuth specification for details.

Server Code
We'll implement a dummy server as a JSP. You can put this in an Apache Tomcat server under a webapp called OAuthTest. You will also need to put several jars in webapp/OAuthTest/WEB-INF/lib:

ls apache-tomcat-5.5.27/webapps/OAuthTest/WEB-INF/lib/
commons-codec-1.3.jar jetty-6.1.11.jar
commons-httpclient-3.1.jar jetty-util-6.1.11.jar
commons-logging-1.1.jar junit.jar
httpclient-4.0-beta1.jar oauth-core-20090108.jar
httpcore-4.0-beta2.jar

The above jars are all from OAuth/java/lib, plus the oauth-core jar that you generated when you compiled things (see previous post--use "mvn clean install").

Here at last is the actual JSP for the dummy service. I call this OAuthTest.jsp.

--------------------

<%
//Presumably this should actually be looked up for a given key.  Use a real key.
String consumerSecret="123121212yourkeyhere18918";

//Presumably the key is sent by the client. This is part of the URL, after all.  Use a real key.
String consumerKey="orkut.com:1231212122";

//Construct the message object. Use null for the URL and let the code construct it.
OAuthMessage message=OAuthServlet.getMessage(request,null);

//Construct an accessor and a consumer
OAuthConsumer consumer=new OAuthConsumer(null, consumerKey, consumerSecret, null);
OAuthAccessor accessor=new OAuthAccessor(consumer);

//Now validate. Weirdly, validator has a void return type. It throws exceptions
//if there are problems.
SimpleOAuthValidator validator=new SimpleOAuthValidator();
validator.validateMessage(message,accessor);

//Now what? Generate some JSON here for example.
System.out.println("It must have worked"); %>


-------------------

Client Code

To build an OAuth client, we can start from our earlier OpenSocial client. For convenience, we will leave in the OpenSocialUrl and OpenSocialHttpRequest classes, which help construct and execute the REST invocation, but one could easily eliminate this and use the standard java.net classes that underly these two.

Here's the client code. Save it in a file called MyOAuthClient.java in OpenSocial's java/samples directory.
---------------------
import org.opensocial.data.*;
import org.opensocial.client.*;
import net.oauth.*;
import java.util.*;

public class MyOAuthClient {

public static void main(String[] args) {
MyOAuthClient mosc=new MyOAuthClient();
}

public MyOAuthClient() {
String REST_BASE_URI=
"http://localhost:8080/OAuthTest/OAuthTest.jsp";
String CONSUMER_SECRET=
"uynAeXiWTisflWX99KU1D2q5";
String CONSUMER_KEY=
"orkut.com:623061448914";
// String VIEWER_ID=
// "03067092798963641994";
String VIEWER_ID="08354253340777199997";

try {
OpenSocialUrl requestUrl = new OpenSocialUrl(REST_BASE_URI);
OpenSocialHttpRequest request=new OpenSocialHttpRequest(requestUrl);
requestUrl.addQueryStringParameter("xoauth_requestor_id", VIEWER_ID);
requestUrl.addQueryStringParameter("st", "");

String requestMethod=request.getMethod();
String postBody = request.getPostBody();

OAuthMessage message =
new OAuthMessage(requestMethod, requestUrl.toString(), null);

OAuthConsumer consumer =
new OAuthConsumer(null, CONSUMER_KEY, CONSUMER_SECRET, null);
consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);

OAuthAccessor accessor = new OAuthAccessor(consumer);
accessor.accessToken = "";
message.addRequiredParameters(accessor);

for (Map.Entry p : message.getParameters()) {
if (!p.getKey().equals(postBody)) {
requestUrl.addQueryStringParameter(
OAuth.percentEncode(p.getKey()),
OAuth.percentEncode(p.getValue()));
}
}

//Take a look at the signed URL
System.out.println("Signed REST URL: "+requestUrl.toString());

//Done with signing. Now back to OpenSocialBatch's submitRest()
//Finally, get the response. This is the meat of the getHttpResponse
//method, without the error checking.
request.execute();
}

catch(Exception ex) {
ex.printStackTrace();
}
}
}
------------------
Compile this from the OpenSocial SVN checkout's java directory with the command ant compile-samples.

Now set your classpath and execute. The following UNIX commands will do the trick:
export CP=`echo $HOME/opensocial-java-client-read-only/java/lib/*.jar | tr ' ' ':'`

export CP=`echo $HOME/opensocial-java-client-read-only/java/dist/*.jar | tr ' ' ':'`:$CP


java -classpath $CP:/Users/marlonpierce/opensocial-java-client-read-only/java/samples/bin/ MyOAuthClient


Check the output of your Tomcat server's catalina.out to see if it worked.

Obviously this has all been a simple exercise to see how to get things going and needs a lot of work to make it a real service.


Monday, January 12, 2009

Quick Note on Google Code OAuth SVN Checkout

I'm looking at the Google OAuth code (http://code.google.com/p/oauth). It's pretty light on instructions. To compile the Java checkout from SVN, you also need to check out the Maven repositories:

mkdir oauth-fun; cd oauth-fun
svn checkout http://oauth.googlecode.com/svn/code/java
svn checkout http://oauth.googlecode.com/svn/code/maven

You compile with

cd java (assuming you are in oauth-fun)
mvn clean install

The problem is that my checkout today didn't compile. I had to add the maven repository to the top level pom.xml (the one in oauth-fun/java) like so:

<repositories>
<repository>
<id>oauth-local</id>
<url>file:../maven</url>
</repository>
</repositories>

Sunday, January 11, 2009

Roll Your Own REST with Open Social Clients

As in the last few posts, I've been looking at the OpenSocial REST/RPC client code. This can be used to access (for example) Orkut as a back-end service. The Java clients are here: http://code.google.com/p/opensocial-java-client/. The general (programming-language neutral) API is documented here: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html

The Java client examples show how to do simple stuff like access a person's profile data and his/her friends' information. The primary higher level API for this is the OpenSocialClient.java class. The problem is that this class only implements a subset of the possible REST/RPC commands at this time. Peeling back the covers a little, however, you can see that this class is really just constructing REST URLs or RPC commands using lower level classes, so you can in principle do this yourself and bypass OpenSocialClient entirely.

I decided to try this out. The code below uses the REST style invocations to duplicate the DisplayProfileData.java sample code. It turned out to be a little convoluted to do this because the OpenSocialClient class itself gets passed to lower level classes (particularly OpenSocialRequstSigner.java), which is not helpful. Hopefully the developers will reorganize this in the future.

Anyway, on to the code. I think it can be modified to construct any OpenSocial REST request, including PUT and DELETE (which are not currently in OpenSocialClient), but that is a test for another day. I don't claim this is beautiful. It is just my attempt to unravel everything.

I used Revision 33 of the SVN checkout. The OAuth snapshot jar in lib can cause problems if you use a different version. See comments below.

----------------

import org.opensocial.data.*;
import org.opensocial.client.*;
import net.oauth.*;
import java.util.*;

public class MyOpenSocialClient {

public static void main(String[] args) {
MyOpenSocialClient mosc=new MyOpenSocialClient();
}

public MyOpenSocialClient() {
String REST_BASE_URI=
"http://sandbox.orkut.com/social/rest";
String CONSUMER_SECRET=
"uynAeXiWTisflWX99KU1D2q5";
String CONSUMER_KEY=
"orkut.com:623061448914";
// String VIEWER_ID=
// "03067092798963641994";
String VIEWER_ID="08354253340777199997";

try {
//Construct the request object
OpenSocialRequest req=new OpenSocialRequest("people","people.get");
req.addParameter("groupId","@self");
req.addParameter("userId","08354253340777199997");

//Construct the request URL from the request object
//This is the OpenSocialBatch.submitRest() method.
OpenSocialUrl requestUrl = new OpenSocialUrl(REST_BASE_URI);
requestUrl.addPathComponent(req.getRestPathComponent());
if (req.getParameter("userId") != null) {
requestUrl.addPathComponent(req.getParameter("userId"));
}
if (req.getParameter("groupId") != null) {
requestUrl.addPathComponent(req.getParameter("groupId"));
}
System.out.println("This is the REST URL: "+requestUrl.toString());

//Now construct and sign the request
//This is the OpenSocialRequestSigner.signRequest() code.
OpenSocialHttpRequest request = new OpenSocialHttpRequest(requestUrl);
requestUrl.addQueryStringParameter("xoauth_requestor_id", VIEWER_ID);
requestUrl.addQueryStringParameter("st", "");

String postBody = request.getPostBody();
System.out.println("Post bod: "+postBody);
String requestMethod = request.getMethod();
// OpenSocialUrl requestUrl = request.getUrl();

OAuthMessage message =
new OAuthMessage(requestMethod, requestUrl.toString(), null);

if (postBody != null) {
message.addParameter(postBody, "");
}

OAuthConsumer consumer =
new OAuthConsumer(null, CONSUMER_KEY, CONSUMER_SECRET, null);
consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);

OAuthAccessor accessor = new OAuthAccessor(consumer);
accessor.accessToken = "";

message.addRequiredParameters(accessor);

//VERSION WARNINIG: This works with SVN version 33 and oauth-20081115.jar but other
//versions may have problems.
for (Map.Entry p : message.getParameters()) {
if (!p.getKey().equals(postBody)) {
requestUrl.addQueryStringParameter(
OAuth.percentEncode(p.getKey()),
OAuth.percentEncode(p.getValue()));
}
}

//Take a look at the signed URL
System.out.println("Signed REST URL: "+requestUrl.toString());

//Done with signing. Now back to OpenSocialBatch's submitRest()
//Finally, get the response. This is the meat of the getHttpResponse
//method, without the error checking.
request.execute();
String responseString=request.getResponseString();
OpenSocialResponse resp=OpenSocialJsonParser.getResponse(responseString, req.getId());
//A little clunky--have to add this to the OpenSocialResponse
//object's "items" map with the "people" key.
resp.addItem("people",responseString);

System.out.println("Response: "+responseString);

//Now cast the response into an OpenSocialPerson object
OpenSocialPerson person=resp.getItemAsPerson("people");

System.out.println("----------");

// Output the name and ID of the requested person
System.out.println("Info. for " + person.getDisplayName());
System.out.println("ID: " + person.getId());

// Retrieve individual fields using the getField method; fields may be
// complex (objects or arrays) or simple (Strings), which you can
// determine by querying the object using the isComplex method.
// The thumbnail URL should be a simple field, so we'll output
// the value as a String
OpenSocialField thumbnailUrlField = person.getField("thumbnailUrl");
if (!thumbnailUrlField.isComplex()) {
System.out.println("Thumbnail URL: " +
thumbnailUrlField.getStringValue());
}

System.out.println("----------");

} catch (Exception e) {
System.out.println("Request failed:" );
e.printStackTrace();
}
}
}

Friday, January 09, 2009

OpenSocial REST and WGET: Not So Much

The Open Social REST/RPC documentation (http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html) says vaguely that you can play with the examples using the UNIX wget command, but I don't see it.

The problem is that the URLs have to be signed and carry proper security information. By fooling around with the example code, you can see that the OpenSocialClient class's fetchPerson() method is really just constructing a URL like the one below:

http://sandbox.orkut.com/social/rest/people/08354253340777199997/@self

The number 083542533407771999 is my orkut user ID. You can't just "wget" this URL or put it in your browser, however. Doing so will return the error

HTTP request sent, awaiting response... 401 The request did not have a proper security token nor oauth message and unauthenticated requests are not allowed

Fooling around with the client codes (see previous post) will reveal the actual, signed URL used in the REST operation:

http://sandbox.orkut.com/social/rest/people/08354253340777199
997/@self?oauth_consumer_key=orkut.com%3A623061448914&oauth_nonce=1231537930162003000&oauth_tim
estamp=1231537930&oauth_signature=0h%2FU49KtBplnmnc%2BhDKsDxFPR9k%3D&oauth_signature_method=HMA
C-SHA1&oauth_token=&xoauth_requestor_id=03067092798963641994&oauth_version=1.0

This URL is good for only one invocation (time-stamped).

To reproduce this, make the following change to DisplayProfileInfo.java:

// c.setProperty(OpenSocialClient.Properties.RPC_ENDPOINT,
// "http://sandbox.orkut.com/social/rpc");
c.setProperty(OpenSocialClient.Properties.REST_BASE_URI,
"http://sandbox.orkut.com/social/rest");

and then add the following line to OpenSocialBatch.java's submitRest() method.

OpenSocialRequestSigner.signRequest(request, client);
System.out.println("This is the post-signed REST url: " + request.getUrl().toString());

More soon. It turns out the the base OpenSocialClient.java class is actually just a wrapper around the other classes (OpenSocialRequest and so on). So although the client does not apparently support write operations, you can work around this by constructing the appropriate REST POST operation.

Thursday, January 08, 2009

OpenSocial REST API Is Here

I just noticed the Google-led OpenSocial REST/RPC APIs are finally available. These allow you to build Java, PHP, Python, and Ruby clients that can access and manipulate your social network data.

Blog announcement: http://opensocialapis.blogspot.com/

Java code: http://code.google.com/p/opensocial-java-client/

Orkut Sandbox information: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html

Client Examples
As you might expect, you can use Orkut's sandbox as your "backend" service. The Java code and examples are pretty simple but work. As a bonus, here is a recursive method that you can use in the DisplayProfileData.java example to display all information about a particular person. It will also work in DisplayFriends.java. You'll need to add imports for OpenSocialObject and OpenSocialField.

protected void processFields(OpenSocialObject osObj) throws Exception {
//See all fields
String[] fields=osObj.fieldNames();
for(String fieldName : fields) {
OpenSocialField afield=osObj.getField(fieldName);
if(!afield.isComplex()) {
System.out.println(fieldName+" "+afield.getStringValue());
}
else {
System.out.println(fieldName+" is complex:" );
Collection complexValues=afield.getValues();
for(OpenSocialObject compVal : complexValues) {
processFields(compVal);
}
}
}

Call it anywhere after creating the sample person, like so:

OpenSocialPerson person = c.fetchPerson("03067092798963641994");
...
processFields(person);

You may also want to change the Person ID in the above line. You get this ID when you add sample.xml social gadget to your sandbox. Mine, for example, is 08354253340777199997. This ID does not need to match the viewer ID discussed below.

Here is the sample output:
156-56-104-143:java mpierce$ java -classpath samples/bin:$CP DisplayProfileData
----------
Info. for API DWH
ID: 03067092798963641994
----------
Printing fields
----------
photos is complex:
value http://www.orkut.com/img/i_nophoto64.gif
type thumbnail
thumbnailUrl http://www.orkut.com/img/i_nophoto64.gif
name is complex:
givenName API
familyName DWH
id 03067092798963641994
isOwner false
isViewer true
------Done---------
Thumbnail URL: http://www.orkut.com/img/i_nophoto64.gif
----------

Modifying the Backend
Here's were the problems start. The sample clients above include a default view ID, consumer secret, and consumer key as part of the OAuth infrastructure:

c.setProperty(OpenSocialClient.Properties.CONSUMER_SECRET,
"uynAeXiWTisflWX99KU1D2q5");
c.setProperty(OpenSocialClient.Properties.CONSUMER_KEY,
"orkut.com:623061448914");
c.setProperty(OpenSocialClient.Properties.VIEWER_ID,
"03067092798963641994");

Of course you will want to replace these with your own values. To do this, you will need to deploy a gadget and verify that you own it. The steps are simple:
  1. Write a gadget. You can use http://opensocial-resources.googlecode.com/svn/samples/rest_rpc/sample.xml for example. I'm not sure if the gadget in anyway affects the REST/RPC server side calls that you can use.
  2. Get an Orkut sandbox account and log into sandbox.orkut.com.
  3. Install your application into your Orkut account.
  4. Verify that you own the gadget: https://www.google.com/gadgets/directory/verify
This last step is the problem. It involves two steps. First you provide the URL for your gadget and submit. You will receive a long signature string, which you need to add to your gadget. Replace the one in the sample.xml if you are using a copy of that. Save your gadget and click the verification web site button to complete the process. Presumably this will give you the consumer key and consumer secret that you need, but I have not gotten this to work for some reason.
See
http://groups.google.com/group/opensocial-orkut/browse_thread/thread/43640406754ab7f6