Refresh Azure user token in a Service without an activity in android - android

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

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:(

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.

Related

How to get refresh token using Microsoft Authentication Library (Msal) in android

I am trying to sign in to my android application using Microsoft Single Sign on, using MSAL implemenation as provided here.
In onCreate
mApp = new PublicClientApplication(this.getApplicationContext(), API.CLIENT_ID, API.AUTHORITY);
When the user presses "Sign in with Microsoft" option, I call the method to acquire token as
mApp.acquireToken(this, getResources().getStringArray(R.array.msal_scopes), getAuthInteractiveCallback());
After handling redirect request in onActivityResult, I grab the authentication response at the callback as
private AuthenticationCallback getAuthInteractiveCallback() {
return new AuthenticationCallback() {
#Override
public void onSuccess(AuthenticationResult authenticationResult) {
/* Successfully got a token, use it to call a protected resource */
accessToken = authenticationResult.getAccessToken();
Log.d("AuthSuccess"," "+accessToken);
}
#Override
public void onError(MsalException exception) {
/* Failed to acquireToken */
Log.d("AuthFail"," "+exception.getMessage());
if (exception instanceof MsalClientException) {
/* Exception inside MSAL, more info inside MsalError.java */
} else if (exception instanceof MsalServiceException) {
/* Exception when communicating with the STS, likely config issue */
}
}
#Override
public void onCancel() {
/* User canceled the authentication */
}
};
}
The problem is, AuthenticationResult object gives the access token, but not the refresh token. The object simply does not have refresh token as one of it's parameter. Do I need to further call another method to grab the refresh token as well? How does one get both access and refresh token from microsoft single sign on using MSAL?!
Currently, the library is not exposing the refresh token:
https://github.com/AzureAD/microsoft-authentication-library-for-android/issues/202

Ensuring a valid auth token is always available

I've been trying to figure out how to authenticate users for my android app. It is based on a website which already has a developed api, using JWT to authenticate.
I have come against the problem of refreshing tokens. Let's say I want to fetch something from the API and I need the auth token for that. I check my current auth token. If it is expired, I need to get a new one using some sort of refresh token.
However, it seems like almost no matter how I think of trying to implement it, I run into a few problems:
I don't want the UI thread to wait while I get a new token
I would prefer that I don't have to explicitly check whether the token
is there (and then refresh it) before making any API call
I've come up with one solution that solves #1 and at least minimizes the pain of #2. I can have some sort of getToken method. As an example, using JS style promises because they're easier for me to understand:
function getToken() {
return new Promise((resolve) => {
// Check for token, and return if valid.
// Otherwise, go to the server and get a new one
...
resolve(token)
}
}
// When making an API call
getToken().then((token) => {
// Call API
})
I think I can work this out so that the request will never be running on the UI thread, which solves #1, and as far as #2, it's at least bearable.
My question is this: is there a better way to do this? It kind of seems like AccountManager might be able to handle this sort of thing for me, but the documentation for it is subpar at best, so I'm not sure how I would even implement it. If AccountManager can do it and you know of a good tutorial for it, please comment with that.
A way to accomplish this is intercept a 401 status code and refresh token.
If you are using Volley, you can extend Request class and override parseNetworkEror(VolleyError error) method. If need be, schedule a Job which will refresh the token (JobDispatcher) and trigger an event to communicate UI about the change (EventBus).
The following example is using OAuth authentication, but can be easily changed to implement JWT.
#Override
protected VolleyError parseNetworkError(VolleyError volleyError) {
if (getDataAccess().shouldRefreshToken(volleyError)) {
if (!EventBus.getDefault().hasSubscriberForEvent(TokenRefreshedEvent.class)) {
EventBus.getDefault().register(this);
}
CSApplication app = CSApplication.getInstance();
FirebaseJobDispatcher dispatcher = app.getJobDispatcher(app.getApplicationContext());
Job myJob = dispatcher.newJobBuilder()
.setService(JobRefreshToken.class)
.setTag("REFRESH_TOKEN")
.setTrigger(Trigger.NOW)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.setConstraints(Constraint.ON_ANY_NETWORK)
.build();
int result = dispatcher.schedule(myJob);
if (result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS) {
LogUtils.log(LogUtils.Type.JOB, GsonRequest.class, "Scheduling job refresh token");
} else {
LogUtils.log(LogUtils.Type.JOB, GsonRequest.class, "Error on schedule refresh token");
}
}
return super.parseNetworkError(volleyError);
}
public boolean shouldRefreshToken(VolleyError error) {
boolean shouldRefreshToken = error.networkResponse != null && error.networkResponse.statusCode == 401;
if (shouldRefreshToken) {
Map<String, String> headers = error.networkResponse.headers;
if (headers.containsKey("WWW-Authenticate")) {
String value = headers.get("WWW-Authenticate");
boolean issuerInvalid = value.contains("The issuer is invalid");
shouldRefreshToken = !issuerInvalid;
if (issuerInvalid) {
log(LogUtils.Type.VOLLEY, DataAccess.class, "Issuer do token é inválido");
}
}
}
return shouldRefreshToken;
}
Job Code
getDataAccess().refreshToken(getApplicationContext(), new VolleyCallback<Void>() {
#Override
public void onSuccess(Void aVoid) {
EventBus.getDefault().post(new TokenRefreshedEvent(true));
job.jobFinished(params, false);
log(LogUtils.Type.JOB, JobRefreshToken.class, "Refresh Token job finished");
}
#Override
public void onError(VolleyError error) {
super.onError(error);
EventBus.getDefault().post(new TokenRefreshedEvent(false));
job.jobFinished(params, false);
}
});
return true;
}
What I ended up doing was creating a method getToken which either returns the current token or gets a new one (blocking). With this strategy, I need to make sure that it never gets called from the UI thread. I created a Retrofit2 interceptor which calls getToken. The benefit of this method is that I can just call my Retrofit methods without worrying about the token at all, and it checks for expiration and gets a new one as necessary.

Handle Background Refresh token call in Retrofit parallel network calls

I am new to android programming and Retrofit , I am making a sample app where i have to make two parallel network calls using access token.
The problem comes when access token is expired and return 401 status code , if I see 401 HTTP status code I have to make a call to refresh token with this access token , but problem with parallel calls is that it leads to race condition for refreshing the refresh token , is there any best practice of way to avoid such situation and how to intelligently refresh the token without any conflict.
OkHttp will automatically ask the Authenticator for credentials when a response is 401 Not Authorised retrying last failed request with them.
public class TokenAuthenticator implements Authenticator {
#Override
public Request authenticate(Proxy proxy, Response response) throws IOException {
// Refresh your access_token using a synchronous api request
newAccessToken = service.refreshToken();
// Add new header to rejected request and retry it
return response.request().newBuilder()
.header(AUTHORIZATION, newAccessToken)
.build();
}
#Override
public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
// Null indicates no attempt to authenticate.
return null;
}
Attach an Authenticator to an OkHttpClient the same way you do with Interceptors
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);
Use this client when creating your Retrofit RestAdapter
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(ENDPOINT)
.setClient(new OkClient(okHttpClient))
.build();
return restAdapter.create(API.class);
Check this: Fore more details visit this link
Try to make a queue for the refresh token operations like:
class TokenProcessor {
private List<Listener> queue = new List<Listener>();
private final Object synch = new Object();
private State state = State.None;
private String token;
private long tokenExpirationDate;
public void getNewToken(Listener listener){
synchronized(synch) {
// check token expiration date
if (isTokenValid()){
listener.onSuccess(token);
return;
}
queue.add(listener);
if (state != State.Working) {
sendRefreshTokenRequest();
}
}
}
private void sendRefreshTokenRequest(){
// get token from your API using Retrofit
// on the response call onRefreshTokenLoaded() method with the token and expiration date
}
private void onRefreshTokenLoaded(String token, long expirationDate){
synchronized(synch){
this.token = token;
this.tokenExpirationDate = expirationDate;
for(Listener listener : queue){
try {
listener.onTokenRefreshed(token);
} catch (Throwable){}
}
queue.clear();
}
}
}
This is an example code, how it can be implemented.
To avoid race conditions, you could synchronize refresh token code using ReentrantLock. For instance, if request A and request B try to refresh token at the same time, since code is synchronized, refresh A gets to actually refresh the token. Once it completes, request B will run refreshToken() and there's should be some logic that tells request B that token has already been refreshed. An example could be storing timestamp of when the token refresh happens then check if token has been refreshed last 10 seconds.
val lock = ReentrantLock(true)
fun refreshToken(): Boolean {
lock.lock()
if (token has been refreshed in last 10 seconds): return true
api.refresh()
lock.unlock()
}
If you don't want to use last 10 seconds logic, here's a different approach. Whenever you refresh token, backend returns {accessToken, expiration-timestamp}. Now, request A saves this token and expiration in disk. Request B will just need to check to make sure token is not expired using the timestamp. If request B gets 401 and token has not expired, it means request A has refreshed the token. Sample code:
val lock = ReentrantLock(true)
fun refreshToken(): Boolean {
lock.lock()
if (token has not expired): return true
api.refresh()
lock.unlock()
}
Otherwise, you probably have to create a queue for refresh token operations as mentioned above.

