Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AccountManager: when to set result?

Context

My app only stores user/pass. No tokens are used.

Question 1

Methods setAccountAuthenticatorResult(Bundle) and onResult(Bundle) are meant to notify the AbstractAccountAuthenticator about the outcome, but I have a project working without them, what are they for ?

Question 2

What is onRequestContinued() for ?

Question 3

When addAccount is finished and the account created, should onActivityResult be called on the Activity that triggered it?

Question 4

If an Intent is returned with key AccountManager.KEY_INTENT in addAccount implementation, the AbstractAccountAuthenticator will start the Intent. I have noticed that many developers add extras. Who gets them ?

public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException
{
    Intent intent = new Intent(mContext, AuthenticatorActivity.class);
    intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

    intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE, accountType);     // <-- this
    intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE, authTokenType);      // <-- this
    intent.putExtra(AuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT, true);   // <-- this


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

    return bundle;
}

Asnwers

kris larson:

Thanks for the answer. I think we might be using the AccountManager wrong to be honest.

We want to share some credentials across our apps, so we have a Service to hold the custom account type. Since apps know the account type and share the signing certificate, they have access to the Account.

When each app is started, they try to get the Account. If no Account exists, they trigger the login page in our Service by calling AccountManager.addAccount(...).

Once the login (through web services) is successfull, we make it available to other apps with AccountManager.addAccountExplicitly(...). Not setting an outcome after it, does not impact the outcome.

How does it affect the AccountManager ? Is this approach correct ?

like image 477
JonZarate Avatar asked May 31 '18 16:05

JonZarate


1 Answers

EDIT: I'll just describe how — after much agony — I implemented the authentication service for our app.

You said you have a Service for the authenticator. I'll assume you did that in the prescribed way: declare a service in your manifest that references your class, which subclasses android.accounts.AbstractAccountAuthenticator. The manifest entry also references an XML file with the account-authenticator tag that declares your account type, plus a name and icons to be used by the Settings | Accounts page. That stuff is all documented.

For our app, I wanted the last signed in user to sign in automatically until they selected "Sign Out" on the navigation drawer. To accomplish this, the username would be persisted in SharedPreferences for the app.

So if the SharedPreferences did not have a username, the app calls AccountManager.newChooseAccountIntent() for our custom account type, then calls startActivityForResult with intent that was returned. If the user selects "Add new account", then addAccount() will be invoked on the authenticator as described below.

Our authentication activity not only has UI for username/password login but also password reset and new trial account creation, so there are a lot of moving parts. When the user enters the username/password and presses the Sign In button, the app authenticates against our server. If successful, the app calls AccountManager.addAccountExplicitly() for that username.

The AccountManager Choose Account activity will call onActivityResult() when this is all finished. If we get RESULT_OK the app persists the username for the next sign in operation.

If the SharedPreferences has a username, and the Account Manager has an account registered for it, we don't need to select anything and just skip to this next phase.

With the account selected/known, now the app can authenticate. It calls peekAuthToken() to see if a token was registered, and invalidateAuthToken() if it was. This is done so that when the app calls getAuthToken() on the account manager, it forces the account manager to call getAuthToken() on the app's authenticator to authenticate with the server. Since the server is not returning tokens, we are using dummy tokens and that is why they are invalidated every time.

Now the interesting part about all this is that if the user selects an account that was already registered, it won't be authenticated, whereas if they selected Add New Account and that operation was successful, the new account will be authenticated. So just be aware that if you notice that signing into a new account causes two round trips to your authentication server, now you know why. (I have some logic in there using setUserData() on the account to indicate pre-authenticated, but it looks like I forgot to finish that feature. Hmmm.)


Let's start with some background to clarify things.

You have some subclass of AbstractAccountAuthenticator. You have created this class to respond to authentication requests for the account type it was registered for. The thing to keep in mind about this class is that the platform AccountManager is always the component your app will interact with; your app will never interact directly with this class. So in a sense the AccountManager is a client of your authentication service. The work that the authenticator performs is stuff that will be in a background thread.

