Note : This post is part of the series "Lessons learned as an independent developer". Please refer to the introduction (in French) for more information. Cet article fait partie de la série intitulée « Leçons d'un développeur indépendant ». Au besoin, lisez mon introduction pour plus d'informations.
Context
AnotherSocialEconomy APIs are based on standards as much as possible: OpenID and OAuth for the authentication and authorization, HTTP-based REST interface (1, 2) for the communication protocol, JSON for the data payload format, etc.
This post is about setting up a Android application which get authorization tokens from a OAuth provider.
OAuth provider
There are many known OAuth providers like Netflix, Twitter, Facebook (coming to OAuth 2.0 soon), Yahoo!, Google, etc. If these providers are convenient, they don't offer much flexibility if some debugging is required.
For this experiment, I'm going to use Google App Engine Java and their OAuth support. For a complete walk-through, refer to Ikai Lan's post: Setting up an OAuth provider on Google App Engine, especially for the part which describes how to get the public and secret keys for your client application to sign communications with the provider.
OAuth client - Work flow
Strategy: Upon creation, the process checks if the authorization tokens have been saved as user preferences.
- If they are present, they are loaded to be used to sign each future communication with the application on the server.
- If they are missing, the OAuth work flow is triggered:
- With the server application keys, a signed request is sent to get a temporary request token.
- With this request token, a URL to the authentication page is required and an
Intent
is created to load the corresponding page in a browser. At this step, the application is stopped.
- The user enters his credentials in the browser and grants access rights to the mobile application. The return URL has a custom format:
ase://oauthresponse
. - The mobile application, which has an
Intent
registered for that custom URL, is restarted and is given the return URL. A verification code is extracted from this URL. - The verification code is used to issue a signed request asking for the access tokens.
- The access tokens are saved as part of the user preferences only if she selected a
'Remember me'
option.
Figure 1: Authorization work flow |
Alternative: If the mobile application offers anonymous services, like browsing the list of registered stores in the case of AnotherSocialEconomy.com, it can be friendlier to delay the authorization verification.
OAuth client - Initiating the authorization process (1, 2, 3)
To simplify the application development, I have decided to use oauth-signpost, a library provided by Matthias Käppler who wanted a slick and simple way to access Netflix services.
Signpost is the easy and intuitive solution for signing HTTP messages on the Java platform in conformance with the OAuth Core 1.0a standard. Signpost follows a modular and flexible design, allowing you to combine it with different HTTP messaging layers.Note that this library is also good to manage remotely Twitter accounts.
This section is about initiating the authorization process, which occurs if the application is not called by the application on the server (with the verification code, see next section) and if the OAuth token could not be found in the user preferences. This is the path with the steps {1, 2, 3} in Figure 1.
if (!justAuthenticated && Preferences.get(Preferences.OAUTH_KEY, "").length() == 0) { // Display the pane with the warning message and the sign in button setContentView(R.layout.main_noauth); // Update the 'Remember me' checkbox with its last saved state, or the default one final String saveOAuthKeysPrefs = Preferences.get(Preferences.SAVE_OAUTH_KEYS, Preferences.SAVE_OAUTH_KEYS_DEFAULT); ((CheckBox) findViewById(R.id.app_noauth_keepmeconnected)).setChecked(Preferences.SAVE_OAUTH_KEYS_YES.equals(saveOAuthKeysPrefs)); // Attach the event handler that will initiate the authorization process up to opening the browser with the authorization page findViewById(R.id.app_noauth_continue).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Check if the 'Keep me connected' check box state changed and save its new state boolean keepMeConnected = ((CheckBox) findViewById(R.id.app_noauth_keepmeconnected)).isChecked(); if (Preferences.SAVE_OAUTH_KEYS_YES.equals(saveOAuthKeysPrefs) != keepMeConnected) { Preferences.set(Preferences.SAVE_OAUTH_KEYS, keepMeConnected ? Preferences.SAVE_OAUTH_KEYS_YES : Preferences.SAVE_OAUTH_KEYS_NO); } // Set up the OAuth library consumer = new CommonsHttpOAuthConsumer("<your_app_public_key>", "<your_app_secret_key>"); provider = new CommonsHttpOAuthProvider( "https://<your_app_id>.appspot.com/_ah/OAuthGetRequestToken", "https://<your_app_id>.appspot.com/_ah/OAuthAuthorizeToken", "https://<your_app_id>.appspot.com/_ah/OAuthGetAccessToken"); try { // Steps 1 & 2: // Get a request token from the application and prepare the URL for the authorization service // Note: the response is going to be handled by the application <intent/> registered for that custom return URL String requestTokenUrl = provider.retrieveRequestToken(consumer, "ase://oauthresponse"); // Step 3: // Invoke a browser intent where the user will be able to log in startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(requestTokenUrl))); } catch(Exception ex) { Toast.makeText(Dashboard.this, R.string.app_noauth_requesttoken_ex, Toast.LENGTH_LONG).show(); Log.e("Dashboard no auth", "Cannot initiate communication to get the request token\nException: " + ex.getClass().getName() + "\nMessage: " + ex.getMessage()); } } }); }
Figure 2 below illustrates the pane
main_noauth
displaying the warning message and the action button, and figure 3 shows the authorization page as provided by Google for the hosted applications on App Engine.Figure 2: Pane displayed if application not yet authorized | Figure 3: Google authorization page |
Whatever action the user takes, the application is going to be called with the URL
ase://oauthresponse
. The next section covers this work flow path.OAuth client - Processing the authorization (4, 5)
The application is registered with an
Intent
associated to the scheme ase
and the host oauthresponse
. The labels themselves are not important, only their uniqueness and the correspondence with the return URL specified at Step 2.<intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="ase" android:host="oauthresponse"/> </intent-filter>
The following code snippet implements the steps 4 and 5 as described in Figure 1.
private boolean checkOAuthReturn(Intent intent) { boolean returnFromAuth = false; Uri uri = intent.getData(); if (uri != null && uri.toString().startsWith("ase://oauthresponse")) { // Step 4: // Get the request token from the Authentication log in page String code = uri.getQueryParameter("oauth_verifier"); try { // Step 5: // Get directly the access tokens provider.retrieveAccessToken(consumer, code); returnFromAuth = true; // Persist the tokens if (Preferences.SAVE_OAUTH_KEYS_YES.equals(Preferences.get(Preferences.SAVE_OAUTH_KEYS, Preferences.SAVE_OAUTH_KEYS_DEFAULT))) { Preferences.set(Preferences.OAUTH_KEY, consumer.getToken()); Preferences.set(Preferences.OAUTH_SECRET, consumer.getTokenSecret()); } } catch(Exception ex) { Toast.makeText(Dashboard.this, R.string.app_noauth_accesstoken_ex, Toast.LENGTH_LONG).show(); Log.e("Dashboard no auth", "Cannot complete communication to get the request token\nException: " + ex.getClass().getName() + "\nMessage: " + ex.getMessage()); } } return returnFromAuth; }
The
Dashboard
class definitions are available in a gist on GitHub. This gist contains also a wrapper of the SharedPreferences
class, the application manifest with the declaration of the Intent
for the custom return URL, and the layout definition of the pane with the warning and the sign in button.OAuth Client - The quirks
My Android application is very simple and is configured with the launch mode
singleTop
. As such, if the system does not destroy the application when the code starts an activity to browse the Authentication service URL, the invocation of the ase://oauthresponse
URL by the browser should trigger a call to the onNewIntent()
method. It never happened during my tests and on my phone... Every time, the application is recreated and a call to onCreate()
is issued. So both functions delegate to the helper checkOAuthReturn()
.@Override protected void onNewIntent(Intent intent) { checkOAuthReturn(intent); }
In this example, I've decided to select the view to associate to the first screen of the application according to the knowledge of the OAuth access token (read from the user preferences or retrieved dynamically thanks to the verification code coming with the
ase://oauthresponse
URL). The following snippet illustrates this flow. In some occasions, it can be better to start a separate activity if the main pane is instrumented to disable the triggers to protected actions. This approach with a separate activity is also better for the portability.@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Preferences.setPreferenceContext(PreferenceManager.getDefaultSharedPreferences(getBaseContext())); boolean justAuthenticated = checkOAuthReturn(getIntent()); if (!justAuthenticated && Preferences.get(Preferences.OAUTH_KEY, "").length() == 0) { setContentView(R.layout.main_noauth); // Instrumentation of the pane to initiate the authorization process on demand // ... } else { setContentView(R.layout.main); } }
I hope this helps.
A+, Dom
No comments:
Post a Comment