1
votes

I am creating an android app with azure mobile service. I have a service that runs always (with startForeground()) and is monitoring some of the user activities. The service sometimes needs to query an azure database invoking APIs, stored in the azure cloud, this way:

mClient.invokeApi("APIname", null, "GET", parameters);
//mClient is the MobileServiceClient instance

At the beginning the user logins by using a LoginActivity and everything works fine. After some time ( usually 1 hour) the token for the client expired and I received exceptions like this:

IDX10223: Lifetime validation failed. The token is expired.

After some searches I found the solution to refresh token here: https://github.com/Microsoft/azure-docs/blob/master/includes/mobile-android-authenticate-app-refresh-token.md

if the activiy is alive the code works and refreshes successfully the token if expired. But if the activity was destroyed it doesn't work. So I decided to pass the ApplicationContext to the client, this way:

mClient.setContext(activity.getApplicationContext());

but now I receive a ClassCastException, because the client tries to cast the context to Activity. Here are the interesting lines of the exception:

     java.lang.ClassCastException: android.app.Application cannot be cast to android.app.Activity
                  at com.microsoft.windowsazure.mobileservices.authentication.LoginManager.showLoginUI(LoginManager.java:349)
                  at com.microsoft.windowsazure.mobileservices.authentication.LoginManager.authenticate(LoginManager.java:161)
                  at com.microsoft.windowsazure.mobileservices.MobileServiceClient.login(MobileServiceClient.java:371)
                  at com.microsoft.windowsazure.mobileservices.MobileServiceClient.login(MobileServiceClient.java:356)
                  at com.microsoft.windowsazure.mobileservices.MobileServiceClient.login(MobileServiceClient.java:309)

So how can I refresh the token from a service without an activity? Or is there another way to keep the client always authenticated?

EDIT

I try to paste here some code, hoping to make clearer the way I am using authentication token. I have a LoginManager for managing authentication. Here some meaningful code from that class:

 public boolean loadUserTokenCache(Context context)
{
    init(context); //update context

    SharedPreferences prefs = context.getSharedPreferences(SHARED_PREF_FILE, Context.MODE_PRIVATE);
    String userId = prefs.getString(USERID_PREF, null);
    if (userId == null)
        return false;
    String token = prefs.getString(LOGIN_TOKEN_PREF, null);
    if (token == null)
        return false;

    MobileServiceUser user = new MobileServiceUser(userId);
    user.setAuthenticationToken(token);
    mClient.setCurrentUser(user);

    return true;
}

The filter is:

    private class RefreshTokenCacheFilter implements ServiceFilter {

    AtomicBoolean mAtomicAuthenticatingFlag = new AtomicBoolean();

    //--------------------http://stackoverflow.com/questions/7860384/android-how-to-runonuithread-in-other-class
    private final Handler handler;
    public RefreshTokenCacheFilter(Context context){
        handler = new Handler(context.getMainLooper());
    }
    private void runOnUiThread(Runnable r) {
        handler.post(r);
    }
    //--------------------

    @Override
    public ListenableFuture<ServiceFilterResponse> handleRequest(
            final ServiceFilterRequest request,
            final NextServiceFilterCallback nextServiceFilterCallback
    )
    {
        // In this example, if authentication is already in progress we block the request
        // until authentication is complete to avoid unnecessary authentications as
        // a result of HTTP status code 401.
        // If authentication was detected, add the token to the request.
        waitAndUpdateRequestToken(request);
        Log.d(Constants.TAG, logClassIdentifier+"REFRESH_TOKEN_CACHE_FILTER is Sending the request down the filter chain for 401 responses");
        Log.d(Constants.TAG, logClassIdentifier+mClient.getContext().toString());
        // Send the request down the filter chain
        // retrying up to 5 times on 401 response codes.
        ListenableFuture<ServiceFilterResponse> future = null;
        ServiceFilterResponse response = null;
        int responseCode = 401;
        for (int i = 0; (i < 5 ) && (responseCode == 401); i++)
        {
            future = nextServiceFilterCallback.onNext(request);
            try {
                response = future.get();
                responseCode = response.getStatus().code;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                if (e.getCause().getClass() == MobileServiceException.class)
                {
                    MobileServiceException mEx = (MobileServiceException) e.getCause();
                    responseCode = mEx.getResponse().getStatus().code;
                    if (responseCode == 401)
                    {
                        // Two simultaneous requests from independent threads could get HTTP status 401.
                        // Protecting against that right here so multiple authentication requests are
                        // not setup to run on the UI thread.
                        // We only want to authenticate once. Requests should just wait and retry
                        // with the new token.
                        if (mAtomicAuthenticatingFlag.compareAndSet(false, true))
                        {
                            // Authenticate on UI thread

                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    // Force a token refresh during authentication.
                                    SharedPreferences pref = context.getSharedPreferences(Constants.SHARED_PREF_FILE, Context.MODE_PRIVATE);
                                    MobileServiceAuthenticationProvider provider = Utilities.getProviderFromName(pref.getString(Constants.LAST_PROVIDER_PREF, null));
                                    authenticate(context, provider, true);
                                }
                            });
                        }

                        // Wait for authentication to complete then update the token in the request.
                        waitAndUpdateRequestToken(request);
                        mAtomicAuthenticatingFlag.set(false);
                    }
                }
            }
        }
        return future;
    }
}

