Android Authenticators I

By | 2013-06-18

I found a blog that gave an example of how to make a custom authenticator for Android. I didn’t find it very clear, so this article covers my understanding that I’ve pulled this together from the example given by that blog author, and from the Android documentation, and then building something that works.

To make a custom account type available in Android’s account page, your app must supply an authenticator <service> in your AndroidManifest.xml that includes the following intent filter:

    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator" />
    </intent-filter>

Which in turn will need the following permission:

    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />

When the OS finds support for this Intent in an app, it will then go looking for information about how it should display it on its settings screen. That’s done by including the following in that same <service> section:

    <meta-data
        android:name="android.accounts.AccountAuthenticator"
        android:resource="@xml/authenticator" />

The actual account description is then in the XML resource file referenced in this meta-data section, res/xml/authenticator.xml in this case. That file contains the actual help that the OS settings module needs:

    <?xml version="1.0" encoding="utf-8"?>
    <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
        android:accountPreferences="@xml/authenticator_preferences"
        android:accountType="@string/authenticator_account_type"
        android:label="@string/authenticator_label"
        android:icon="@drawable/ic_launcher"
        android:smallIcon="@drawable/ic_launcher" />

The most important setting above is the android:accountType attribute. That’s the unique name that causes your interface rather than any other (Google, Facebook, or Twitter, say) to be used when the user picks this type when they click “Add Account” – it’s best defined as a resource reference because you’ll want it available in the code later.

Note that we’ve also referred to another resource, res/xml/authenticator_preferences.xml. This file defines the user interface for configuring the authentication service as a whole – this is not the same thing as configuring an individual account, this is settings for the account type.

    <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
        <PreferenceCategory android:title="FussyLogic Authenticator" />
        <CheckBoxPreference
            android:key="isDebug"
            android:summary="Connecting to a debug server instead of production server"
            android:title="Use debug server" />
        <SwitchPreference
            android:key="logsVerbose"
            android:summary="Increase logging verbosity for LogCat output"
            android:title="Debug Logs" />
    </PreferenceScreen>

These are just examples, you could use whatever you want. I’m missing one piece of information however – how you access these settings. I’d be delighted if someone more knowledgeable can tell me, as the Android documentation is lacking. It certainly seems possible, the Skype app does it.

The presence of the Service, and the above settings are enough to make Android offer your authenticator when the user adds an account. The authenticator Service code itself is short:

    package uk.co.fussylogic.accounttester;
    
    import android.app.Service;
    import android.content.Intent;
    import android.os.IBinder;
    
    public class AuthenticatorService extends Service {
        @Override
        public IBinder onBind(Intent intent) {
            if (intent.getAction().equals(
                    android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT))
                return null;
            
            AbstractAccountAuthenticator authenticator =
                new FussyLogicAuthenticator(this);
            return authenticator.getIBinder();
        }

        // --------------------------------

        public class FussyLogicAuthenticator extends AbstractAccountAuthenticator {
            protected Context mContext;

            public FussyLogicAuthenticator(Context context) {
                super(context);
                this.mContext = context;
            }

            // ... implement abstract methods ...
        };
    }

Android calls onBind() in a Service when another component wants to connect to it for a dialogue (this is in contrast to onStartCommand(), which is used when the service only undertakes actions, it doesn’t return answers). The IBinder class defines the RPC interface – that is, it describes the functions that may be called by the binding client. This is the case for all bound services. In the particular case of an authenticator service, the IBinder class must be one that describes the calls that the Android accounts system requires. Fortunately, we needn’t worry about creating an appropriate IBinder, Android can provide an appropriate IBinder using its AbstractAccountAuthenticator class, which you can see above as the call to FussyLogicAuthenticator.getIBinder() – that is a child class of AbstractAccountAuthenticator, which, being abstract, we have to subclass to provide a concrete implementation, FussyLogicAuthenticator. There are a number of abstract methods that we have to implement, that the AbstractAccountAuthenticator.getIBinder() will have described. For the most part, the pattern for implementing these abstract methods is the same:

  • If we know the answer already, return a Bundle with it
  • If we don’t know the answer, and need to ask the user something in order to find the answer, we create an Intent that will start an Activity to interact with the user. We return that Intent via the Bundle, under the key, KEY_INTENT. It’s the caller’s responsibility to make use of that intent; if the caller is Android, it will do so.
  • Alternatively, if no synchronous response is possible, but an asynchronous response is possible then it may return null and use the given AccountAuthenticatorResponse to return it later.

There is some crossover between these last two, the Intent you create to interact with the user will likely store the AccountAuthenticatorResponse so that the interactive process has somewhere to return its results once it’s done – i.e. it’s an asynchronous response, it’s just that it’s an interactive asynchronous response.

