Oauth 2.0 authorization for LinkedIn in Android

Even though there is no such android specific sdk from linkedIn(like facebook and twitter sdk for android).Setting up linkedIn authorization with Oauth 1.0 was still easy using:

  • scribe-java
  • Social-auth for android.
  • And the list of tools here.

But its not the same story for authorization with Oauth2.0. Not too many usefull libraries or android specific examples. I tried using these:

  • Android Oauth-client
  • tnj.

I have read that Oauth 2.0 is much simpler to implement than the 1.0. Still I am not able to do so.

Any pointers towards implementing Oauth2.0 for LinkedIn in Android?


Oauth2.0 authentication for LinkedIN.

Step 1:

  • Register your app with linkedIn by following this document. And get your api_key and api_secret.

Step 2:

MainActivity:

public class MainActivity extends Activity {

/*CONSTANT FOR THE AUTHORIZATION PROCESS*/

/****FILL THIS WITH YOUR INFORMATION*********/
//This is the public api key of our application
private static final String API_KEY = "YOUR_API_KEY";
//This is the private api key of our application
private static final String SECRET_KEY = "YOUR_API_SECRET";
//This is any string we want to use. This will be used for avoiding CSRF attacks. You can generate one here: http://strongpasswordgenerator.com/
private static final String STATE = "E3ZYKC1T6H2yP4z";
//This is the url that LinkedIn Auth process will redirect to. We can put whatever we want that starts with http:// or https:// .
//We use a made up url that we will intercept when redirecting. Avoid Uppercases. 
private static final String REDIRECT_URI = "http://com.amalbit.redirecturl";
/*********************************************/

//These are constants used for build the urls
private static final String AUTHORIZATION_URL = "https://www.linkedin.com/uas/oauth2/authorization";
private static final String ACCESS_TOKEN_URL = "https://www.linkedin.com/uas/oauth2/accessToken";
private static final String SECRET_KEY_PARAM = "client_secret";
private static final String RESPONSE_TYPE_PARAM = "response_type";
private static final String GRANT_TYPE_PARAM = "grant_type";
private static final String GRANT_TYPE = "authorization_code";
private static final String RESPONSE_TYPE_VALUE ="code";
private static final String CLIENT_ID_PARAM = "client_id";
private static final String STATE_PARAM = "state";
private static final String REDIRECT_URI_PARAM = "redirect_uri";
/*---------------------------------------*/
private static final String QUESTION_MARK = "?";
private static final String AMPERSAND = "&";
private static final String EQUALS = "=";

private WebView webView;
private ProgressDialog pd;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    //get the webView from the layout
    webView = (WebView) findViewById(R.id.main_activity_web_view);

    //Request focus for the webview
    webView.requestFocus(View.FOCUS_DOWN);

    //Show a progress dialog to the user
    pd = ProgressDialog.show(this, "", this.getString(R.string.loading),true);

    //Set a custom web view client
    webView.setWebViewClient(new WebViewClient(){
          @Override
          public void onPageFinished(WebView view, String url) {
                //This method will be executed each time a page finished loading.
                //The only we do is dismiss the progressDialog, in case we are showing any.
              if(pd!=null && pd.isShowing()){
                  pd.dismiss();
              }
          }
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String authorizationUrl) {
            //This method will be called when the Auth proccess redirect to our RedirectUri.
            //We will check the url looking for our RedirectUri.
            if(authorizationUrl.startsWith(REDIRECT_URI)){
                Log.i("Authorize", "");
                Uri uri = Uri.parse(authorizationUrl);
                //We take from the url the authorizationToken and the state token. We have to check that the state token returned by the Service is the same we sent.
                //If not, that means the request may be a result of CSRF and must be rejected.
                String stateToken = uri.getQueryParameter(STATE_PARAM);
                if(stateToken==null || !stateToken.equals(STATE)){
                    Log.e("Authorize", "State token doesn't match");
                    return true;
                }

                //If the user doesn't allow authorization to our application, the authorizationToken Will be null.
                String authorizationToken = uri.getQueryParameter(RESPONSE_TYPE_VALUE);
                if(authorizationToken==null){
                    Log.i("Authorize", "The user doesn't allow authorization.");
                    return true;
                }
                Log.i("Authorize", "Auth token received: "+authorizationToken);

                //Generate URL for requesting Access Token
                String accessTokenUrl = getAccessTokenUrl(authorizationToken);
                //We make the request in a AsyncTask
                new PostRequestAsyncTask().execute(accessTokenUrl);

            }else{
                //Default behaviour
                Log.i("Authorize","Redirecting to: "+authorizationUrl);
                webView.loadUrl(authorizationUrl);
            }
            return true;
        }
    });

    //Get the authorization Url
    String authUrl = getAuthorizationUrl();
    Log.i("Authorize","Loading Auth Url: "+authUrl);
    //Load the authorization URL into the webView
    webView.loadUrl(authUrl);
}

