box / box-android-sdk

Apache License 2.0
62 stars 68 forks source link

What is the best practice to switch accounts without re login? #367

Open fiasko131 opened 6 years ago

fiasko131 commented 6 years ago

Hello, My application uses multiple cloud providers and for each provider uses multiple accounts. Once I create an account I store the accessToken and the refreshtoken etc ... in database. Then when I want to reconnect or change account here is how I proceed:

    // inition of BoxConfig
    BoxConfig.IS_LOG_ENABLED = false; //<------???
    BoxConfig.CLIENT_ID = BOX_CLIENT_ID;
    BoxConfig.CLIENT_SECRET = BOX_CLIENT_SECRET;
    BoxConfig.REDIRECT_URL = "https://localhost";

    mBoxSession = new BoxSession(getActivity(),null);
    // here i set the stored accessToken and refreshtoken for a given account
    mBoxSession.getAuthInfo().setAccessToken(item.accessToken);
    mBoxSession.getAuthInfo().setRefreshToken(item.refreshToken);

    mBoxSession.authenticate().addOnCompletedListener(new 
    BoxFutureTask.OnCompletedListener<BoxSession>() {
               @Override
               public void onCompleted(BoxResponse<BoxSession> response) {

                  if (response.isSuccess()) {
                      // if success i do my stuff
                  } else {
              // if not i call onRefreshed 
                    onRefreshed(mBoxSession.getAuthInfo());

                  }
              }
    });                                                                                             

    @Override
    public void onRefreshed(final BoxAuthentication.BoxAuthenticationInfo info) {
    // here i set the new accessToken and refreshToken
        currentBoxAccessToken = info.accessToken();
        currentBoxRefreshToken = info.refreshToken();
        mBoxSession.getAuthInfo().setAccessToken(currentBoxAccessToken);
        mBoxSession.getAuthInfo().setRefreshToken(currentBoxRefreshToken);
        mFolderApi = null;
        mFileApi = null;
       // and i store the new access and refresh token
       // and finaly i do my previous stuff
    }

But that does not prevent sometimes that the view of login appears ???

doncung commented 6 years ago

If you do not want the login activity to ever appear you will want to set an AuthenticationRefreshProvider either globally in BoxAuthentication or locally in your session. Then you can decide what to do when launchAuthUi is called.

If you are still using our login UI for the beginning login and to handle log out situations you can provide an AuthStorage implementation in BoxAuthentication. This will allow you to store and retrieve users from your database instead of our default shared pref based implementation.

fiasko131 commented 6 years ago

So according to you, my logic of storing and refreshing tokens is not enough to prevent login screen appears? Do you have an example using AuthStorage and especially an example of the reconnection process? Thanks a lot.

fiasko131 commented 6 years ago

So I try to understand the logic of your sdk:

Can you tell me where is the code that triggers the account selection view (with the toast: "you have been logged out")? I found the ChooseAuthenticationFragment fragment that filled the listview. Failing I could try to ensure that only the account interested in the login out is present in the list because here is what I get with three accounts: screenshot

As you can see, the listview has 2 empty cells and the 3rd one does not correspond to the account concerned. if the user clicks on an "empty" cells the application crash with a nullpointer.

So can yo help me to intercept the log out and see what I can do with the authStorage , or to fill this list only with the right account?

Thank you for your patient

doncung commented 6 years ago

The reason you are seeing the choose account UI is because the BoxSession object is being constructed with null for the user id. This has a special meaning to the session object, being that the user id will be chosen by the user and so when authenticate is called the UI is shown so that the user chooses which user to authenticate. If you want to login a particular user, you can just specify the userId in which case the logic will get the required access and refresh tokens from the LocalAuthStorage implementation (default to shared prefs). By default if the BoxSession is constructed with just the context it will default to the last authenticated user.

You will want to call BoxAuthentication.setAuthStorage sometime early in the lifetime of your application, ideally inside of your application's onCreate flow. At that point the SDK will refer to your implementation of this class to populate the UI. I assume what has happened is that some odd values have been put into your shared prefs which has caused the empty cells and the unknown account.

fiasko131 commented 6 years ago

OK... So instead of set the tokens I set the userId that I get in my database:

    mBoxSession = new BoxSession(getActivity(),userId);//<--- that i stored during account's creation                                           
    mBoxSession.authenticate().addOnCompletedListener(new 
    BoxFutureTask.OnCompletedListener<BoxSession>() {
               @Override
               public void onCompleted(BoxResponse<BoxSession> response) {//<--- never returned                             
                  if (response.isSuccess()) {
                      // if success i do my stuff
                  } else {
              // if not i call onRefreshed 
                    onRefreshed(mBoxSession.getAuthInfo());                                        
                  }
              }
    }); 

Yes but in this case the onCompleted is never returned?

So I do without:

 activity.mBoxSession = new BoxSession(getActivity(),mBoxUserId);
 activity.mBoxSession.setSessionAuthListener(FragmentClouds.this);//<----FragmentClouds implements 
 //the listener (onRefreshed, onFailure ,onAuthCreated...etc

 //do my stuff

