Android Google App Engine Authentication Example

Google App Engine often referred to as GAE is a platform as a service (PaaS) cloud computing platform for developing and hosting web applications in Google-managed data centers. If your android application backend is supported using the GAE then you can take advantage of the Account Manager to handle the authentication. The benefit to the user is that they can use their Google accounts to get authenticated and have access to the secured data for that application.

The following steps are needed to get authenticated with Google App Engine 
  1. Get a list of google accounts stored on the device
  2. Have the user choose which account they would like to use
  3. Request an authentication token
  4. If the response to the token is an Intent then start a new activity to get users permission to use that account otherwise save the token for the next step
  5. Use the token to request an auth cookie
  6. All done! Now you can use the auth cookie to make authenticated requests

Android Manifest

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.as400samplecode"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="15"
        android:targetSdkVersion="15" />
    
 <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>
    
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/title_activity_main" 
            android:launchMode="standard"
            android:multiprocess="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Application Layout - activity_main.xml

<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
 android:layout_height="match_parent">

 <TextView android:id="@+id/textView1" android:layout_width="wrap_content"
  android:layout_height="wrap_content" android:layout_alignParentLeft="true"
  android:layout_alignParentTop="true" android:layout_marginBottom="10dp"
  android:text="@string/choose_account" android:textAppearance="?android:attr/textAppearanceMedium"
  android:textStyle="bold" />

 <Spinner android:id="@+id/account" android:layout_width="match_parent"
  android:layout_height="wrap_content" android:layout_alignParentLeft="true"
  android:layout_below="@+id/textView1" />

 <Button android:id="@+id/startAuth" android:layout_width="wrap_content"
  android:layout_height="wrap_content" android:layout_alignParentLeft="true"
  android:layout_below="@+id/account" android:layout_marginTop="10dp"
  android:text="@string/start_auth" />

 <Button android:id="@+id/expireToken" android:layout_width="wrap_content"
  android:layout_height="wrap_content" android:layout_alignParentLeft="true"
  android:layout_below="@+id/startAuth" android:layout_marginTop="10dp"
  android:text="@string/expire_token" />

</RelativeLayout>

Application Activity - MainActivity.java

package com.as400samplecode;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpParams;

import android.os.AsyncTask;
import android.os.Bundle;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.content.Intent;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.Toast;

public class MainActivity extends Activity implements OnClickListener{

 private String LOG_TAG = "GAEAuthMainActivity";
 private static final int USER_PERMISSION = 989;

 private AccountManager accountManager;
 private Account[] accounts;
 boolean expireToken = false;
 
 private DefaultHttpClient httpclient = new DefaultHttpClient();
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  Spinner spinner = (Spinner) findViewById(R.id.account);
  //create an ArrayAdaptar from the String Array
  ArrayAdapter<String> dataAdapter = new ArrayAdapter<String>(this,
    android.R.layout.simple_spinner_item, getAccountList());
  //set the view for the Drop down list
  dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  //set the ArrayAdapter to the spinner
  spinner.setAdapter(dataAdapter);