/**
 * Method that generates the url for get the access token from the Service
 * @return Url
 */
private static String getAccessTokenUrl(String authorizationToken){
    return ACCESS_TOKEN_URL
            +QUESTION_MARK
            +GRANT_TYPE_PARAM+EQUALS+GRANT_TYPE
            +AMPERSAND
            +RESPONSE_TYPE_VALUE+EQUALS+authorizationToken
            +AMPERSAND
            +CLIENT_ID_PARAM+EQUALS+API_KEY
            +AMPERSAND
            +REDIRECT_URI_PARAM+EQUALS+REDIRECT_URI
            +AMPERSAND
            +SECRET_KEY_PARAM+EQUALS+SECRET_KEY;
}
/**
 * Method that generates the url for get the authorization token from the Service
 * @return Url
 */
private static String getAuthorizationUrl(){
    return AUTHORIZATION_URL
            +QUESTION_MARK+RESPONSE_TYPE_PARAM+EQUALS+RESPONSE_TYPE_VALUE
            +AMPERSAND+CLIENT_ID_PARAM+EQUALS+API_KEY
            +AMPERSAND+STATE_PARAM+EQUALS+STATE
            +AMPERSAND+REDIRECT_URI_PARAM+EQUALS+REDIRECT_URI;
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

private class PostRequestAsyncTask extends AsyncTask<String, Void, Boolean>{

    @Override
    protected void onPreExecute(){
        pd = ProgressDialog.show(MainActivity.this, "", MainActivity.this.getString(R.string.loading),true);
    }

    @Override
    protected Boolean doInBackground(String... urls) {
        if(urls.length>0){
            String url = urls[0];
            HttpClient httpClient = new DefaultHttpClient();
            HttpPost httpost = new HttpPost(url);
            try{
                HttpResponse response = httpClient.execute(httpost);
                if(response!=null){
                    //If status is OK 200
                    if(response.getStatusLine().getStatusCode()==200){
                        String result = EntityUtils.toString(response.getEntity());
                        //Convert the string result to a JSON Object
                        JSONObject resultJson = new JSONObject(result);
                        //Extract data from JSON Response
                        int expiresIn = resultJson.has("expires_in") ? resultJson.getInt("expires_in") : 0;

                        String accessToken = resultJson.has("access_token") ? resultJson.getString("access_token") : null;
                        Log.e("Tokenm", ""+accessToken);
                        if(expiresIn>0 && accessToken!=null){
                            Log.i("Authorize", "This is the access Token: "+accessToken+". It will expires in "+expiresIn+" secs");

                            //Calculate date of expiration
                            Calendar calendar = Calendar.getInstance();
                            calendar.add(Calendar.SECOND, expiresIn);
                            long expireDate = calendar.getTimeInMillis();

                            ////Store both expires in and access token in shared preferences
                            SharedPreferences preferences = MainActivity.this.getSharedPreferences("user_info", 0);
                            SharedPreferences.Editor editor = preferences.edit();
                            editor.putLong("expires", expireDate);
                            editor.putString("accessToken", accessToken);
                            editor.commit();

                            return true;
                        }
                    }
                }
            }catch(IOException e){
                Log.e("Authorize","Error Http response "+e.getLocalizedMessage());  
            }
            catch (ParseException e) {
                Log.e("Authorize","Error Parsing Http response "+e.getLocalizedMessage());
            } catch (JSONException e) {
                Log.e("Authorize","Error Parsing Http response "+e.getLocalizedMessage());
            }
        }
        return false;
    }

    @Override
    protected void onPostExecute(Boolean status){
        if(pd!=null && pd.isShowing()){
            pd.dismiss();
        }
        if(status){
            //If everything went Ok, change to another activity.
            Intent startProfileActivity = new Intent(MainActivity.this, ProfileActivity.class);
            MainActivity.this.startActivity(startProfileActivity);
        }
    }

};
}