It works .... but when the refrestime has expired and I want to reconnect, nothing happens. the login UI does not appear at all ???

and i obtain the system:out:

I/System.out: falsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalseunhandled json member '250xxx610' xxx # this is the userid that i want to reconnect{"access_token":"psnGxQaTcWxxxxxxxOfGtxK7drPsR","refresh_token":"pSY7lRQPiyhUkbrsLC3n1eZFdGAopuNVkxxxxxxxxxxxxxls6YqOBaWwGGCgfj","refresh_time":1529046901512,"client_id":"ejmdy01ay28rm6fok7wb8fhxyga3iawn","user":{"type":"user","id":"250942610","name":"my name","login":"xxxxxx....... I/System.out: unhandled json member '221xxx241' xxx {"access_token":"gGWgJd1L64tLyDEEKxxxxxxSeFNL","refresh_token":"Bap0wN1tn7jeQbO2YmF6SLwAZqaJKlmagjORU6aeGOKH93IBKknaKrIXlm7SjnPT","refresh_time":1529047563773,"client_id":"ejmdy01ay28rm6fok7wb8fhxyga3iawn","user":{"type":"user","id":"221113241","name":"thomaxxxx@hotmail.fr","login":"thomaxxx@hotmail.fr", I/System.out: unhandled json member '189xxx696' xxx {"access_token":"PaYed94PcOnfz7lxxxx0Fi5c6qcQ8G","expires_in":3927,"restricted_to":"[]","refresh_token":"WbA1laRU508CoWBTdwfJgmv2tARADLNq7ARKifuzwl8NgMxxxxxxsokROOTOu","token_type":"bearer","user":{"type":"user","id":"189574696","name":"pierrexxxxx@gmail.com","login":"pierrxxxx@gmail.com","created_at I/System.out: unhandled json member 'restricted_to' xxx "[]" current object class com.box.androidsdk.content.auth.BoxAuthentication$BoxAuthenticationInfo I/System.out: unhandled json member 'token_type' xxx "bearer" current object class com.box.androidsdk.content.auth.BoxAuthentication$BoxAuthenticationInfo

and:

I/BoxContentSdk: Request (GET): https://api.box.com/2.0/folders/0/items?offset=0&limit=1000&fields=size%2Ccreated_at%2Cextension%2Cid%2Cparent%2Cname%2Cpath_collection%2Cid%2Ccreated_at%2Cparent I/BoxContentSdk: Request Header: Authorization:Bearer PaYed94PcOnfz7l6gJ0E30Fi5c6qcQ8G I/BoxContentSdk: Request Header: User-Agent:com.box.sdk.android I/BoxContentSdk: Request Header: Accept-Encoding:gzip I/BoxContentSdk: Request Header: Accept-Charset:utf-8 I/BoxContentSdk: Request Header: Content-Type:application/json I/BoxContentSdk: Response (401):

401 unauthorised? the

catch (BoxException e) {
      e.printStackTrace()
}

catch nothing.

What i am doing wrong?

I don't understand why is so tricky to switch between accounts :((

doncung commented 6 years ago

Generally shouldn't be, we use the SDK in our application. Are you still using your database to keep any refresh/access tokens or did you logout any of the users? It looks like the api call being made is for a different account 221113241 in this case. Logout will invalidate both the access and refresh tokens for a given user so it is likely that is the cause of the error.

After setting mBoxSession to your new BoxSession with the 250xxx610 you have to recreate any BoxApi objects you are using to make requests. The SDK is designed to allow you to make api calls in parallel with multiple users, you just need to construct your api objects with the session tied to the user you want. Based off your log the call is being made for a different user.

fiasko131 commented 6 years ago

My goal like with other cloud providers that I use, is to move from one account to another without having to identify me again once the creation is done.

So with the latest version I used, ie by building the session with the userid new BoxSession (context, userId), and never calling logout ... is there a reason for the sdk Logout himself?

Whenever I build a BoxSession with the userId, of course I rebuild FileApi and FolderApi with the new session.

So can you confirm that by doing this:

//click to connect account 1
mBoxSession = null;
mFileApi = null;
mFolderApi = null
mBoxSession = new BoxSession(getActivity(),userId1);
mFileApi = new FileApi(mBoxSession)
mFolderApi = new FolderApi(mBoxSession)
// build my list of items account 1

//click to connect account 2
mBoxSession = null;
mFileApi = null;
mFolderApi = null
mBoxSession = new BoxSession(getActivity(),userId2);
mFileApi = new FileApi(mBoxSession)
mFolderApi = new FolderApi(mBoxSession)
// build my list of items account 2

it's enough to go from one account to another and I would not log out with the toast "you have been logged out"? and so in this case no more listener is useful?

What is strange is that it works for a given time the refreshtime?

dblankety commented 6 years ago

Logout is a security measure. It invalidates tokens on the server side, and removes the app from the logged in list in the user's account.

