Wednesday, December 23, 2009

Authenticating an Android App to Google App Engine.

8/23/2010 update: This is a little out of date now. It works, but it makes you get a user id and password from your user. A lot of this code is now obsolete if you use account sync functions on android 2.2 and higher where you can get an auth token using the phone's credentials.

I posted an update to this article that has the code for using the phone's credentials here: http://javagwt.blogspot.com/2010/08/authenticating-android-to-app-engine.html



So you have a google app engine project running on myapp.appspot.com with all of your users data, and REST web services available so other apps can download xml from it over http - but now you want to download that XML to an app running on an android based phone AND provide google account credentials so users will download their data just like they're logged into your app.


I spent the last two days trying to put together samples of how to do this from all over the web. The challenge here is to have an Android Application call REST web services hosted on Google App Engine but to provide login account credentials so the App Engine code considers the request as logged in. If that makes sense to you, then you've come to the right place. I'm not sure if this is the most secure way to work with your app data, so please continue at your own risk. No warranty etc!

There are quite a few other people out there blogging about the same thing here, i found that each one of their posts were missing one critical piece of information. I'm posting here a class that is as straight forward as possible, you create an object with a username and password and from there you can load a URL from app engine with the authentication credentials you need.

The act of making http requests to an app engine site and providing credentials along with the request is to first get an auth token from google, which is a temporary string, then passing that token to the login page of your app, which in turn gives you an authentication cookie. Then add that cookie to the header of all of your future http requests.

Sounds simple enough....

I did learn that if you try to run this code using the android SDK in a plain java application with a reference to the SDK in your build path, you will get an error: Stub! whatever that means - what it means is that code using the android SDK needs to be ran in an android emulator.

I want to post the entire class for this in one piece, so please find it all at the bottom of this post. You can Create a new Android Project In eclipse, add a class called GoogleAuthentication and paste what i have below verbatim. Change the final string gaeAppBaseUrl at the top to point to your own app url yourapp.appspot.com

when you create an instance of the class, you pass the email and password to the constructor which gets the authkey and cookie. Sample of that is at the bottom of this post.

The GetLoggedIn() function is the only function here you don't need, but is an example of calling a REST web service URL that corosponds to a servelet running this code on the App Engine program Nimbits Data Historian.  When you load the url  http://app.nimbits.com/nimbits/Service/user  it returns false because you are not logged in when this code fires on the app engine:

public static User getUser() {
com.google.appengine.api.users.UserService u = UserServiceFactory.getUserService();
return u.getCurrentUser();
}

public static void checkLoggedIn() throws NotLoggedInException {
if (getUser() == null) {
throw new NotLoggedInException("False");
}
}


Our mission is to provide make the http request provide the header cookie so the above call returns true, and we have access to the serServiceFactory.getUserService();

If you look at the GetLoggedIn() function below, you'll see how i'm making the http request to that URL but adding the auth cookie needed in the header of the request that was created on contruction of the object.

Ok, here is the code:




import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.DefaultHttpClient;

public class GoogleAuthentication {
private final String gaeAppBaseUrl = "http://nimbits1.appspot.com/";
private final String gaeAppLoginUrl = gaeAppBaseUrl + "_ah/login";
private final String googleLoginUrl = "https://www.google.com/accounts/ClientLogin";
private final String service = "ah";
private Cookie authCookie = null;


//sample function that makes an http request and downloads
//the result providing the auth cookie.
public String GetLoggedIn() throws IOException
{

String result;
String destUrl = "http://app.nimbits.com/nimbits/Service/user";
URL url = new URL(destUrl);
URLConnection conn = url.openConnection ();
conn.addRequestProperty("Cookie",authCookie.getName() + "=" + authCookie.getValue());
BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = rd.readLine()) != null)
{
sb.append(line);
}
rd.close();
result = sb.toString();
return result;
}

//Constructor creates the cookie
public GoogleAuthentication(String googleAccount, String googlePassword)
{
if (authCookie == null)
{

try {
String authToken = GetToken(googleAccount,googlePassword);
authCookie = getAuthCookie(authToken);
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}


private String GetToken(String googleAccount, String googlePassword) throws MalformedURLException, ProtocolException, UnsupportedEncodingException, IOException
{
String token = null;
HttpURLConnection h = GetConnection(googleAccount,googlePassword);
token= extractAuthTokenFromResponse(h);
return token;
}


private Cookie getAuthCookie(String authToken) throws ClientProtocolException, IOException {
DefaultHttpClient httpClient = new DefaultHttpClient();
Cookie retObj = null;
String cookieUrl = gaeAppLoginUrl + "?continue="
+ URLEncoder.encode(gaeAppBaseUrl,"UTF-8") + "&auth=" + URLEncoder.encode
(authToken,"UTF-8");
HttpGet httpget = new HttpGet(cookieUrl);
HttpResponse response = httpClient.execute(httpget);
if (response.getStatusLine().getStatusCode() == HttpURLConnection.HTTP_OK ||
response.getStatusLine().getStatusCode() == HttpURLConnection.HTTP_NO_CONTENT) {

if (httpClient.getCookieStore().getCookies().size() > 0) {
retObj= httpClient.getCookieStore().getCookies().get(0);
}

}
return retObj;

}

private HttpURLConnection GetConnection(String username, String password)
throws MalformedURLException, IOException, ProtocolException,
UnsupportedEncodingException {
HttpURLConnection urlConnection;
URL url = new URL(googleLoginUrl);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setDoInput(true);
urlConnection.setDoOutput(true);
urlConnection.setUseCaches(false);
urlConnection.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
StringBuilder content = new StringBuilder();
content.append("Email=").append(username);
content.append("&Passwd=").append(password);
content.append("&service=").append(service);

OutputStream outputStream = urlConnection.getOutputStream();
outputStream.write(content.toString().getBytes("UTF-8"));
outputStream.close();
return urlConnection;
}
private String extractAuthTokenFromResponse(HttpURLConnection urlConnection)
throws IOException {
int responseCode = urlConnection.getResponseCode();
System.out.println(responseCode);
StringBuffer resp = new StringBuffer();
if (responseCode == HttpURLConnection.HTTP_OK) {
InputStream inputStream = urlConnection.getInputStream();

BufferedReader rd = new BufferedReader(new InputStreamReader(inputStream));
String line;


while ((line = rd.readLine()) != null) {

if(line.startsWith("Auth="))
{
resp.append(line.substring(5));

}

}

rd.close();


}
return resp.toString();
}
}


here is is the code in the main android class that makes the request and displays the result on the screen



public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String result = null;
TextView tv = new TextView(this);
GoogleAuthentication G;
try {
G = new GoogleAuthentication("email@gmail.com", "secretpassword");
result = G.GetLoggedIn();
} catch (IOException e) {
result = e.getMessage();
}

tv.setText(result);

setContentView(tv);
}


and voila

6 comments:

  1. Thanks for this post, Ben. Just getting to it now. The way I see it, you can either get an authToken remotely, as you are doing. Or you can get an authToken on the phone, using AccountManager. Am I right about this?

    The downside of doing it remotely is that you have to code in your password. Unless there is some way to avoid putting your password in the clear in your code. What I believe is that if you use AccountManager, you can access your password without putting it in the clear. So it seems that generating it locally is a better bet.

    ReplyDelete
  2. Hi,

    First thanks for great tutorial of login.
    I go over on many manuals, blogs,etc how to make call from Android to App Engine with google auth.
    The problem is i am using a http://www.restlet.org/ framework.

    And i really can't understand how can i use your example. :(
    How do i merge all example in one working application.

    Meaning:
    1. Do login and get cookie
    2. Use the cookie in my remote call App Engine from Android client using reslet framework.

    ReplyDelete
  3. @fickas: yes, this post is getting a little out of date. Since Android 2.2 i'm using the account manager permission to get the cookies i need without the user having to give my app their credentials.

    @vogash: I'm using reslet too, you the example i have here should do a post and get to a rest web service and app engine will consider it an authenticated request. However, check out the above comment - i'm going to see if I can post an example with account sync soon.

    ReplyDelete
  4. New posting on using the phone's credentials with source: http://javagwt.blogspot.com/2010/08/authenticating-android-to-app-engine.html

    ReplyDelete
  5. Android is one of the fastest moving mobile application due to its incredible features.I like your blog creativity.
    Android app developers

    ReplyDelete
  6. I am very thankful to all your team for sharing such inspirational information.
    one click root download

    ReplyDelete