This is the follow-up of the previous post on android accounts at http://www.digigene.com/android/accounts-in-android-part-one/.
My complete code for account authentication is available as an open-source code in Github.
The AccountManager is responsible for account management in android. A singleton object of this class is needed first to use its methods:
1 |
AccountManager accountManager = AccountManager.get(context); |
In the following, I am going to shed light on the different scenarios in the authentication procedure.
Account Registration
The first step is REGISTRATION, meant when an account is added for the first time (as summarized in the concluding table of the previous post). AccountManager has two methods for this purpose which may easily get mixed up:
1 |
public boolean addAccountExplicitly (Account account, String password, Bundle userdata) |
and
1 |
public AccountManagerFuture addAccount (String accountType, String authTokenType, String[] requiredFeatures, Bundle addAccountOptions, Activity activity, AccountManagerCallback callback, Handler handler) |
The first method shall be used only when an account (with the provided password and/or userdata) is to be saved as an Account in Settings→Accounts. This obviously cannot be used in the initial registration phase, when the user has not been given an opportunity to interact with the app, for example, by choosing a name as account name or so on.
So the second method (
addAccount) should be used for this purpose. This is the same method used for adding new account both from inside app and Accounts in android Settings.
Now the question is, where shall be the implementation of this method to be accessible to both ways of account registration?
Android has an abstract class named
AbstractAccountAuthenticator which outlines the base methods used in account authentication. This class shall be extended by the user by overriding these base methods and providing the details of the implementation.
The base method in
AbstractAccountAuthenticator for adding account is
addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) which returns a
Bundle containing an intent for starting the activity responsible for showing the user interface for collecting the necessary registration data from the user. The arguments of this method demand that account type and authentication token type be known before adding the user. The
accountType is your package name, organization, brand name, etc, whereas
authTokenType can represent the access level or group, such as “DEMO” or “FULL”. A sample implementation would be:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class AppAccountAuthenticator extends AbstractAccountAuthenticator { private Context context; private Class<? extends RegistrationActivity> registrationActivityClass; public AppAccountAuthenticator(Context context) { super(context); this.context = context; loadRegistrationClassFromSharedPref(); } @Override public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { Intent intent = makeIntent(response, accountType, authTokenType, requiredFeatures, options); Bundle bundle = makeBundle(intent); return bundle; } |
where
1 2 3 4 5 6 |
private Intent makeIntent(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) { Intent intent = new Intent(context, registrationActivityClass); intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType); intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); return intent; } |
and
1 2 3 4 5 |
private Bundle makeBundle(Intent intent) { Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); return bundle; } |
In the above, intent.putExtra(AuthenticatorManager.KEY_AUTH_TOKEN_TYPE, authTokenType) is mandatory in telling the android AccountManager that this intent contains the response to the addAccount method. To distinguish this intent in the returned bundle, it must be augmented with AccountManager.KEY_INTENT as key.
registrationActivityClass above is the Activity class name for the Activity shown to the user for registration. This class is like any other Activity, but it must:
- extend AccountAuthenticatorActivity
- make a request to the server together with the required user info from the user required for registration through the Activity
- actually add the account to the Accounts on the device by calling the addAccountExplicitly described before, once registration on the server is successful
- make a Bundle from the necessary data and use setAccountAuthenticatorResult(bundle) to set the Activity result back to the calling class upon successful registration
If this looks vague, I will make it clear through an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
public class RegistrationActivity extends AccountAuthenticatorActivity { private String accountName, accountType, authTokenType; private String[] requiredFeatures; private Bundle options; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this. /* create your custom layout here */ /* get accountName, password and other required information from the user */ } protected void register(String accountName, String password, String[] requiredFeatures, Bundle options) { Context context = getBaseContext(); if (accountName == null || accountName.trim().isEmpty()) { Toast.makeText(context, context.getString(R.string.auth_msg_account_name_is_null), Toast.LENGTH_SHORT).show(); return; } new RegisterAsync(context, accountName, password, requiredFeatures, options).execute(); } private class RegisterAsync extends AsyncTask { private Context context; private String accountName; private String password; private String[] requiredFeatures; private Bundle options; private Account account; private RegisterResult registerResult; public RegisterAsync(Context context, String accountName, String password, String[] requiredFeatures, Bundle options) { this.context = context; this.accountName = accountName; this.password = password; this.requiredFeatures = requiredFeatures; this.options = options; } @Override protected Object doInBackground(Object[] params) { account = new Account(accountName, accountType); /* This is the method for making registration request to the server, it would be better to define it outside of this class */ registerResult = registerInServer(context, account, password, authTokenType, requiredFeatures, options); return null; } @Override protected void onPostExecute(Object o) { if (registerResult.isSuccessful) { String refreshToken = registerResult.refreshToken; AccountManager.get(context).addAccountExplicitly(account, refreshToken, options); Bundle bundle = makeBundle(accountName, accountType, authTokenType, refreshToken, options); setAccountAuthenticatorResult(bundle); finish(); } else { throw new RuntimeException("User registration is not successful in authenticator due to the following error:/n" + registerResult.errMessage); } } } private Bundle makeBundle(String accountName, String accountType, String authTokenType, String refreshToken, Bundle options) { Bundle bundle = new Bundle(); bundle.putString(AccountManager.KEY_ACCOUNT_NAME, accountName); bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType); bundle.putString(AccountManager.KEY_PASSWORD, refreshToken); return bundle; } } |
In the code excerpt above, successful user registration through registerInServer returns a refresh token from the server. In android, this is also called password, as in addAccountExplicitly (Account account, String password, Bundle userdata). This is misleading, however, as the password shall only be used in registerInServer for making registration request to the server and not be stored anywhere on the device. This is because password is the only way by which a user is authorized to use the whole services of a provider and must never be compromised. Google strongly advises against it here and does advise to consult security professionals for it. As a professional who has been involved in some banking projects that demand very high levels of security, I recommend you to never ever save any kind of password, encrypted or not, on the client’s device. In fact, this is the main reason OAuth 2.0 has replaced the traditional authentication methods.
Take Google as an example. It has several apps and services, all accessible by a single password. So if this unique password were saved by any Google app on the device using addAccountExplicitly, it would be easily accessible in rooted phones. In unrooted devices, however, android avoids this by preventing apps with different UID signature than the authenticator from calling the method setPassword of the AccountManager.
The refresh token, thus, should be saved instead for this purpose. One might now ask, what if this token is disclosed? I will write about it later in this post … stay tuned 😉
When setAccountAuthenticatorResult(bundle) is called, the user is taken back to where he was before calling addAccount. If account registration is through settings, no further steps is undertaken and the account is added. However, if it is from inside the app, other steps can be taken using an AccountManagerCallback which is called after registration is done. Let’s take a closer look at the addAccount method from AccountManager:
1 2 3 |
public AccountManagerFuture addAccount (String accountType, String authTokenType, String[] requiredFeatures, Bundle addAccountOptions, Activity activity, AccountManagerCallback callback, Handler handler) |
In account registeration through settings,
callback is set to
null by android, while it can be set to do further authentication steps, e.g. getting access token, when called from inside app by setting the appropriate
callback.
The concluding part of this is on how the
AccountManager should know about the class extending the
AbstractAccountAuthenticator to call its relevant methods. This has to be done through a bound
Service. In fact, when
accountManager.addAccount is used, it broadcasts an action named
android.accounts.AccountAuthenticator which in turn invokes a bound service instantiating the class extended from
AbstractAccountAuthenticator. The is what
manifest.xml should include to address this:
1 2 3 4 5 6 7 8 |
<service android:name="com.digigene.accountauthenticator.AuthenticatorService"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> </service> |
Special attention should be paid to the resource meta-data. It is an xml file that has to include the following for the authenticator to work:
1 2 3 4 5 6 7 |
<?xml version="1.0" encoding="utf-8"?> <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="@string/auth_account_type" android:icon="@mipmap/ic_launcher" android:label="@string/auth_account_type" android:smallIcon="@mipmap/ic_launcher" /> |
Here, label is the name shown on the right of the account icon (‘DigiGene’ in this figure). There is some ambiguity, however, about what the difference between icon and smallIcon is. Android declares here that both of them refer to the icon at the left of the account label (android robot icon in this figure), but where the smallIcon is used instead of icon depends on the screen size. This definition does not seem to be exact in view of the following comparison:
From the above figure, it seems that android uses the smallIcon for the Accounts page (left screenshot in the above image). The larger icon is only used for the page showing the different accounts of an account type (right screenshot) in Note 4, whereas Nexus 5 displays the same icon size for both screens. As the screen widths of Nexus 5 and Note 4 are 388 dp and 445 dp respectively, screen width of 400 dp can be guessed as a threshold for the screen width above which the larger icon is applied. This is not exact, nevertheless.
Many implementations of account authenticator do not work due to not setting this xml file properly, which cause the service not to start.
A sample code for defining the bound service would be:
1 2 3 4 5 6 7 8 9 |
public class AuthenticatorService extends Service { @Override public IBinder onBind(Intent intent) { AppAccountAuthenticator appAccountAuthenticator = new AppAccountAuthenticator(this); return appAccountAuthenticator.getIBinder(); } } |
In the next post, I will write about how to authorize the user with an already registered account.
Process: demo.com.accountmanagerdemo, PID: 6764
Caused by: java.lang.SecurityException: uid 10638 cannot explicitly add accounts of type: demo.com.accountmanagerdemo
at android.os.Parcel.readException(Parcel.java:1693)
at android.os.Parcel.readException(Parcel.java:1646)
Hi, Ali, I got the about exception when I try to add account, can you help to tell me what is the problem. thank you very much
Have you added this your manifest?
service android:name=”com.digigene.accountauthenticator.AuthenticatorService”>
service>
where do I set the server URL?
Great article, what is the difference between registration and sign up?