It’s not unreasonable to create your AbstractAccountAuthenticator child class as an inner-class to AuthenticatorService; as it won’t need to be visible to any other part of the application other than via the IBinder.

addAccount()

Let’s see that process in action in the addAccount() implementation. addAccount() is called (via the hoops we jumped through above) in response to the Add Account option being selected on the Android settings page.

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response,
            String accountType, String authTokenType,
            String[] requiredFeatures, Bundle options)
            throws NetworkErrorException {
        // We absolutely cannot add an account without some information
        // from the user; so we're definitely going to return an Intent
        // via KEY_INTENT
        final Bundle bundle = new Bundle();

        // We're going to use a LoginActivity to talk to the user (mContext
        // we'll have noted on construction).
        final Intent intent = new Intent(mContext, LoginActivity.class);
        
        // We can configure that activity however we wish via the
        // Intent.  We'll set ARG_IS_ADDING_NEW_ACCOUNT so the Activity
        // knows to ask for the account name as well
        intent.putExtra(LoginActivity.ARG_ACCOUNT_TYPE, accountType);
        intent.putExtra(LoginActivity.ARG_AUTH_TYPE, authTokenType);
        intent.putExtra(LoginActivity.ARG_IS_ADDING_NEW_ACCOUNT, true);

        // It will also need to know how to send its response to the
        // account manager; LoginActivity must derive from
        // AccountAuthenticatorActivity, which will want this key set
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
                response);

        // Wrap up this intent, and return it, which will cause the
        // intent to be run
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

The Intent will be run, which will in turn trigger our Activity, in this case called LoginActivity. I used Eclipse-ADT’s template, “Login Activity”, which you can get from Right-Click on source directory -> New -> Other -> Android -> Android Activity, and then changed it to inherit from AccountAuthenticatorActivity. Most of it is just UI work, only gathering credentials, sanity-checking them and supplying a “login” button. The important thing for us is what happens when that login button is clicked. The work of that is done in a separate thread, so the GUI stays responsive. That’s done like this (these are snippets only, you’ll have to put them in the right places yourself):

    // ... in onCreate(), let the base class handle the
    // KEY_ACCOUNT_AUTHENTICATOR_RESPONSE decode
    super.onCreate(bundle);
    // ... in onCreate() most likely ...
    mAccountManager = AccountManager.get(getBaseContext());
    mAccountName = getIntent().getStringExtra(ARG_ACCOUNT_NAME);
    mAccountType = getIntent().getStringExtra(ARG_ACCOUNT_TYPE);
    mAuthType = getIntent().getStringExtra(ARG_AUTH_TYPE);
    // and your UI fields for password, and account name editing

    // ... get here in response to a login click ...
    showProgress(true);
    UserLoginTask mAuthTask = new UserLoginTask();
    mAuthTask.execute((Void) null);

    // ... elsewhere ...
    public class UserLoginTask extends AsyncTask<Void, Void, Intent> {
        @Override
        protected Intent doInBackground(Void... params) {
            final Intent res = new Intent();
            String authToken;

            // Call some external method which handles your
            // application-specific login, returning a token
            try {
                authToken = fetchTokenFromCredentials(mAccountName, mPassword, mAuthTokenType);

                res.putExtra(AccountManager.KEY_ACCOUNT_NAME, mAccountName);
                res.putExtra(AccountManager.KEY_ACCOUNT_TYPE, mAccountType);
                res.putExtra(AccountManager.KEY_AUTHTOKEN, authToken);
                // We'll add an extra one for us
                res.putExtra(PARAM_USER_PASS, mPassword);
            } catch (Exception e) {
                res.putExtra(KEY_ERROR_MESSAGE, e.getMessage());
            }

            return res;
        }

        @Override
        protected void onPostExecute(final Intent intent) {
            // Complete, clear the reference
            mAuthTask = null;

            // If we got an error message, put it on the UI
            if (intent.hasExtra(KEY_ERROR_MESSAGE)) {
                mLoginStatusMessageView.setError(intent.getStringExtra(KEY_ERROR_MESSAGE));
                mLoginStatusMessageView.requestFocus();
            } else {
                finishLogin(intent);
                // Close the activity, we're done
                finish();
            }
        }

        @Override
        protected void onCancelled() {
            mAuthTask = null;
            // Stop whatever UI noise you were making
            showProgress(false);
        }
    }

