I recently had to do an Android app, that needed to talk to a Rails powered JSON API. After reading a lot about different ways of securing the Rails API and doing authentication/authorization, I decided to go with OAuth for authentication and a token-based approach for authorization.

Since I was doing Android, I figured the easiest way to do OAuth was to use Google OAuth2, but it still turned out to be a bit of a challenge, so this post will hopefully give inspiration to others that need to achieve the same.

Table of contents

Google OAuth2 in general

The first thing to pay special attention to, is the actual steps that are involved in the authentication process. I had to go through the documentation a couple of times before I could understand what was actually being communicated between the OAuth2 server, the Android app and the Rails server.

The general flow for authenticating and obtaining user information looks like this:

Sequency diagram of communication according to Google

Token request

The token returned by the Google OAuth Autorization Server is used to validate requests for a users information from the Google Userinfo Service afterwards. It has nothing to do with the token used for our token-based authorization, securing the Rails server's API (to limit the confusion, I'll call that token an API key)

Obtaining user information

The token request is done from the Android application, but it's the Rails server that stores information about users. Therefore I need to send the token to the Rails server, so the server can send a valid request directly to the Google Userinfo Service and obtain the user information.

The Rails server then uses the Google user information to find a corresponding User record.

  • If a User record exists, it will retrieve the users API key from the database and return it to the Android application.
  • If no User record was found, it will create a new one from the user information, generate a new API key for the new user and return it to the Android application.

With the user-specific API key, the Android application can now do authorized requests to our Rails server.

So in my case

The sequence looks like this:

Sequence diagram of actual communication

The Android part

Note: I've created a GitHub repo: Jachobsen/google_oauth2_android_example with the complete Android project to make it easier for you to see all the details.

Manifest

First off, you'll need to add the following permissions to the manifest:

In AndroidManifest.xml

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

Activities

Based on your needs, there are a lot of ways to handle login in the UI. To keep things simple, I just start a GoogleLoginActivity containing a button for signing in with Google. When the user hits the button, it will use the phone's default google account to log in. If the user has more than one google account set up, a DialogFragment will show a list of the google accounts, so the user can choose which one to use. Once the user successfully logs in, the activity goes away and reveals MainActivity.

Screenshot of LoginActivity

I expect that you know how to write the XML for such a simple layout, so I won't include it in the post. You can also find the code in the sample repo.

Flow overview

This is a diagram of the flow for obtaining the token in Android:

AccountManager flow diagram

Get account

First step is to add the button event listener.

In GoogleLoginActivity.java:

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

    findViewById(R.id.google_login_button).setOnClickListener(
        new View.OnClickListener() {

          @Override
          public void onClick(View view) {
            selectAccount();
          }
        });
  }

and for the selectAccount() method:

private AccountManager mAccountManager;

private void selectAccount() {
  mAccountManager = AccountManager.get(GoogleLoginActivity.this);
  Account[] accounts = mAccountManager.getAccountsByType("com.google");

  if (accounts.length == 0) {
    Toast.makeText(getApplicationContext(), R.string.no_account_found_message, Toast.LENGTH_SHORT).show();
  } else if (accounts.length == 1) {
    getTokenForAccount(accounts[0]);
  } else {
    String[] accountNames = new String[accounts.length];

    for (int i = 0; i < accounts.length; i++) {
      accountNames[i] = accounts[i].name;
    }

    DialogFragment newFragment = SelectAccountDialogFragment.newInstance(R.string.select_account_dialog_title, accountNames);
    newFragment.show(getFragmentManager(), "dialog");
  }
}

The key thing here is the AccountManager which has information about accounts stored on the phone. To get Google accounts I use getAccountsByType("com.google") - the identifier "com.google" would of course be different if I wanted twitter accounts etc.

Depending on how many accounts the manager returns, I either show error message, use the single account found or show a custom SelectAccountDialogFragment to let the user choose which Google account to use.

In case of multiple accounts, I extract the account names (for Google accounts that means the e-mails) and send them to the fragment's newInstance() method.

In the SelectAccountDialogFragment, I create a protocol to message the activity when an account was picked. The account names are shown in the same order as the AccountManager, so I can just use the index of the selected account name.

Choosing between multiple accounts

In SelectAccountDialogFragment.java

private SelectAccountDialogProtocol mDelegate = null;

public interface SelectAccountDialogProtocol {
  public void gotAccount(int index);
}

So the only thing needed is to create an AlertDialog which displays the account names:

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
  int title = getArguments().getInt("title");
  String[] accountNames = getArguments().getStringArray("account_names");

  return new AlertDialog.Builder(getActivity())
                     .setTitle(title)
                     .setItems(accountNames, new DialogInterface.OnClickListener() {
                          public void onClick(DialogInterface dialog, int which) {
                            mDelegate.gotAccount(which);
                            }
                          })
                     .create();
}

and regular delegate handling in the onAttach() and onDetach():

@Override
public void onAttach(Activity activity) {
  try {
    mDelegate = (SelectAccountDialogProtocol)activity;
  } catch(ClassCastException e) {}

  super.onAttach(activity);
}

@Override
public void onDetach() {
  mDelegate = null;
  super.onDetach();
}

Back in the GoogleLoginActivity the protocol is implemented like this:

public class GoogleLoginActivity extends Activity implements SelectAccountDialogProtocol {

public void gotAccount(int index) {
  Account[] accounts = mAccountManager.getAccountsByType("com.google");
  getTokenForAccount(accounts[index]);
}

Get auth token

The getTokenForAccount() does the call to Google to get the auth token.

In GoogleLoginActivity.java:

private void getTokenForAccount(Account account) {
  Bundle options = new Bundle();

  mAccountManager.getAuthToken(
      account,
      "oauth2:https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
      options,
      this,
      new OnTokenAcquired(),
      null
      );
}

the second argument determines the scope, which means what information I will be able to get from the Google Userinfo Service when that call is made from the Rails server later. The OAuth 2.0 Playground is a great place to discover which scopes are available if different info is needed in your case.

The callback OnTokenAcquired() is called when the auth server responds.

In GoogleLoginActivity.java:

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
  @Override
  public void run(AccountManagerFuture<Bundle> result) {

    Bundle bundle = null;
    try {
      bundle = result.getResult();

      String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
      Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT);

      if (launch != null) {
        startActivityForResult(launch, 0);
        return;
      }

      requestAPIKeyFromOAuthToken(token);
    } catch (OperationCanceledException e) {
      e.printStackTrace();
    } catch (AuthenticatorException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }

  }
}

In the callback the token is retrieved from the bundle via AccountManager.KEY_AUTHTOKEN. The first time the app does a token request, an activity comes up automatically where the user has to grant the app access to the info at Google. Depending on what scope you've set, the information in the dialog is different.

Screenshot of permission request

If the user chooses "Allow access", they won't see the dialog again.

Get API key from Rails server

The last step is to send the auth token to the Rails server in exchange for the API key.

I found James Smith's amazing Android library called Android Asynchronous Http Client that simplifies HTTP requests in Android significantly. If you're not using it already, it's definitely worth a try!

So all that's needed is to make a POST request with the auth token and the Rails server should respond with an API key.

In GoogleLoginActivity.java:

private void requestAPIKeyFromOAuthToken(String token) {
  AsyncHttpClient client = new AsyncHttpClient();
  RequestParams params = new RequestParams("token", token);

  client.post("https://myrailsapp.com/api/v1/auth/verify", params, new AsyncHttpResponseHandler() {
    @Override
    public void onSuccess(String response) {
      Log.w("OAUTH", "Got API key from server: " + response);
      finish();
    }

    @Override
    public void onFailure(Throwable e) {
      Log.w("OAUTH", "Error getting API key");
      Toast.makeText(getApplicationContext(), R.string.connection_error_message, Toast.LENGTH_LONG).show();

      e.printStackTrace();
    }
  });
}

Keeping login state

I'm sure there's better and more secure ways of keeping information about if the user is logged in, but to keep it simple right now, one suggestion is to save the API key in Application Preferences. Then add a logout button, which just clears the API key from the Application Preferences. On startup you can then do a check to see if the API key is present, and if not, present the GoogleLoginActivity.

Another interesting thing could be to change the API end-point so that it returns the User object as JSON instead of just the API key.

The Rails part

I've followed RailsCasts #352 on securing and API. Ryan Bates does an excellent job at explaining things, so I won't go into detail about the ApiKey model or the restrict_access before_filter.

Controller

First I created a controller with an action that gets the user information from the Google Userinfo Service using the auth token, then finds (or creates) the user and returns the user's API key.

I named the controller OmniauthVerificationsController and used the gem HTTParty for making requests to the Google Userinfo Service.

In /controllers/api/v1/omniauth_verifications_controller.rb:

class Api::V1::OmniauthVerificationsController < Api::BaseController
  skip_before_filter :restrict_access

  respond_to :json

  def verify_token
    render status: :forbidden unless params[:token]

    token = params[:token]
    response = HTTParty.get("https://www.googleapis.com/oauth2/v2/userinfo",
                            headers: {"Access_token"  => token,
                                      "Authorization" => "OAuth #{token}"})

    if response.code == 200
      data = JSON.parse(response.body)
      @user = User.find_for_verfied_token_response(data.symbolize_keys)
    end

    render :json => @user.api_key.to_json || {}
  end

  private

  def request_error
    render status: :forbidden
  end
end

Obviously, I need to skip the authorization before_filter since the Android application doesn't have the API key when hitting this end-point.

This is also where you could decide to change what is returned to the Android application - maybe the full user object instead of just the API key (as previously described).

Model

Based on Railscasts #241, the User record has a :uid and :provider to store OAuth2 information.

The method for handling the response data in the User model looks like this:

In models/user.rb:

def self.find_for_verfied_token_response(auth)
  user = User.where(:provider => "google_oauth2", :uid => auth[:id]).first

  unless user
    user = User.create(:name => auth[:name],
                       :email => auth[:email],
                       :provider => "google_oauth2",
                       :uid => auth[:id])
    user.api_key = ApiKey.create!
  end

  user
end

The information returned by Google Userinfo Service includes the unique Google user id, which is saved as :uid.

In the case of a new user, I generate a new API key for the user.

Routes

The last step is to setup a route for the end-point so the Android application can POST the token.

In routes.rb:

namespace :api, defaults: {format: :json} do
  namespace :v1 do
    post 'auth/verify' => 'omniauth_verifications#verify_token'
  end
end

That's pretty much it for the Rails part! (or at least the OAuth2 relevant stuff).

Resources