Authenticating against App Engine from an Android app

Many an Android app requires a server backend of some sort, and what better choice than App Engine? It's free, reliable, and does everything you're likely to need in a backend. It has one other major advantage, too: It supports Google Account authentication, and nearly all Android users will already have a Google Account.

So given that we want a backend for our app, and given that we want to have user authentication, how do we go about this? We could prompt the user for their credentials, but that seems less than ideal: the Android device already has their credentials, and users may not trust us with them. Is there a way we can leverage an Android API to take care of authentication? It turns out there is.

Authentication with App Engine, regardless of where you're doing it, is a three-stage process:

  1. Obtain an authentication token. This can be done with ClientLogin for installed apps, for example, or with AuthSub for a webapp. When logging in directly to an application, this is the part of the login process where your user sees a Google signin screen.
  2. Take that authentication token, and use it to obtain an authentication cookie.
  3. Use that authentication cookie in all subsequent requests.

Let's tackle those in order. On Android, step 1 of the process can be accomplished using the android.accounts API, which allows you to list accounts and obtain authentication tokens for them. First, your app is going to need some permissions:

<uses-permission android:name="android.permission.GET_ACCOUNTS"></uses-permission>
<uses-permission android:name="android.permission.USE_CREDENTIALS"></uses-permission>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>

Let's create a simple activity that lets the user select one of their accounts to authenticate with. Here's the source of that activity:

public class AccountList extends ListActivity {
	protected AccountManager accountManager;
	protected Intent intent;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        accountManager = AccountManager.get(getApplicationContext());
        Account[] accounts = accountManager.getAccountsByType("com.google");
        this.setListAdapter(new ArrayAdapter(this, R.layout.list_item, accounts));        
    }

	@Override
	protected void onListItemClick(ListView l, View v, int position, long id) {
		Account account = (Account)getListView().getItemAtPosition(position);
		Intent intent = new Intent(this, AppInfo.class);
		intent.putExtra("account", account);
		startActivity(intent);
	}
}

This should be fairly easy to follow. Our Activity is a ListActivity, meaning it displays as a list view. To populate it, we instantiate a new AccountManager and call getAccountsByType on it, specifying that we want only accounts of type "com.google". When the user clicks on an account, we invoke the "AppInfo" activity, which will be responsible for getting the token and cookie, and displaying some info from our app. Here's the first part of that activity:

public class AppInfo extends Activity {
	DefaultHttpClient http_client = new DefaultHttpClient();

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.app_info);
	}

	@Override
	protected void onResume() {
		super.onResume();
		Intent intent = getIntent();
		AccountManager accountManager = AccountManager.get(getApplicationContext());
		Account account = (Account)intent.getExtras().get("account");
		accountManager.getAuthToken(account, "ah", false, new GetAuthTokenCallback(), null);
	}

In onResume(), we obtain ourselves another AccountManager instance, and call getAuthToken on it. getAuthToken is an asynchronous call, so we have to provide a callback function that will be called when it's done. You may be wondering why we're doing this every time the activity is resumed - this will become clear shortly. Here's the code for the GetAuthTokenCallback:

	private class GetAuthTokenCallback implements AccountManagerCallback {
		public void run(AccountManagerFuture result) {
			Bundle bundle;
			try {
				bundle = result.getResult();
				Intent intent = (Intent)bundle.get(AccountManager.KEY_INTENT);
				if(intent != null) {
					// User input required
					startActivity(intent);
				} else {
					onGetAuthToken(bundle);
				}
			} catch (OperationCanceledException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (AuthenticatorException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	};

Here we can see the reason for our decision to fetch a token every time the activity is resumed: The callback can return the auth token, which we want, or it can return an intent. When it returns an intent, that indicates confirmation from the user is required, and in order to do so, we should invoke the intent. Unfortunately, the intent doesn't provide a result when the user confirms their choice, so we have no option but to check again every time the user returns to our activity.

In the case that prompting was required, we simply start the activity that does the prompting and return. If a prompt wasn't required - for instance, because the user already was prompted and responded in the affirmative - we call the onGetAuthToken() method, shown below:

	protected void onGetAuthToken(Bundle bundle) {
		String auth_token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
		new GetCookieTask().execute(auth_token);
	}

Now that we have the token, we've completed phase one of the authentication process, and it's onGetAuthToken's job to kick off phase 2: trading in our authentication token for a cookie. This is where GetCookieTask comes in:

	private class GetCookieTask extends AsyncTask {
		protected Boolean doInBackground(String... tokens) {
			try {
				// Don't follow redirects
				http_client.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);
				
				HttpGet http_get = new HttpGet("https://yourapp.appspot.com/_ah/login?continue=http://localhost/&auth=" + tokens[0]);
				HttpResponse response;
				response = http_client.execute(http_get);
				if(response.getStatusLine().getStatusCode() != 302)
					// Response should be a redirect
					return false;
				
				for(Cookie cookie : http_client.getCookieStore().getCookies()) {
					if(cookie.getName().equals("ACSID"))
						return true;
				}
			} catch (ClientProtocolException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} finally {
				http_client.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, true);
			}
			return false;
		}
		
		protected void onPostExecute(Boolean result) {
			new AuthenticatedRequestTask().execute("http://yourapp.appspot.com/admin/");
		}
	}

At first glance, this is a substantial chunk of code, but let's take it one line at a time. First, we tell the http client class that we don't want it to follow redirects. This is because the page we'll use to fetch the cookie will set the cookie and return a redirect to the page we specify. Since we just care about the cookie, there's no sense following the redirect.

Next, we construct a new HttpGet object for the URL we get the cookie from. This URL is composed of three parts: Your app's domain, the special path '/_ah/login', and the two query string parameters. The 'continue' parameter specifies where the redirect will be to, while the 'auth' parameter contains our hard-won authentication cookie. Once we've constructed the HttpGet, we execute it against the client.

On getting the response, we check a couple of things: First, did it return a redirect? Second, did it return the cookie we were looking for? If all is well, we return successful. I'm sure I don't need to point out here that the error handling (both here and in the app in general) is rather poor: A real application would, hopefully, do a much better job of it!

Finally, the onPostExecute() method starts the third and final part of the process, that of fetching a URL from our site using our shiny new authentication cookie. Here's the source for the AuthenticatedRequestTask:

	private class AuthenticatedRequestTask extends AsyncTask {
		@Override
		protected HttpResponse doInBackground(String... urls) {
			try {
				HttpGet http_get = new HttpGet(urls[0]);
				return http_client.execute(http_get);
			} catch (ClientProtocolException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			return null;
		}
		
		protected void onPostExecute(HttpResponse result) {
			try {
				BufferedReader reader = new BufferedReader(new InputStreamReader(result.getEntity().getContent()));
				String first_line = reader.readLine();
				Toast.makeText(getApplicationContext(), first_line, Toast.LENGTH_LONG).show();				
			} catch (IllegalStateException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

This should look familiar if you've used the Apache HTTP client library before: It's a perfectly ordinary HTTP request. The cookie is now stored in the HttpClient we're using, so all our requests from here on in require no special tricks. From App Engine's point of view, though, they all come from an authenticated user, using the account the phone's owner selected.

That's all there is to it. One brief caveat: You won't be able to test this on the Android emulator, as the emulator images don't support Google Accounts - you'll need a real device for testing.

The complete source of the app is available here. If you're thinking that a library that neatly wraps this functionality up in an easy to use package would be a good idea, you're quite right - but nobody's gotten around to writing one yet. Perhaps you could be the first.

Got an Android app that you're writing using an App Engine backend, or an idea for one? Let us know in the comments!

Comments

blog comments powered by Disqus