We’re missing finishLogin() to send the result back to the account manager.

    private void finishLogin(Intent intent) {
        String accountPassword = intent.getStringExtra(PARAM_USER_PASS);
        final Account account = new Account(
                mAccountName,
                intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE));
        String authtoken = intent.getStringExtra(AccountManager.KEY_AUTHTOKEN);

        if (getIntent().getBooleanExtra(ARG_IS_ADDING_NEW_ACCOUNT, false)) {
            // Creating the account
            // Password is optional to this call, safer not to send it really.
            mAccountManager.addAccountExplicitly(account, accountPassword, null);
        } else {
            // Password change only
            mAccountManager.setPassword(account, accountPassword);
        }
        // set the auth token we got (Not setting the auth token will cause
        // another call to the server to authenticate the user)
        mAccountManager.setAuthToken(account, mAuthType, authtoken);

        // Our base class can do what Android requires with the
        // KEY_ACCOUNT_AUTHENTICATOR_RESPONSE extra that onCreate has
        // already grabbed
        setAccountAuthenticatorResult(intent.getExtras());
        // Tell the account manager settings page that all went well
        setResult(RESULT_OK, intent);
    }

Lots of code here, but nothing too scary. All we’re really doing is passing parameters into an Activity via an Intent, having that Activity add its own parameters that it requests from the user, then have all of those used in an AsyncTask to perform the actual logon without blocking the UI, and finally to pass the result of that logon back to the calling Activity via another Intent.

The most important part of all that is in finishLogin(), which is the part that sends something back to the OS. The key calls are:

  • mAccountManager.addAccountExplicitly() et al. These calls add the credentials and authentication token to the locally held account manager object – which in turn passes them to the OS. These are all that are required to make the account appear in the Android account list.
  • setAccountAuthenticatorResult() tells the caller the response to the original act of creating this activity. This is done via the AccountAuthenticatorResponse that got extracted from the KEY_ACCOUNT_AUTHENTICATOR_RESPONSE extra when the activity started.
  • setResult() returns the successful result and the full response data.

It’s not yet clear to me why there are two result return paths.

Let’s look more closely at what gets returned in the Intent’s extra data, that isn’t internal to our Activity:

  • AccountManager.KEY_ACCOUNT_NAME
  • AccountManager.KEY_ACCOUNT_TYPE
  • AccountManager.KEY_AUTHTOKEN

Only the authentication token was unknown at the start. Pretty simple in the end.

getAuthToken()

Now, onto usage. Now that we have a method of creating an account of our own custom type, using our own custom credentials, logging in with them and storing an authentication token to remember that login, how will our application fetch that token for its own use?

Back in our AbstractAccountAuthenticator is an abstract method we need to implement. getAuthToken() is the day-to-day work horse. When an application wants to make use of an account’s remote service, it (indirectly) calls this getAuthToken() (it’s important to note that this is different from the client-side getAuthToken() we’ll see later).

    @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response,
            Account account, String authTokenType, Bundle options)
            throws NetworkErrorException {

        // We can add rejection of a request for a token type we
        // don't support here

        // Get the instance of the AccountManager that's making the
        // request
        final AccountManager am = AccountManager.get(mContext);

        // See if there is already an authentication token stored
        String authToken = am.peekAuthToken(account, authTokenType);

        // If we have no token, use the account credentials to fetch
        // a new one, effectively another logon
        if (TextUtils.isEmpty(authToken)) {
            final String password = am.getPassword(account);
            if (password != null) {
                authToken = fetchTokenFromCredentials(account.name, password, authTokenType)
            }
        }

        // If we either got a cached token, or fetched a new one, hand
        // it back to the client that called us.
        if (!TextUtils.isEmpty(authToken)) {
            final Bundle result = new Bundle();
            result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
            result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
            result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
            return result;
        }

        // If we get here, then we don't have a token, and we don't have
        // a password that will let us get a new one (or we weren't able
        // to use the password we do have).  We need to fetch
        // information from the user, we do that by creating an Intent
        // to an Activity child class.
        final Intent intent = new Intent(mContext, LoginActivity.class);

        // We want to give the Activity the information we want it to
        // return to the AccountManager.  We'll cover that with the
        // KEY_ACCOUNT_AUTHENTICATOR_RESPONSE parameter.
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
                response);
        // We'll also give it the parameters we've already looked up, or
        // were given.
        intent.putExtra(LoginActivity.ARG_IS_ADDING_NEW_ACCOUNT, false);
        intent.putExtra(LoginActivity.ARG_ACCOUNT_NAME, account.name);
        intent.putExtra(LoginActivity.ARG_ACCOUNT_TYPE, account.type);
        intent.putExtra(LoginActivity.ARG_AUTH_TYPE, authTokenType);

        // Remember that we have to return a Bundle, not an Intent, but
        // we can tell the caller to run our intent to get its
        // information with the KEY_INTENT parameter in the returned
        // Bundle
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

Most of that is commentary. There is unshown detail hidden in the fetchTokenFromCredentials() method – as with our addAccount() Activity, that is specific to your application, logon however you wish.

All the above has essentially supplied a server-side implementation (this is the Android authentication service “server” rather than whatever server out on the Internet we’re wrapping) of a custom account type for Android’s AccountManager system to use. Next time we’ll look at how we make use of that account in our app – the client side.

Leave a Reply