Use refresh token if auth token expired in account authenticator

Denis Yakovenko picture Denis Yakovenko · Dec 30, 2015 · Viewed 7.9k times · Source

I have an app which uses AccountManager to store users' accounts. Users log in and sign up through my REST API using OAuth2.0 password-username credentials flow.

The access tokens that users receive expire in 2 hours and need to be refreshed until expired again, and so on.

I need to implement this refreshing functionality in my authenticator.

I have a model called AccessToken which has the following fields:

String accessToken, String tokenType, Long expiresIn, String refreshToken, String scope, Long createdAt.

So currently, in my getAuthToken method in AccountAuthenticator class I receive this AccessToken object and use its accessToken field as an Auth Token for my Account Manager.

What I need is to somehow store both refresh token and auth token using my account manager and when the app tries to access the API and gets the error response: {"error": "access token expired"}, to refresh the current access token using the refreshToken string from the AccessToken object received previously. However, I'm not sure how I should do it.

My getAuthToken method in authenticator class currently looks like this:

@Override
public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account,
                           String authTokenType, Bundle options) throws NetworkErrorException {
    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;
    }

    final AccountManager manager = AccountManager.get(context.getApplicationContext());

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

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

    if (TextUtils.isEmpty(authToken)) {
        final String password = manager.getPassword(account);
        if (password != null) {
            try {
                authToken = discountyService.getAccessToken(DiscountyService.ACCESS_GRANT_TYPE,
                        account.name, password).toBlocking().first().getAccessToken();
// =======
// Here the above discountyService.getAccessToken(...) call returns
// AccessToken object on which I call the .getAccessToken() 
// getter which returns a string.
// =======
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    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;
    }

    final Intent intent = new Intent(context, LoginActivity.class);
    intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, accountAuthenticatorResponse);
    intent.putExtra(LoginActivity.ARG_ACCOUNT_TYPE, account.type);
    intent.putExtra(LoginActivity.ARG_AUTH_TYPE, authTokenType);
    final Bundle bundle = new Bundle();
    bundle.putParcelable(AccountManager.KEY_INTENT, intent);
    return bundle;
}

and the class AccountGeneral just contains some constants:

public class AccountGeneral {

    public static final String ACCOUNT_TYPE = "com.discounty";

    public static final String ACCOUNT_NAME = "Discounty";

    public static final String AUTHTOKEN_TYPE_READ_ONLY = "Read only";

    public static final String AUTHTOKE_TYPE_READ_ONLY_LABEL = "Read only access to a Discounty account";

    public static final String AUTHTOKEN_TYPE_FULL_ACCESS = "Full access";

    public static final String AUTHTOKEN_TYPE_FULL_ACCESS_LABEL = "Full access to a Discounty account";
}

The app will also be using SyncAdapter and will interact with the API quite frequently to sync the data from and to the server, and these API calls will also need to use access tokens as parameters in the requests, so I really need to implement this refreshing functionality and make it automatic.


Does anybody know how to implement that correctly?


PS: I'll be using a local database to store all my data and I could store the token objects as well. This seems like an easy hack, although insecure. Maybe I should always store just one refresh token at a time as a db record and update it as the app receives the new token?

PPS: I'm free to change the way the API works anyhow, so if there are suggestions on improving the mobile app by making the API better, they are very appreciated as well.

Answer

Grace Coder picture Grace Coder · Dec 30, 2015

You can save the refresh token into your account's user data when you first add your account using:

Bundle userdata = new Bundle;
userdata.putString("refreshToken", refreshToken);
mAccountManager.addAccountExplicitly (account, password, userdata);

You can also set the userdata after adding the account by calling:

mAccountManager.setUserData(account, "refreshToken", refreshToken);

When you access token is expired you can retrieve your refresh token by calling:

String refreshToken = mAccountManager.getUserData(account, "refreshToken");

Use the refreshToken to retrieve a new access token.