Thursday, January 14, 2010

Accessing a security-enabled Google App Engine service from a Java client

After a rather long search on Google pages and forums I could only find fragmented information how to programmatically access a Google App Engine service that requires users to authenticate. In this blog post I'm going to summarize my findings for a Java client application.

With programmatic access I mean that the user doesn't need to enter username and password into a login form created by Google but rather into an installed client application and the client coordinates the authentication and authorization process programmatically. The mechanism used here is the ClientLogin for installed applications.

The first step is to obtain an authentication token from the Google Accounts API. The easiest way to do that is with the GData client library for Java.

import java.net.URLEncoder;

import com.google.gdata.client.GoogleAuthTokenFactory;
import com.google.gdata.util.AuthenticationException;

public class AuthExample {

public static void main(String[] args) throws Exception {

String username = "myusername@gmail.com";
String password = "mypassword";
String serviceName = "ah";

GoogleAuthTokenFactory factory = new GoogleAuthTokenFactory(serviceName, "", null);
// Obtain authentication token from Google Accounts
String token = factory.getAuthToken(username, password, null, null, serviceName, "");

...
}
}

One has to provide username an password and the name of the Google service that should be accessed. For Google App Engine the service name is always ah, regardless of the name of the deployed application. The next step is to do a login at Google App Engine. The login URL is https://example.appspot.com/_ah/login?continue=https%3A%2F%2Fexample.appspot.com%2Fexample&auth=DQAAAJc...qNUA8. The continue query parameter instructs the login service where to rederict after successful login. In this example the redirect goes to https://example.appspot.com/example. The auth query parameter contains the authentication token obtained before.

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

public class AuthExample {

public static void main(String[] args) throws Exception {
...

String token = ...
String serviceUrl = "https://example.appspot.com/example";
String loginUrl = "https://example.appspot.com/_ah/login?continue=" +
URLEncoder.encode(serviceUrl, "UTF-8") + "&auth=" + token;

HttpClient httpclient = new DefaultHttpClient();
HttpGet httpget = new HttpGet(loginUrl);
HttpResponse response = httpclient.execute(httpget);
// process response
// ...

httpclient.getConnectionManager().shutdown();
}
}

When the login service sends a redirect after successful login, it also returns a cookie that allows the client to finally access the protected App Engine service at https://example.appspot.com/example. The redirect and cookie handling is done by the httpclient automatically. For the duration of the session the protected App Engine service can be accessed with that cookie.

Update: If the service expects POST requests instead of GET requests then an automated redirect is not an option. In this case, redirect must be disabled for the for the httpclient and a POST request to the serviceUrl must be created manually. Also, the authorization cookie must be set explicitly.

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.impl.client.DefaultHttpClient;

public class AuthExample {

public static void main(String[] args) throws Exception {
...

String token = ...
String loginUrl = "https://example.appspot.com/_ah/login?auth=" + token;
String serviceUrl = "https://example.appspot.com/example";

HttpClient httpclient = new DefaultHttpClient();
httpclient.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);
HttpGet httpget = new HttpGet(loginUrl);
HttpResponse response = httpclient.execute(httpget);
// Get cookie returned from login service
Header[] headers = response.getHeaders("Set-Cookie");
httpclient.getConnectionManager().shutdown();

httpclient = new DefaultHttpClient();
HttpPost httppost = new HttpPost(serviceUrl);
// set cookie returned by login service
for (Header header : headers) {
httppost.addHeader("Cookie", header.getValue());
}
// set request entity body
// ...

response = httpclient.execute(httppost);
// process response
// ...

httpclient.getConnectionManager().shutdown();
}
}
Update: Login to a local development server. To get access to a security-enabled application on the local development server there's no need for getting an authentication token. Instead, POST an email address and a redirect URL to http://localhost:<port>/_ah/login and the server returns an authorization cookie. Here's an example:
HttpClient httpClient = new DefaultHttpClient();
httpClient.getParams().setBooleanParameter(
ClientPNames.HANDLE_REDIRECTS, false);
// POST login data to GAE SDK dev server
HttpPost httpPost = new HttpPost(
"http://localhost:8888/_ah/login");
httpPost.setHeader("Content-Type",
"application/x-www-form-urlencoded");
String email = URLEncoder.encode(
"test@example.com", "UTF-8");
String redirectUrl = URLEncoder.encode(
"http://localhost:8888", "UTF-8");
httpPost.setEntity(new StringEntity(
"email=" + email + "&continue=" + redirectUrl));
HttpResponse response = httpClient.execute(httpPost);
// Extract authorization cookie from response
String cookie = response.getFirstHeader("Set-Cookie").getValue();
httpClient.getConnectionManager().shutdown();
// Create a new client and access the secured
// service with the authorization cookie
httpClient = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://localhost:8888");
httpget.addHeader("Cookie", cookie);
response = httpClient.execute(httpget);
System.out.println(IOUtils.toString(response.getEntity().getContent()));
httpClient.getConnectionManager().shutdown();

11 comments:

  1. Martin, Thanks a lot for this. I'm trying to access a service I'm setting up on AppEngine & this was invaluable. I'm using HttpClient 2.1 which seems somewhat different to yours though...

    ReplyDelete
  2. Thanks for your work, Martin. I don't think this approach works for the SDK test server -- do you know what to do for that? I need to be able to do a client login to the test server, too...

    ReplyDelete
  3. Sean,

    I justed updated the blog post with an example how to login to a local development server. Hope that works for you.

    ReplyDelete
  4. Is there any way to make it so I don't have to keep adding
    httpget.addHeader("Cookie", cookie);
    to each request when I am using the dev server?

    ReplyDelete
  5. @Jon, refer to http://hc.apache.org/httpcomponents-client-4.0.1/tutorial/html/statemgmt.html, the State Management chapter in the HttpClient documentation.

    ReplyDelete
  6. Just wondering how to do a 'Log Out' or this can only be done with 'AuthSub' ?

    ReplyDelete
  7. Will answer myself: ClientLogin tokens cannot be revoked (one downside of using it)

    http://www.google.com/support/forum/p/apps-apis/thread?tid=75abfb5b924b3eab&hl=en

    ReplyDelete
  8. Great post.

    One addition for " Login to a local development server." section:

    Add [...]&isAdmin=on[...] to login as a admin user against local dev server.

    example:
    [...]
    httpPost.setEntity(new StringEntity(
    "email=" + email + "&continue=" + redirectUrl + "&isAdmin=on"));
    [...]

    ReplyDelete
  9. Hi , I am trying to work on this tutorial. May I please know which is the relevant jar file that I need to import from GData client library for Java to access GoogleAuthTokenFactory.

    ReplyDelete