The authenticate method ( I modified some little things for correct showing of dialog and main activity, but the way it works should be the same as the original code from Microsoft):

  /**
     * Returns true if mClient is not null;
     * A standard sign-in requires the client to contact both the identity
     * provider and the back-end Azure service every time the app starts.
     * This method is inefficient, and you can have usage-related issues if
     * many customers try to start your app simultaneously. A better approach is
     * to cache the authorization token returned by the Azure service, and try
     * to use this first before using a provider-based sign-in.
     * This authenticate method uses a token cache.
     *
     * Authenticates with the desired login provider. Also caches the token.
     *
     * If a local token cache is detected, the token cache is used instead of an actual
     * login unless bRefresh is set to true forcing a refresh.
     *
     * @param bRefreshCache
     *            Indicates whether to force a token refresh.
     */
    public boolean authenticate(final Context context, MobileServiceAuthenticationProvider provider, final boolean bRefreshCache) {
        if (mClient== null)
            return false;
        final ProgressDialog pd = null;//Utilities.createAndShowProgressDialog(context, "Logging in", "Log in");

        bAuthenticating = true;

        // First try to load a token cache if one exists.
        if (!bRefreshCache && loadUserTokenCache(context)) {
            Log.d(Constants.TAG, logClassIdentifier+"User cached token loaded successfully");

            // Other threads may be blocked waiting to be notified when
            // authentication is complete.
            synchronized(mAuthenticationLock)
            {
                bAuthenticating = false;
                mAuthenticationLock.notifyAll();
            }

            QueryManager.getUser(context, mClient, mClient.getCurrentUser().getUserId(), pd);
            return true;
        }else{
            Log.d(Constants.TAG, logClassIdentifier+"No cached token found or bRefreshCache");
        }

        // If we failed to load a token cache, login and create a token cache
        init(context);//update context for client

        ListenableFuture<MobileServiceUser> mLogin = mClient.login(provider);

        Futures.addCallback(mLogin, new FutureCallback<MobileServiceUser>() {
            @Override
            public void onFailure(Throwable exc) {
                String msg = exc.getMessage();
                if ( msg.equals("User Canceled"))
                    return;

                if ( pd!= null && pd.isShowing())
                    pd.dismiss();
                createAndShowDialog(context, msg, "Error");

                synchronized(mAuthenticationLock)
                {
                    bAuthenticating = false;
                    mAuthenticationLock.notifyAll();
                }

            }
            @Override
            public void onSuccess(MobileServiceUser user) {
                cacheUserToken(context, mClient.getCurrentUser());
                if(!bRefreshCache)//otherwise main activity is launched even from other activity (like shop activity)
                    QueryManager.getUser(context, mClient, mClient.getCurrentUser().getUserId(), pd);//loads user's info and shows MainActivity
                else if ( pd!= null && pd.isShowing())
                    pd.dismiss();
                synchronized(mAuthenticationLock)
                {
                    bAuthenticating = false;
                    mAuthenticationLock.notifyAll();
                }
                ClientUtility.UserId = mClient.getCurrentUser().getUserId();
            }
        });

        return true;
    }
2
I cannot find a full example for refreshing expired tokens. What makes it more complicated is that the mClient is bound to an activity context which, by the time the token has expired, will probably have been destroyed. Microsoft does not provide a method for handling this case.Lazaros Papadopoulos

2 Answers

1
votes

I think that the API should have a method for refreshing tokens without showing activity ( that as far as I understant, is needed only for inserting credentials; but credentials are not needed for token refreshing). Another solution I am thinking on is to switch to a different cloud services provider, giving up on Microsoft Azure:(

0
votes

The error java.lang.ClassCastException: android.app.Application cannot be cast to android.app.Activity was caused by the method MobileServiceClient.setContext need a context of Activity like activity.this, but the context from activity.getApplicationContext() is a context for the whole android app. It is incorrect usage.

The offical solution for your needs is shown in the section Cache authentication tokens on the client, please refer to it to try solving your issue.