How and when is the token refreshed when using BMS Client, and is there a way to force the refresh?

We are using:
BMSClient.getInstance().registerAuthenticationListener("realm", new CustomAuthentication(this));
and:
AuthorizationManager.createInstance(this.getApplicationContext());
AuthorizationManager.getInstance().setAuthorizationPersistencePolicy(AuthorizationManager.PersistencePolicy.ALWAYS);
to store the authorization data on the phone. "The authorization data will be saved on local storage" is set to ALWAYS.
The code above is always run on our splashscreen on startup so it is always run when the app restarted.
The problem we have had is that after some time (hours or days) when what we suspect the token has expired we get response in the form of HTTP 307. Even after restarting the app we keep getting this response on our requests. The only way to get around it is to go into the app from settings and clear all data.
The following questions would help us go forward in our testing and possible solution:
How long is the token cached in BMSClient? (testing purposes)
Can AuthorizationManager help us in any way to force a new fetch of token?
Are they working on log out functionality?
Our custom listener:
public class CustomAuth implements AuthenticationListener {
private Context activityContext;
public CustomAuth(Context activityContext) {
this.activityContext = activityContext;
}
#Override
public void onAuthenticationChallengeReceived(AuthenticationContext authContext, JSONObject challenge, Context context) {
//1. read the challenge JSONObject
//2. handle the challenge (use the context for handling UI based operations)
//3. return response using the AuthenticationContext authContext
SharedPreferences preferences = activityContext.getSharedPreferences("UserPreference", Context.MODE_PRIVATE);
String email = preferences.getString("email", "");
if(email.equals("")) {
email = "unidentified-user#error.com";
}
JSONObject jsonEmail = new JSONObject();
try {
jsonEmail.put("email", email);
} catch (JSONException e) {
authContext.submitAuthenticationChallengeAnswer(null);
}
authContext.submitAuthenticationChallengeAnswer(jsonEmail);
}
#Override
public void onAuthenticationSuccess(Context context, JSONObject info) {
//additional operations in case of authentication success
Log.d("Authentication", "Auth success: " + String.valueOf(info));
}
#Override
public void onAuthenticationFailure(Context context, JSONObject info) {
//additional operations in case of authentication failure
Log.d("Authentication", "Auth failure ." + String.valueOf(info));
}
}
Have you tried using AuthorizationManager.clearAuthorizationData() API when 307 is received and resending the request?
To answer your questions:
1) The authorization token will be cached indefinitely. The token expires after a 60 minute time period, but will remain cached until a new token is obtained.
It is best practice to obtain a new token once the original token has expired. This can be accomplished by running a new authorization challenge once the previous token has expired.
2) You can always use AuthorizationManager to obtain a new token once the previous token has expired by accessing a protected resource, using obtainAuthorizationHeader, etc.
3) There is currently no way to log out of MCA using the AuthorizationManager. I will speak with the development team about future plans.
In regards to the main problem I see in your question. I would expect you are experiencing this problem because you are attempting to use an expired token against the authorization service. Even though the token is still cached on the device it expired an hour after creation. I would attempt to run a new authorization challenge against the MCA service once the token expires.
If you want to provide your code that may also help me investigate further.