And the xmlLayout:

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <WebView
        android:id="@+id/main_activity_web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

The token is saved in the sharedpreference file.

Simple android project repo here in github.


I got it working, but it took me... some time.

I followed LinkedIn Authentication to manage that.
I still strongly advice to still read this link, as I do not cover all the cases in my examples (errors, error handling, best pratices, parameters usage, precise documentation...)

  • First, you need to have your LinkedIn API Key and Secret Key. If you don't, register an app on here.

  • Second, you need an Activity in the application that can receive the authorization code. For that, it needs to be set as browsable (launchable from a browser) in the AndroidManifest.xml file :

        <activity
          android:name=".ResultActivity"
          android:label="" >
          <intent-filter>
            <action android:name="android.intent.action.VIEW"/>
    
            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>
          </intent-filter>
    

    Although not recommended, it's possible to use a data tag to retrieve URIs using a custom scheme :

      <data android:scheme="oauth"/>
    
  • After that, you need to redirect the user to the LinkedIn's authorization dialog, using a specific URL :

    https://www.linkedin.com/uas/oauth2/authorization?response_type=code
                                       &client_id=YOUR_API_KEY
                                       &scope=SCOPE 
                                       &state=STATE
                                       &redirect_uri=YOUR_REDIRECT_URI
    

    You can use a WebView to directly show it in your application, or let the system handle it through an Intent like :

    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(/* FULL URL */));
    startActivity(intent);
    

    The only problem here is that the API does not accept schemes other than http or https, meaning you can't just pass the intent URI as the redirect_uri parameter.

    So I created a landing page on my server, with only purpose is to redirect to the application. We can imagine something like (in ugly shorten PHP) (Intent ref.) :

    header('Location: ' . "intent:#Intent;component=your.package/.ResultActivity;S.code=" . $_GET['code'] . ";S.state=" . $_GET['state'] . ";end");
    die();
    

    So everything's set! Now the onCreate(Bundle) of the ResultActivity :

    Intent intent = getIntent();
    String authorizationCode = intent.getStringExtra("code");
    

    There is another way to pass parameters here, if the data tag was used earlier.

  • Almost there! Now you just need to perform a simple POST request on that URL :

    https://www.linkedin.com/uas/oauth2/accessToken?grant_type=authorization_code
                                        &code=AUTHORIZATION_CODE
                                        &redirect_uri=YOUR_REDIRECT_URI
                                        &client_id=YOUR_API_KEY
                                        &client_secret=YOUR_SECRET_KEY
    

    Returning a JSON object on success :

    {"expires_in":5184000,"access_token":"AQXdSP_W41_UPs5ioT_t8HESyODB4FqbkJ8LrV_5mff4gPODzOYR"}

Et voilà ! You can now make your API calls using the access_token. Don't forget to store it somewhere so you don't have through these steps again.

I hope this wasn't too long to read and that it can help some people. :)


OAuth 2.0 is much simpler than 1.0 and can be done without any help from an external library. However, if you are already using scribe-java, it will be even easier.

The implementation is straight-forward. You need to create a WebView that has a custom WebViewClient which captures and overrides loading behavior for your callback URL. Thus, when the WebView attempts to load that URL, you can intercept the process and extract a verifier. The verifier can be passed to scribe-java to exchange for an access token.

To start the whole process, you just need to tell your WebView to load the authorization URL.

I have sample code hosted here. The app authenticates with Buffer's API but most of the code can be reused. You may be interested in the fragment which hosts my custom WebView and the background job that gets access token.

Feel free to ask me any follow up questions.