One possible reason for some of your issues could be the listener you are setting is based on a shared activity. Even though you are replacing the pointer for your previous session, the listener for it can still be called as the logic is based on weak references and the OS doesn't need to clean it up.

Your shared listener logic needs to differentiate different users, or you need multiple listeners.

On Fri, Jun 15, 2018, 3:14 PM Aristide13 notifications@github.com wrote:

My goal like with other cloud providers that I use, is to move from one account to another without having to identify me again once the creation is done.

So with the latest version I used, ie by building the session with the userid new BoxSession (context, userId), and never calling logout ... is there a reason for the sdk Logout himself?

Whenever I build a BoxSession with the userId, of course I rebuild FileApi and FolderApi with the new session.

So can you confirm that by doing this:

//click to connect account 1 mBoxSession = null; mFileApi = null; mFolderApi = null mBoxSession = new BoxSession(getActivity(),userId1); mFileApi = new FileApi(mBoxSession) mFolderApi = new FolderApi(mBoxSession)// build my list of items account 1 //click to connect account 2 mBoxSession = null; mFileApi = null; mFolderApi = null mBoxSession = new BoxSession(getActivity(),userId2); mFileApi = new FileApi(mBoxSession) mFolderApi = new FolderApi(mBoxSession)// build my list of items account 2

it's enough to go from one account to another and I would not log out with the toast "you have been logged out"? and so in this case no more listener is useful?

What is strange is that it works for a given time the refreshtime?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/box/box-android-sdk/issues/367#issuecomment-397755513, or mute the thread https://github.com/notifications/unsubscribe-auth/AM2YsJx6wCPqvDSQiFZAJaN3gEG2oSjoks5t9DHGgaJpZM4UmPP0 .

fiasko131 commented 6 years ago

OK @dblankety so for the listener issue if I proceed as follows ?:

//click item1 to connect account 1
mUserId = item1.getUserId();
activity.mBoxSession.setSessionAuthListener(null);
mBoxSession = null;
mFileApi = null;
mFolderApi = null
mBoxSession = new BoxSession(getActivity(),mUserId);
activity.mBoxSession.setSessionAuthListener(listener);
mFileApi = new FileApi(mBoxSession)
mFolderApi = new FolderApi(mBoxSession)
// build my list of items account 1

//click item2 to connect account 2
mUserId = item2.getUserId();
activity.mBoxSession.setSessionAuthListener(null);
mBoxSession = null;
mFileApi = null;
mFolderApi = null
mBoxSession = new BoxSession(getActivity(),mUserId);
activity.mBoxSession.setSessionAuthListener(listener);
mFileApi = new FileApi(mBoxSession)
mFolderApi = new FolderApi(mBoxSession)
// build my list of items account 2
...........
 BoxAuthentication.AuthListener listener = new BoxAuthentication.AuthListener() {
               @Override
                public void onRefreshed(BoxAuthentication.BoxAuthenticationInfo   info) {
                          // or if i get the same listener
                          if (info.getUser().getId() == mUserId) // do stuff
                }
                .......
  };

Also as @doncung explained to me that the box application was using your Sdk, i installed it. I configured three accounts about ten hours ago ... for the moment I did not have to redo a login , unlike my case???

After how long does the security logout occur with the default refreshtime?

doncung commented 6 years ago

Looks like it should work. The SDK handles refreshing automatically so as long as you don't explicitly call logout or get an event that would trigger an invalid refresh the users should remain logged in.

fiasko131 commented 6 years ago

@doncung no, the principle mentioned above does not work. After a certain duration ?? the requests are no longer accepted ... To try to use your sdk in my application I did this, every time i change my session for an userId:

activity.mBoxSession = new BoxSession(getActivity(),mBoxUserId);
activity.mBoxSession.setSessionAuthListener(listener);
activity.mBoxSession.refresh().addOnCompletedListener(new BoxFutureTask.OnCompletedListener<BoxSession>() {
        @Override
        public void onCompleted(BoxResponse<BoxSession> response) {
        if (response.isSuccess()){
                   // do my stuff                                             
        }else {
                   //do my stuff                                             
        }                                                
 }); 

It works but the response.issucces() return false.... But sometimes after a while onCompleted is not returned ??? And I have to restart the application ... but at least I do not have to log in again ... Really I do not understand the problem and the logic of this sdk, and examples with the multiple accounts are inexistent.

doncung commented 6 years ago

We will try to improve our documentation for this use case. A failed refresh can cause a logout of the user since it means the access and refresh tokens cannot be used and requires a new login. Is there an exception you are seeing in the response.getException() in the situation it returns false. I'll see if I can reproduce with that sample, but if not the stack trace of the exception can help.

Also when you say after a while, what do you mean? the OnCompletedListener added to the refresh is specific to that forced refresh() call. The listener added to the session on the other hand should be getting called as long as that session is still in memory. You can also register a listener on BoxAuthentication.getInstance() which should get a call as long as your listener is in memory.

fiasko131 commented 6 years ago

@doncung I will try to catch the exception with response.getException() and give it to you. But in the mentionned case, onCompleted() is not called at all. Thank you again for your support.