  Button startAuth = (Button) findViewById(R.id.startAuth);
  startAuth.setOnClickListener(this);
  Button expireToken = (Button) findViewById(R.id.expireToken);
  expireToken.setOnClickListener(this);
 }

 public void onClick(View v) {

  Spinner spinner = (Spinner) findViewById(R.id.account);
  Account account = accounts[spinner.getSelectedItemPosition()];

  switch (v.getId()) {
  
  //send request to get a new or existing token
  case R.id.startAuth:

   expireToken = false;
   accountManager.getAuthToken(account, "ah", null, false, new OnTokenAcquired(), null);
   break;

  //send request to expire a token 
  case R.id.expireToken:

   expireToken = true;
   accountManager.getAuthToken(account, "ah", null, false, new OnTokenAcquired(), null);
   break;

   // More buttons go here (if any) ...

  }
 }

 protected void onActivityResult(int requestCode, int resultCode, Intent data)
 {
  switch(requestCode) {
  
  //if the user approved the use of the account make another request 
  //for the auth token else display a message
  case USER_PERMISSION: 
   if (resultCode == RESULT_OK) {
    Spinner spinner = (Spinner) findViewById(R.id.account);
    Account account = accounts[spinner.getSelectedItemPosition()];
    accountManager.getAuthToken(account, "ah", null, false, new OnTokenAcquired(), null);
   }
   else if(resultCode ==  RESULT_CANCELED){
    Toast.makeText(getBaseContext(), "Permission denied by User!", 
      Toast.LENGTH_LONG).show();
   }
   break;
  }

 }   


 // get a list of all Google accounts stored in the device account manager
 // and then display them in a spinner for selection
 private ArrayList<String> getAccountList(){

  ArrayList<String> accountList = new ArrayList<String>();

  accountManager = AccountManager.get(getApplicationContext());
  accounts = accountManager.getAccountsByType("com.google");
  for(Account account: accounts){
   Log.v(LOG_TAG, account.name + "/" + account.type);
   accountList.add(account.name);
  }

  return accountList;
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  getMenuInflater().inflate(R.menu.activity_main, menu);
  return true;
 }

 //the result for the auth token request is returned to your application 
 //via the Account Manager Callback you specified when making the request.
 //check the returned bundle if an Intent is stored against the AccountManager.KEY_INTENT key.
 //if there is an Intent then start the activity using that intent to ask for user permission
 //otherwise you can retrieve the auth token from the bundle.
 private class OnTokenAcquired implements AccountManagerCallback<Bundle> {

  public void run(AccountManagerFuture<Bundle>  result) {

   Bundle bundle;

   try {
    bundle = (Bundle) result.getResult();
    if (bundle.containsKey(AccountManager.KEY_INTENT)) {
     Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
     intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
     startActivityForResult(intent, USER_PERMISSION);
    } else {
     setAuthToken(bundle);
    }
   }
   catch(Exception e){
    e.printStackTrace();
   }
  }
 };

 //using the auth token and ask for a auth cookie
 protected void setAuthToken(Bundle bundle) {
  String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
  if(expireToken){
   accountManager.invalidateAuthToken("ah", authToken);
  }
  else {
   new GetCookie().execute(authToken);
  }
 }

 //using the token to get an auth cookie
 private class GetCookie extends AsyncTask<String, Void, Boolean> {
  
  HttpParams params = httpclient.getParams();
  private HttpResponse response;
  
  
  protected Boolean doInBackground(String... tokens) {

   try {

    // Don't follow redirects
    params.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);

    HttpGet httpGet = new HttpGet("http://{replace_your_subdomain}.appspot.com/_ah/login?continue=http://localhost/&auth=" + tokens[0]);
    response = httpclient.execute(httpGet);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    response.getEntity().writeTo(out);
    out.close();
    
    if(response.getStatusLine().getStatusCode() != 302){
     // Response should be a redirect
     Log.v(LOG_TAG, "No cookie");
     return false;
    }

    //check if we received the ACSID or the SACSID cookie, depends on http or https request
    for(Cookie cookie : httpclient.getCookieStore().getCookies()) {
     Log.v(LOG_TAG, cookie.getName());
     if(cookie.getName().equals("ACSID") || cookie.getName().equals("SACSID")){
      Log.v(LOG_TAG, "Got cookie");
      return true;
     } 
    }

   } catch (ClientProtocolException e) {
    e.printStackTrace();
    cancel(true);
   } catch (IOException e) {
    e.printStackTrace();
    cancel(true);
   } catch (Exception e) {
    e.printStackTrace();
    cancel(true);
   } finally {
    params.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, true);
   }
   return false;
  }

  protected void onPostExecute(Boolean result) {
   Log.v(LOG_TAG, "Done cookie");
   new MyAuthenticatedRequest().execute("http://{replace_your_subdomain}.appspot.com/hello");
  }
 }

 //make your authenticated request here using the same httpclient that received the cookie
 private class MyAuthenticatedRequest extends AsyncTask<String, Void, Boolean> {
  
  
  private HttpResponse response;
  private String content =  null;

  protected Boolean doInBackground(String... urls) {

   try {

    HttpGet httpGet = new HttpGet(urls[0]);
    response = httpclient.execute(httpGet);
    StatusLine statusLine = response.getStatusLine();
    Log.v(LOG_TAG, statusLine.getReasonPhrase());
    for(Cookie cookie : httpclient.getCookieStore().getCookies()) {
     Log.v(LOG_TAG, cookie.getName());
    }
    if(statusLine.getStatusCode() == HttpStatus.SC_OK){
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     response.getEntity().writeTo(out);
     out.close();
     content = out.toString();
     return true;
    }
   } catch (ClientProtocolException e) {
    e.printStackTrace();
    cancel(true);
   } catch (IOException e) {
    e.printStackTrace();
    cancel(true);
   } catch (Exception e) {
    e.printStackTrace();
    cancel(true);
   } 
   return false;
  }

  //display the response from the request above 
  protected void onPostExecute(Boolean result) {
   Log.v(LOG_TAG, content);
   Toast.makeText(getBaseContext(), "Response from request: " + content, 
     Toast.LENGTH_LONG).show();
  }
 }
}

3 comments :

  1. How does the GAE side look? which security constrains should one add in the web.xml
    Or
    How can the User object be retrieved on the GAE side?

    ReplyDelete
  2. they can use their Search engines records to get authenticated and have access the properly secured information for that program.

    buy rs gold
    FF14 Gil

    ReplyDelete