0

How to Write An AndroidAuthenticator

rsz_rsz_rsz_google-account-on-android-hero.jpg

What is Account Authenticator

This is a great piece to authenticate user-accounts which is used by many popular applications e.g. Google, DropBox, Facebook, Twitter, Evernote etc. This is a recommended way to authenticate the account-information like username & password which defines the appearance of user’s account in the Accounts & Sync settings page as below:

rsz_share-paid-android-apps-for-free-legallyw1456.jpg

Benefits of Account Authenticator

  • Useful to create background synchronization mechanism like SyncAdapter.

  • Standard way to authenticate users with supporting different tokens.

  • Account sharing with different privileges.

To implement an Account Authenticator needs 04 basic things are:

  • A Service that returns a subclass of AbstractAccountAuthenticator from the onBind method,

  • An Authenticator which extends AbstractAccountAuthenticator class to communicate with the account.

  • Activity to prompt the user to enter their credentials (username & password).

  • XML layout file to define the apperance of the authenticator in the settings page.

This article describes about how to write your own Account Authenticator.

Terms to Know

  1. AccountManager : This is a manager to provide the access to a centralized registry to authenticate credentials (username & password) once per account and helps to request an auth token. Here is a diagram about how to get a token.

oauth_dance.png

AccountManager simplifies the authentication process for few cases:

  • When one user tries to change the password of another user then an expired token can help to stop the shit process!
  • When you need an UI during a running background service to interact with credintials to manage your account.
  • When you feel to have the "login once and get authenticated in all accounts of the related apps" feautre like all Google’s apps.

It can handle multiple token types for a single account (e.g. Read-only vs. Full-access) & easily shares auth-tokens between apps.

  1. Authentication Token (auth-token) : This is a security token (or sometimes a authentication token, cryptographic token, software token, virtual token, or key fob) is given to ease authentication. Here is a funny monkey-diagram about how to get a token to access any account!

Untitled Diagram.png

  1. AbstractAccountAuthenticator : An authenticating class containing all logic for working with account (authorization, access rights etc). The AccountManager finds the appropriate AbstractAccountAuthenticator because one AbstractAccountAuthenticator may be used by different application (like Google account for Gmail, Calendar, Drive etc).

  2. AccountAuthenticatorActivity - The base Activity to log-in/create account is called by the AccountManager to identify the user's account.

Now you can start to write your own AndroidAuthenticator for your application. Here is the steps to do:

  • Creating an AndroidAuthenticator with extending AbstractAccountAuthenticator class.
  • Write a AccountAuthenticatorService class to bind the Authenticator.

So, let's do it! (honho)

Creating the Authenticator

First of all, you will need an Authenticator with extending AbstractAccountAuthenticator class as following:

package yourPackageHere;

import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.os.Bundle;

public class MyAccountAuthenticator extends AbstractAccountAuthenticator {

    public MyAccountAuthenticator(Context context) {
        super(context);
    }

    @Override
    public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
        return null;
    }

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
                             String[] requiredFeatures, Bundle options) throws NetworkErrorException {
        return null;
    }

    @Override
    public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
            throws NetworkErrorException {
        return null;
    }

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

    @Override
    public String getAuthTokenLabel(String authTokenType) {
        return null;
    }

    @Override
    public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)
            throws NetworkErrorException {
        return null;
    }

    @Override
    public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features)
            throws NetworkErrorException {
        return null;
    }
}

You may observe some methods are existing on the above class e.g. addAccount(), getAuthToken(), hasFeatures() etc. You need to add some codesnaps as your requirement. To add a new account you need to do as following:

@Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
                             String[] requiredFeatures, Bundle options) throws NetworkErrorException {

        Intent intent = new Intent(mContext, SignInAuthenticatorActivity.class);
        intent.putExtra(SignInAuthenticatorActivity.ARG_ACCOUNT_TYPE, accountType);
        intent.putExtra(SignInAuthenticatorActivity.ARG_AUTH_TYPE, authTokenType);
        intent.putExtra(SignInAuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT, true);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

        Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

Where, SignInAuthenticatorActivity is reponsible to show an login interface to access your account. In that case, you can also add SignUpActivity to provide the interface to create a new account.

However, You need another method in the authenticator class to get the auth-token as following:

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

        // If the caller requested an authToken type we don't support, then
        // return an error
        if (!authTokenType.equals(AccountGeneral.AUTHTOKEN_TYPE_READ_ONLY) &&
                !authTokenType.equals(AccountGeneral.AUTHTOKEN_TYPE_FULL_ACCESS)) {
            final Bundle result = new Bundle();
            result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType");
            return result;
        }

        // Extract the username and password from the Account Manager, and ask
        // the server for an appropriate AuthToken.
        AccountManager am = AccountManager.get(mContext);

        String authToken = am.peekAuthToken(account, authTokenType);

        Log.d("" + TAG, " > peekAuthToken returned - " + authToken);

        // Lets give another try to authenticate the user
        if (TextUtils.isEmpty(authToken)) {
            final String password = am.getPassword(account);
            if (password != null) {
                try {
                    Log.d("" + TAG, " > re-authenticating with the existing password");
                    authToken = AccountGeneral.sServerAuthenticate.userSignIn(account.name, password, authTokenType);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        // If we get an authToken - we return it
        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;
        }

        Intent intent = new Intent(mContext, SignInAuthenticatorActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        intent.putExtra(SignInAuthenticatorActivity.ARG_ACCOUNT_TYPE, account.type);
        intent.putExtra(SignInAuthenticatorActivity.ARG_AUTH_TYPE, authTokenType);
        intent.putExtra(SignInAuthenticatorActivity.ARG_ACCOUNT_NAME, account.name);
        Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

So far, you need the following user permissions in the AndroidManifest:

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

Write a Service class to bind the AccountAuthenticator

The Service class contains an IBinder containing the authenticator as below:

public class MyAuthenticatorService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        MyAccountAuthenticator authenticator = new MyAccountAuthenticator(this);
        return authenticator.getIBinder();
    }
}

Now, you need to declare the service in your AndroidManifest with required meta data.

<service android:name=".MyAuthenticatorService">
      <intent-filter>
          <action android:name="android.accounts.AccountAuthenticator" />
      </intent-filter>
      <meta-data android:name="android.accounts.AccountAuthenticator"
          android:resource="@xml/authenticator" />
</service>

Where you need to pass a xml file to provide the user interface where users can interact with the AccountAuthenticator. The authenticator.xml file is following:

<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountPreferences="@xml/prefs"
    android:accountType="your package here"
    android:icon="@drawable/ic_supervisor_account_normal"
    android:label="@string/settings_label"
    android:smallIcon="@drawable/ic_supervisor_account_small" />

Here, another xml file as prefs.xml is needed to define the preferences layout! (facepalm)

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory android:title="Preferences Title" />
    <CheckBoxPreference
        android:key="isDebug"
        android:summary="Connecting to a debug server instead of prod server"
        android:title="Use debug server" />
    <SwitchPreference
        android:key="logsVerbose"
        android:summary="Show debug logs on LogCat"
        android:title="Debug Logs" />
</PreferenceScreen>

Adding a New Account

To add a new account you will need a codesnap to submit the user-credentials (username & password) from your login/signup activity as following:

public void submit() {
        final String userName = ((TextView) findViewById(R.id.accountName)).getText().toString();
        final String userPass = ((TextView) findViewById(R.id.accountPassword)).getText().toString();
        final String accountType = getIntent().getStringExtra(ARG_ACCOUNT_TYPE);
        new AsyncTask<String, Void, Intent>() {
            @SuppressLint("LongLogTag")
            @Override
            protected Intent doInBackground(String... params) {
                String authtoken = null;
                Bundle data = new Bundle();
                try {
                    authtoken = AccountGeneral.sServerAuthenticate
                            .userSignIn(userName, userPass, mAuthTokenType);

                    data.putString(AccountManager.KEY_ACCOUNT_NAME, userName);
                    data.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
                    data.putString(AccountManager.KEY_AUTHTOKEN, authtoken);
                    data.putString(PARAM_USER_PASS, userPass);

                } catch (Exception e) {
                    data.putString(KEY_ERROR_MESSAGE, e.getMessage());
                }

                final Intent res = new Intent();
                res.putExtras(data);
                return res;
            }

            @Override
            protected void onPostExecute(Intent intent) {
                if (intent.hasExtra(KEY_ERROR_MESSAGE)) {
                    Toast.makeText(getBaseContext(), intent.getStringExtra(KEY_ERROR_MESSAGE),
                            Toast.LENGTH_SHORT).show();
                } else {
                    finishLogin(intent);
                }
            }
        }.execute();
    }

    @SuppressLint("LongLogTag")
    private void finishLogin(Intent intent) {
        String accountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
        String accountPassword = intent.getStringExtra(PARAM_USER_PASS);
        final Account account = new Account(accountName, intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE));
        if (getIntent().getBooleanExtra(ARG_IS_ADDING_NEW_ACCOUNT, false)) {
            String authtoken = intent.getStringExtra(AccountManager.KEY_AUTHTOKEN);
            String authtokenType = mAuthTokenType;
            mAccountManager.addAccountExplicitly(account, accountPassword, null);
            mAccountManager.setAuthToken(account, authtokenType, authtoken);
        } else {
            Log.d("SignInAuthenticatorActivity", TAG + "> finishLogin > setPassword");
            mAccountManager.setPassword(account, accountPassword);
        }
        setAccountAuthenticatorResult(intent.getExtras());
        setResult(RESULT_OK, intent);
        finish();
    }

The finishLogin() method basically maintains 02 cases as following:

  1. Existing account: There is already a record stored on the AccountManager. The old auth-token will be replaced by the new token but if the user had changed his password you need to update the AccountManager with the new password too.

  2. New account: When creating a new account, the auth-token is actually not stored immediately to the AccountManager, it needs to be saved explicitly.

Anyway, After submitting the credentials you will get the returns at onActivityResult():

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // The sign up activity returned that the user has successfully created an account
        if (requestCode == REQ_SIGNUP && resultCode == RESULT_OK) {
            finishLogin(data);
        } else
            super.onActivityResult(requestCode, resultCode, data);
    }

Okay, that's all (yaoming)

Actually, I want to keep this article as short as possible with providing the minimum knowledge & code base you need to create your own AndroidAuthenticator for your beloved application! But you need to more study before starting to write yourself. For your convenience, I have added a sample AndroidAuthenticator here: AndroidAccountManager. After running the github-sample you will get a sample idea like the following screenshots:

dd.png

References

Happy Coding!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí