OAuth instance state in Android
Asked Answered
I

6

9

I'm trying to use OAuth in an Android app. I have it working correctly but have sometimes run into a problem during the authentication phase. In Android, I launch the browser for the user to login and authenticate. Then the callback url will redirect back to my application.

Here is the problem. My application has a OAuth consumer and provider as members of my main class. When the browser is launched for authentication, sometimes my main Activity is discarded to save memory. When the callback url relaunches my main Activity, the provider and consumer are new instances and therefor don't work when I try to make a request to the api. If the main Activiy was not freed during the authentication phase, then everything works correctly because I'm still working with the original consumer and provider.

I tried using onSaveInstanceState() and onRestoreInstanceState(), but haven't been successful. It seems the onRestoreInstanceState() is not called when my callback url is handled. Seems to go straight to the onResume().

What is the correct method for persisting the consumer and provider in this case?

Interlocutress answered 27/12, 2009 at 9:19 Comment(0)
P
3

Complete Save / restore solution

Apart from the request_token and token_secret, the isOauth10a() state is important to be restored in the provider. There could be more state information in the future. Hence, I like the persist and load solution the best.

I extended GrkEngineer's solution to be more complete. It saves / restores both the provider and consumer, handles all exceptions, and sets the httpClient while restoring.

protected void loadProviderConsumer()
{
  try {
    FileInputStream fin = this.openFileInput("tmp_provider.dat");
    ObjectInputStream ois = new ObjectInputStream(fin);
    provider = (CommonsHttpOAuthProvider) ois.readObject();
    provider.setHttpClient(httpClient);
    ois.close();
    fin.close();

    fin = this.openFileInput("tmp_consumer.dat");
    ois = new ObjectInputStream(fin);
    consumer = (CommonsHttpOAuthConsumer) ois.readObject();
    ois.close();
    fin.close();

    Log.d("OAuthTwitter", "Loaded state");
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (StreamCorruptedException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
  }
}

protected void persistProviderConsumer()
{

  try {
    FileOutputStream fout = this.openFileOutput("tmp_provider.dat", MODE_PRIVATE);
    ObjectOutputStream oos = new ObjectOutputStream(fout);
    oos.writeObject(provider);
    oos.close();
    fout.close();

    fout = this.openFileOutput("tmp_consumer.dat", MODE_PRIVATE);
    oos = new ObjectOutputStream(fout);
    oos.writeObject(consumer);
    oos.close();
    fout.close();

    Log.d("OAuthTwitter", "Saved state");
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }
}

I have tested this code and it works.

Pontic answered 3/12, 2010 at 18:47 Comment(0)
I
3

You can read my old post here. Generally what I've done was to use static reference and using Activity with WebView instead of standalone browser to display authentication form

Icing answered 28/12, 2009 at 0:56 Comment(4)
I actually started with your old post. Very helpful. I might try your solution, but I wanted to try something else to help me better understand some of the OAuth stuff. I came across this example, code.google.com/p/jpoco/source/browse/trunk/jpoco-android-app/… in which he stored the various tokens as SharedPreferences for the application. If I wanted to do something similar, what parts of the consumer and provider would I need to save? I also thought that this might be helpful in not having to re-authenticate each time the app is run.Interlocutress
You don't have to re-authenticate, just save the token and reuse it. Twitter does not expire it. You right - in my example I wasn't trying to understand how the signing is done (for example) but merely how to use it. From my experience with DroidIn and from the feedback I'm getting - it works pretty well. Alternatively I see people using key generating solution where user after authentication is presented with key which he needs to input back at the application signing page but that feels like more painIcing
Yea your example was really helpful. I guess I was curious to understand if the provider/consumer fields could be saved to disk using SharedPreferences. Then when the callback returns, you could repopulate the provider/consumer with the saved fields.Interlocutress
I struggle with that for a while but being lazy found it easier to save these into a singleton which unfortunately only works while your app stays in foreground, calling the browser often results in restarting the app thread and then provider/consumer are gone. I found using WebView activity simpler solution than serializing to diskIcing
I
3

I solved this by persisting the provider object to a file. I'm using the signpost library and both the provider and consumer are serializable.

protected void loadProvider()
{
    FileInputStream fin = this.openFileInput("provider.dat");
    ObjectInputStream ois = new ObjectInputStream(fin);
    this.provider = (DefaultOAuthProvider) ois.readObject();
    ois.close();
    consumer = this.provider.getConsumer(); 
}

protected void persistProvider()
{
    FileOutputStream fout = this.openFileOutput("provider.dat", MODE_PRIVATE);
    ObjectOutputStream oos = new ObjectOutputStream(fout);
    oos.writeObject(this.provider);
    oos.close();
}

I call persist provider just before launching the browser view intent for authentication, and I restore the provider in onResume() just before calling provider.retrieveAccessToken(). If you call persistProvider() and loadProvider() in a couple more locations, you can also have it save the proper tokens post-authentication. This would remove the need to re-authenticate (as long as the token is valid).

I do still wish I knew which fields in the provider class are actually needed to persist. Might be kind of slow to serialize the entire object.

Interlocutress answered 29/12, 2009 at 4:50 Comment(1)
I enhanced this solution with success. See my answer.Pontic
P
3

Complete Save / restore solution

Apart from the request_token and token_secret, the isOauth10a() state is important to be restored in the provider. There could be more state information in the future. Hence, I like the persist and load solution the best.

I extended GrkEngineer's solution to be more complete. It saves / restores both the provider and consumer, handles all exceptions, and sets the httpClient while restoring.

protected void loadProviderConsumer()
{
  try {
    FileInputStream fin = this.openFileInput("tmp_provider.dat");
    ObjectInputStream ois = new ObjectInputStream(fin);
    provider = (CommonsHttpOAuthProvider) ois.readObject();
    provider.setHttpClient(httpClient);
    ois.close();
    fin.close();

    fin = this.openFileInput("tmp_consumer.dat");
    ois = new ObjectInputStream(fin);
    consumer = (CommonsHttpOAuthConsumer) ois.readObject();
    ois.close();
    fin.close();

    Log.d("OAuthTwitter", "Loaded state");
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (StreamCorruptedException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
  }
}

protected void persistProviderConsumer()
{

  try {
    FileOutputStream fout = this.openFileOutput("tmp_provider.dat", MODE_PRIVATE);
    ObjectOutputStream oos = new ObjectOutputStream(fout);
    oos.writeObject(provider);
    oos.close();
    fout.close();

    fout = this.openFileOutput("tmp_consumer.dat", MODE_PRIVATE);
    oos = new ObjectOutputStream(fout);
    oos.writeObject(consumer);
    oos.close();
    fout.close();

    Log.d("OAuthTwitter", "Saved state");
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }
}

I have tested this code and it works.

Pontic answered 3/12, 2010 at 18:47 Comment(0)
A
2

You only need to persist consumer.getToken() and consumer.getTokenSecret()

Later you can simply recreate a new consumer(customerKey,customerKeySecret) and consumer.setTokenWithSecret(token, tokenSecret)

What was tricky is to find out was the following:

  1. Use CommonsHttpOAuthConsumer and CommonsHttpOAuthProvider (on android) the DefaultOAuthProvider will not work.

  2. When using HttpURLConnection, you cannot sign POST requests that carry query parameters in the message payload

Anabelle answered 25/6, 2010 at 12:23 Comment(1)
Yea that's useful info about the CommonsHttpOAuthConsumer vs DefaultOAuthProvider. As for the saving of the consumer token and secret, this is all I save in between application runs. The original question dealt more with the problem of the application shutting down and restarting when you launch the browser to first sign in.Interlocutress
V
2

Possibly the reason why the original poster had problems maintaining instance state is because the default behaviour for Android is to start a new activity for each new intent. That's why GrkEngineer was not seeing onRestoreInstanceState being called after the web callback.

Storing your request token as a shared preference is one solution so that it can be acccessed from the new activity that is started after the OAuth web callback.

I originally tried using shared preferences and it seemed to work ok. However, I don't think that is the best solution. Ideally, you want to force Android to deliver the callback to your original activity (I'll explain why below).

I tried using the singleTask, and singleInstance launch modes to accomplish this with partial success, but it felt wrong, and the Android docs hint that those modes are not recommended for general use.

After much digging through the documentation and testing I found that using the following flags when creating the intent, causes Android to deliver the intent to an existing instance of the activity (recreating it if it's been killed).

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

The reason why I needed to get the callback to be handled by the original activity was so I could integrate with android AccountManager. I used the following sample to get me started:

http://developer.android.com/resources/samples/SampleSyncAdapter/index.html

One of the key parts of integrating with the AccountManager authenticator mechanism is the AccountAuthenticatorResponse that is passed into your activity to start the authentication process.

I found the big problem with implementing this was keeping a reference to the AccountAuthenticatorResponse object. This is passed into your AuthenticatorActivity and you need to call methods on it once authentication is complete so that the standard accounts UI is left in the correct state. However, I hit the same problem that GrkEngineer initially hit. When I tried to restart my OAuth authenticator activity after the OAuth callback, I was always getting a new instance which had lost the reference to the AccountAuthenticatorResponse object and I couldn't see any way to persist that object.

The key was to use the intent flags I described above.

AuthenticatorActivity is started using FLAG_ACTIVITY_NEW_TASK by my AbstractAccountAuthenticator. It fetches the request token (using AsyncTask) and starts the browser to ask the user to authorize.

OAuthCallbackHandlerActivity is registered to handle my custom callback scheme. When it gets called after the user grants access, it makes a call to AuthenticatorActivity using the flags Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP intent.

This causes my original AuthenticatorActivity to be reactivated. The AccountAuthenticatorResponse object is still available (as are the request token/secret, which I saved in OnSaveInstanceState). The activity can now get the access token (again using AsyncTask) and then call the completion methods on the AccountAuthenticatorResponse object.

The key to making this work was using the intent flags I mentioned, and also making sure that the AuthenticatorActivity is started in your application task rather than the account manager task. FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_SINGLE_TOP will only cause an existing instance of an activity to be reused if they are in the same task. So if the activity that you want to get back to is started in some other task, then the original instance would not be re-used.

I tested this on an emulator using Dev Tools to kill my AuthenticatorActivity immediately so I could test the recreation process. It worked great using onSaveInstanceState/onRestoreInstanceState for the request token/secret. And I didn't even have to worry about restoring the AccountAuthenticatorResponse object. That was restored by Android itself - magic!

Vocalise answered 19/9, 2010 at 19:8 Comment(0)
M
1

I had the same problem. All you need to persist is the requestToken and the tokenSecret that you get after calling retrieveRequestToken. In your onResume() method, recreate the consumer and the provider object as described here. That way you don't need to persist the whole consumer and provider objects and will still be able to retrieve the accessToken.

Mammet answered 15/7, 2010 at 13:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.