Use refresh token if auth token expired in account authenticator
Asked Answered
U

1

14

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.

Ugo answered 30/12, 2015 at 11:18 Comment(25)
how did you manage to get the refresh token ?Cellular
@Denis Yakovenko Can u share ur codes because i also struggling to understand how to manage both access and refresh token and sync functionality using account manager.Please share if u canInsignificant
@Insignificant I cannot say that I've found a good solution for it and that the code on the link is good, but still, here's my repo with the whole app. Take a look at app/src/main/java/discounty/com/activities/LoginActivity.java. The usage of refresh tokens is mostly there. Hope it helpsUgo
@Denis Yakovenko oh..thanks for your this example.It seems to be big but still far better to understand the logic than alll the other examples that i seen,but still i have a doubt, where is the section wich calls ur server url for authentication,token refresh,etc..i didn't saw a url calling to ur own server for authentication and token refreshment after a quick lookInsignificant
@Insignificant you didn't see the urls because I'm using the Retrofit library for doing API calls. Here's the file where all my urls and queries are defined: interfaces/DiscountyService.java. You can also find a base url in api/ServiceGenerator.javaUgo
@Denis Yakovenko are u using oauth2.0 in ur server end.if yes how do u manages the expiry of refresh token using account manager??Insignificant
@Insignificant Yes, I use OAuth2.0 and I refresh the token every time I make a request. Have a closer look at my method persistNewDiscountCardToServerAsync(...). There's some RxJava in there, but still it is quite understandable how I manage access tokens in my app. The method creates an object on the server side, just read it line by line and try to understand what's going on on each step. Hope it helpsUgo
@Denis Yakovenko i think u r only refreshing the access token when it expired.Am asking how to refresh the refresh token when it expires?when we request a api call with grant type as refresh token does the oauth2 server checks the expiry of this refresh token that we passed in this request??what my understanding is that the when a request with refresh token comes the server only refresh the access token.Is it right?Insignificant
@Insignificant The refresh token request is defined here. refreshAccessToken returns a new AccessToken object. Examine it. Note that I refresh the access token every time I make a request to the server (notice the usage of refreshAccessToken(...) method on every CRUD request).Ugo
@Denis Yakovenko yes that i agree..suppose the server issues an access token with a validity of 2 hour and a refresh token of validity 3 days,then after 2 hour when we send the refresh token the server will issue a new access token,that i know,but what will happen when the refresh token with a validity of 3 days expire???Insignificant
@Insignificant The refresh tokens do not expire on my server. Only access tokens do expire. The refresh token gets expired only once it is used. There's no specific time for it to get expired, it just can be used only once.Ugo
@Denis Yakovenko oh..that means when you send a request with REFRESH_GRANT_TYPE then the server will send a new refresh token along with the new access token and in android u replace the old refresh token with the new refresh token and also the expired access token with new access token...am i right??Insignificant
@Insignificant Yep, I think you got itUgo
@Denis Yakovenko Actually i am new in oauth 2 and when i request for an access token i got a access token with a small lifetime and a refresh token having a long time validity.so i thought the refresh token also gets expired when the specified periods comes.Insignificant
@Denis Yakovenko thanks for all ur quick replys.Actually am in big trouble to implement this oauth2 and authentication from android using account manager, and after researching so many days i have implemented oauth 2 in my server.Then again in trouble to connect to this server from android.After talking to u i got my confidence back, and ur example project also helps.Thanku once again for ur help.Insignificant
@Denis Yakovenko does i want to store the user password in the account manager after login.Is there any use for saving this password in android because this access token and refresh token will do all the works with the server.Then why to store password??Insignificant
@Insignificant In my case, I used the email-password strategy for my oauth2 authentication, that's why I needed to store it in the account manager.Ugo
@Denis Yakovenko ok.i have one more question.Suppose a user is authenticated in multiple devices at a time and if he changes his password from one among the device, then what will happen??does the server will issue a new access token.Is this cause any problem in other devices?Insignificant
@Insignificant if the server's oauth2 implementation is correct (e.g. it uses some solid library), then everything will be fine, there will be no problems. You don't have to think about it when implementing the android appUgo
ok.after starting to implement authentication i have lots of doubts like this and am searching for somebody like u to help me.thanku sirInsignificant
@Denis Yakovenko Does this given example contains the class which implements the interface DiscountyServiceInsignificant
@Insignificant There's no need for a class. Read about the Retrofit library by Square.Ugo
@Denis Yakovenko How can i make the refresh token for single time use only with lifelong validity as u said using spring security oauth2.Can u provide a solution for my this post #38604403Insignificant
@Denis Yakovenko if i set the DefaultTokenServices property "reuseRefreshToken" to "no" it makes the refresh token to single time use only,but still it provide the refresh token a validity of 1 month by default.Also if i set the property "reuseRefreshToken" to "no" then it will cause problem when the same user login from another device.Any solution??Insignificant
@Denis Yakovenko if i called accountManager.invalidateAuthToken() without clearing the password to refresh the access token its not calling the authenticator.getAuthToken() method.But when i clear the password and then called the accountManager.invalidateAuthToken() it will call the getAuthToken() method.Why so??I don't want to clear the password when refreshing the tokenInsignificant
O
7

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.

Orson answered 30/12, 2015 at 11:57 Comment(3)
Note that you always should set the user data using setUserDatasince addAccountExplicitly is known to be broken. It doesn't always set the user data passed in the bundle. See https://mcmap.net/q/901921/-custom-authentication-how-to-get-custom-user-data-valuesAcrocarpous
@Grace Coder how to use the refresh token to retrieve the new access token from server using account manager.how can i manage the expiry of access token with refresh token using account manager. Please reply...Insignificant
@Insignificant when you retrieve the refresh token from the server, the server will return the refresh token and the expiry of the refresh token. Then in your account manager you need to check whether the refresh token is expired by comparing the current date return from the device before you request for the new access token. If the refresh token is expired, you need to go through the entire process of OAuth 2 again which include prompting user for his/her password again.Orson

© 2022 - 2024 — McMap. All rights reserved.