Now part of the work the authentication service must do is interact with the user, asking for user account names, passwords, fingerprint IDs, smart cards, etc. So let's assume you have an AuthenticatorActivity that does that work. Typically you will subclass android.accounts.AccountAuthenticatorActivity for this. This activity class is really nothing special. The only thing it does is to expect a reference to an AccountAuthenticatorResponse when it starts, then to call that response's onResult() method when the activity exits.

The app I work on is similar to yours in that there is no token returned from the authentication service. I still implemented getAuthToken for two reasons:

  • to stay consistent to the workflow that Google has mandated.
  • to make it easy to return a real token if/when we enhance our authentication service to return a token.

So let's follow the bouncing ball to understand how everything fits together.

  • Your app activity has implemented the AccountManagerCallback interface in order to get asynchronous messages from the AccountManager.
  • Let's assume your app has persisted a user name for authentication. Your app creates an Account object with this username.
  • Optionally your app can call AccountManager.getAccountsByType() to ensure that the account exists.
  • Your app calls AccountManager.getAuthToken() for the account.
  • The AccountManager sees that your authenticator is registered for that account type and calls authenticator's getAuthToken method.
  • The getAuthToken method will contact your authentication server and get a response.
  • If successful, your authenticator returns the (dummy) auth token.

If the login fails, then things get interesting.

  • Since your authenticator is not returning an auth token, it must return an intent to start your AuthenticatorActivity so that the user can re-authenticate.

  • The AccountManager uses your intent to start your AuthenticatorActivity. As part of the request, it will get a reference to an AccountAuthenticatorResponse it will need later.

  • Your AuthenticatorActivity interacts with the user, gets username/password, contacts your server, gets a response. Let's say the authentication succeeds.
  • Your activity will finish, calling the onResult method of the AccountAuthenticatorResponse it was given.
  • The AccountManager on getting notification from the response object will invoke the callback method with the results.

So with that we can answer your questions:

Methods setAccountAuthenticatorResult(Bundle) and onResult(Bundle) are meant to notify the AbstractAccountAuthenticator about the outcome, but I have a project working without them, what are they for ?

More correctly, they are meant to notify the AccountManager about the outcome. Are you sure it's working? Have you tried logging in with an invalid username/password? AccountManager will assume authentication has been cancelled unless it gets notified otherwise.

What is onRequestContinued() for ?

I don't have an exact answer. My guess is that it's to keep the AccountManager in sync somehow with what is happening with the UI in regards to authentication.

If an Intent is returned with key AccountManager.KEY_INTENT in addAccount implementation, the AbstractAccountAuthenticator will start the Intent. I have noticed that many developers add extras. Who gets them ?

The answer is: You do. This intent will go the AuthenticatorActivity that you have created to handle authentication, so any data your activity needs to perform authentication needs to be passed in with these extras.

When addAccount is finished and the account created, should onActivityResult be called on the Activity that triggered it?

The use case for this is when your app calls AccountManager.newChooseAccountIntent() then calls startActivityForResult() with the resulting intent. The AccountManager's "choose account" activity is then started.

Now since "add new account" is an option on the "choose account" UI, the AccountManager will call your authenticator's addAccount() method when that option is selected. Your authenticator then returns a Bundle as described in the AbstractAccountAuthenticator docs back to AccountManager. After this the "choose account" activity will finish and call onActivityResult() on your activity.

Hopefully you're starting to see that the AccountManager acts as a broker between your app and your authenticator components. To make this clearer, consider that you could package up those components in a way that other apps could use them to authenticate to your service without knowing any details. If you created your own custom account type, you can invoke your authentication directly from the Settings | Accounts page of the device. This is a good test to see if your addAccount() method is implemented correctly.

like image 86
kris larson Avatar answered Sep 25 '22 00:09

kris larson