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();