Quickblox manage session and recalling?

I am working with QuickBlox library for video chat. How can i manage it session?? because when i move to the next activity from the live chat activity i just lost the session because it says "Chat can't initialized" then i have to create the session again to do the calling. So what's the lifetime of quickblox session and how can i manage that.
I am also facing problem with recalling when stop the call or move to the next activity and try to recall i was not able to do that actually i tried different things so each time i am getting different errors. So if any one has experience with QuickBlox library need help here.
When i stop a call i call this function.
private void stopCall() {
//Toggle view show the smile view again
//ToggleSmileView();
try
{
cancelCallTimer();
if (videoChat != null) {
videoChat.stopCall();
videoChat = null;
}
if (videoChannel != null) {
videoChannel.close();
videoChannel = null;
}
sessionId = null;
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
and when i do the call i call this function
private void call() {
//toggle view
//ToggleSmileView();
// get opponent
//
VideoChatApplication app = (VideoChatApplication)getApplication();
opponent = new QBUser();
opponent.setId((app.getCurrentUser().getId() == VideoChatApplication.FIRST_USER_ID ? VideoChatApplication.SECOND_USER_ID : VideoChatApplication.FIRST_USER_ID));
// call
//
callTimer = new Timer();
callTimer.schedule(new CancelCallTimerTask(), 30 * 1000);
createSenderChannel();
initVideoChat();
if (videoChat != null)
{
videoChat.call(opponent, getCallType(), 3000);
//toggleMicrophoneMute();
}
else
{
logAndToast("Stop current chat before call");
}
}
For: Lifetime of quickblox session and how can i manage that.
To authenticate your application you have to set valid a auth_key and
generate a signature using your application auth_secret and receive a
session token which you should use to send requests to QuickBlox API
And,
Expiration time for token is 2 hours. Please, be aware about it. If
you will perform query with expired token - you will receive error
Required session does not exist.
Source: Authentication and Authorization Session Info
That part fits the Android sample code of creating the session,
QBAuth.createSession(new QBEntityCallbackImpl<QBSession>() {
#Override
public void onSuccess(QBSession session, Bundle params) {
Log.i(TAG, "session created, token = " + session.getToken());
}
#Override
public void onError(List<String> errors) {
}
});
Source: Android developers documentation
I have worked with the Android SDK, and feel it still needs some work, esp to reach a stage equivalent to the iOS SDK and REST API.
Though looking at your code, you should use getToken() before creating the new QBUser and related video chat calls, if the token has expired, just create a new one.
I have implemented similar code, not a video chat application, but in a general manner, write the functions in onSuccess() of session creation if the session needs to be recreated.
Fyi, for the multiple ones, you can try checking the error with the summary that has been given, categorized into 4; ..developers/Errors

Categories

Resources