4

I've been having problems implementing Google Play Services login on my android app and passing the authorisation code to my backend server, so the server will exchange the code for access token and refresh token.

First let me write a few lines what has already been tried/read:

on code.google.com/apis/console I've created a new project, with two clients (WEB client and Android installed client) read articles onhttps://developers.google.com/+/mobile/android/sign-in#cross-platform_single_sign_on and http://android-developers.blogspot.com/2013/01/verifying-back-end-calls-from-android.html

Here is my code for client side that retrieves the authorization code and IdToken:

    package com.google.drive.samples.crossclientoauth2;

    import java.util.Arrays;
    import java.util.List;

    import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.EditText;

import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;

public class MainActivity extends Activity {

  final private String CLIENT_ID = MY WEB SERVER'S CLIENT ID;
  final private List<String> SCOPES = Arrays.asList(new String[]{
      "https://www.googleapis.com/auth/plus.login",
      "https://www.googleapis.com/auth/drive",
      "https://www.googleapis.com/auth/youtube",
      "https://www.googleapis.com/auth/youtube.readonly"
  });

  // I have modified the above line of code.  

  private GoogleAccountCredential mCredential;

  private EditText mExchangeCodeEditText;
  private EditText mIdTokenEditText;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);
    mExchangeCodeEditText = (EditText) findViewById(R.id.editTextExchangeCode);
    mIdTokenEditText = (EditText) findViewById(R.id.editTextIdToken);

    // initiate a credential object with drive and plus.login scopes
    // cross identity is only available for tokens retrieved with plus.login
    mCredential = GoogleAccountCredential.usingOAuth2(this, null);

    // user needs to select an account, start account picker
    startActivityForResult(
        mCredential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);

  }

  /**
   * Handles the callbacks from result returning
   * account picker and permission requester activities.
   */
  @Override
  protected void onActivityResult(
      final int requestCode, final int resultCode, final Intent data) {
    switch (requestCode) {
    // user has  returned back from the account picker,
    // initiate the rest of the flow with the account he/she has chosen.
    case REQUEST_ACCOUNT_PICKER:
      String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
      if (accountName != null) {
        mCredential.setSelectedAccountName(accountName);
        new RetrieveExchangeCodeAsyncTask().execute();
        new RetrieveJwtAsyncTask().execute();
      }
      break;
    // user has returned back from the permissions screen,
    // if he/she has given enough permissions, retry the the request.
    case REQUEST_AUTHORIZATION:
      if (resultCode == Activity.RESULT_OK) {
        // replay the same operations
        new RetrieveExchangeCodeAsyncTask().execute();
        new RetrieveJwtAsyncTask().execute();
      }
      break;
    }
  }

  /**
   * Retrieves the exchange code to be sent to the
   * server-side component of the app.
   */

  public class RetrieveExchangeCodeAsyncTask
      extends AsyncTask<Void, Boolean, String> {


    @Override
    protected String doInBackground(Void... params) {
      String scope = String.format("oauth2:server:client_id:%s:api_scope:%s",
          CLIENT_ID, TextUtils.join(" ", SCOPES));
      try {
        return GoogleAuthUtil.getToken(
            MainActivity.this, mCredential.getSelectedAccountName(), scope);
      } catch (UserRecoverableAuthException e) {
        startActivityForResult(e.getIntent(), REQUEST_AUTHORIZATION);
      } catch (Exception e) {
        e.printStackTrace(); // TODO: handle the exception
      }
      return null;
    }

    @Override
    protected void onPostExecute(String code) {
      // exchange code with server-side to retrieve an additional
      // access token on the server-side.
        Log.v("first One ","code 1 is: "+ code);
      mExchangeCodeEditText.setText(code);
    }
  }

  /**
   * Retrieves a JWT to identify the user without the
   * regular client-side authorization flow. The jwt payload needs to be
   * sent to the server-side component.
   */
  public class RetrieveJwtAsyncTask
      extends AsyncTask<Void, Boolean, String> {

    @Override
    protected String doInBackground(Void... params) {
      String scope = "audience:server:client_id:" + CLIENT_ID;
      try {
        return GoogleAuthUtil.getToken(
            MainActivity.this, mCredential.getSelectedAccountName(), scope);
      } catch(UserRecoverableAuthIOException e) {
        startActivityForResult(e.getIntent(), REQUEST_AUTHORIZATION);
      } catch (Exception e) {
        e.printStackTrace(); // TODO: handle the exception
      }
      return null;
    }

    @Override
    protected void onPostExecute(String idToken) {
      // exchange encrypted idToken with server-side to identify the user
        Log.v("Second One","2222"+ idToken);
      mIdTokenEditText.setText(idToken);
    }
  }

  private static final int REQUEST_ACCOUNT_PICKER = 100;
  private static final int REQUEST_AUTHORIZATION = 200;

}

The above code gives me two codes: 1.One returned by RetrieveExchangeCodeAsyncTask - named code. 2.Second returned by RetrieveJwtAsyncTask class- named IdToken.

Now in the first place i am confused as to which one from the above do i need to send to my web server where it will be exchanged. I tried using the first one(the one that starts as "4/....") for exchange at my server side but got A null pointer exception. Also please specify what redirect URI i need to use.

Here is my server-side code for exchange:

package com.myAuthSample.tial;

import java.io.IOException;

import com.myAuthSample.tial.MyClass.CodeExchangeException;
import com.myAuthSample.tial.MyClass.NoRefreshTokenException;

public class MyMainDemo {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
          try {
            MyClass.getCredentials("4/...something...", "state");  //passed the retrieved authorization code
        } catch (CodeExchangeException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoRefreshTokenException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}


public class MyClass {


    // Path to client_secrets.json which should contain a JSON document such as:
      //   {
      //     "web": {
      //       "client_id": "[[YOUR_CLIENT_ID]]",
      //       "client_secret": "[[YOUR_CLIENT_SECRET]]",
      //       "auth_uri": "https://accounts.google.com/o/oauth2/auth",
      //       "token_uri": "https://accounts.google.com/o/oauth2/token"
      //     }
      //   }
      private static final String CLIENTSECRETS_LOCATION = "/client_secrets_2.json";// client secrets of my android client

      // private static final String REDIRECT_URI = "<YOUR_REGISTERED_REDIRECT_URI>";

      private static String REDIRECT_URI ="";



      private static final List<String> SCOPES = Arrays.asList(
              "https://www.googleapis.com/auth/plus.login",
              "https://www.googleapis.com/auth/youtube");

      private static GoogleAuthorizationCodeFlow flow = null;

      /**
       * Exception thrown when an error occurred while retrieving credentials.
       */
      public static class GetCredentialsException extends Exception {

        protected String authorizationUrl;

        /**
         * Construct a GetCredentialsException.
         *
         * @param authorizationUrl The authorization URL to redirect the user to.
         */
        public GetCredentialsException(String authorizationUrl) {
          this.authorizationUrl = authorizationUrl;
        }

        /**
         * Set the authorization URL.
         */
        public void setAuthorizationUrl(String authorizationUrl) {
          this.authorizationUrl = authorizationUrl;
        }

        /**
         * @return the authorizationUrl
         */
        public String getAuthorizationUrl() {
          return authorizationUrl;
        }
      }

      /**
       * Exception thrown when a code exchange has failed.
       */
      public static class CodeExchangeException extends GetCredentialsException {

        /**
         * Construct a CodeExchangeException.
         *
         * @param authorizationUrl The authorization URL to redirect the user to.
         */
        public CodeExchangeException(String authorizationUrl) {
          super(authorizationUrl);
        }

      }

      /**
       * Exception thrown when no refresh token has been found.
       */
      public static class NoRefreshTokenException extends GetCredentialsException {

        /**
         * Construct a NoRefreshTokenException.
         *
         * @param authorizationUrl The authorization URL to redirect the user to.
         */
        public NoRefreshTokenException(String authorizationUrl) {
          super(authorizationUrl);
        }

      }

      /**
       * Exception thrown when no user ID could be retrieved.
       */
      private static class NoUserIdException extends Exception {
      }

      /**
       * Retrieved stored credentials for the provided user ID.
       *
       * @param userId User's ID.
       * @return Stored Credential if found, {@code null} otherwise.
       */
      static Credential getStoredCredentials(String userId) {
        // TODO: Implement this method to work with your database. Instantiate a new
        // Credential instance with stored accessToken and refreshToken.
        throw new UnsupportedOperationException();
      }

      /**
       * Store OAuth 2.0 credentials in the application's database.
       *
       * @param userId User's ID.
       * @param credentials The OAuth 2.0 credentials to store.
       */
      static void storeCredentials(String userId, Credential credentials) {
        // TODO: Implement this method to work with your database.
        // Store the credentials.getAccessToken() and credentials.getRefreshToken()
        // string values in your database.
          System.out.println("credentials are :    " + credentials.toString());
        throw new UnsupportedOperationException();
      }

      /**
       * Build an authorization flow and store it as a static class attribute.
       *
       * @return GoogleAuthorizationCodeFlow instance.
       * @throws IOException Unable to load client_secrets.json.
       */
      static GoogleAuthorizationCodeFlow getFlow() throws IOException {
        if (flow == null) {
          HttpTransport httpTransport = new NetHttpTransport();
          JacksonFactory jsonFactory = new JacksonFactory();    //...this was the original line....
        //  JsonFactory jsonFactory = new JacksonFactory();

         //my code....        
          Reader clientSecretReader = new InputStreamReader(MyClass.class.getResourceAsStream(CLIENTSECRETS_LOCATION));

          GoogleClientSecrets clientSecrets =
                  GoogleClientSecrets.load(jsonFactory,clientSecretReader);

          REDIRECT_URI =clientSecrets.getDetails().getRedirectUris().get(0);
          // my code ends...

    /*      GoogleClientSecrets clientSecrets =
              GoogleClientSecrets.load(jsonFactory,
                  MyClass.class.getResourceAsStream(CLIENTSECRETS_LOCATION));
      */
          flow =
              new GoogleAuthorizationCodeFlow.Builder(httpTransport, jsonFactory, clientSecrets, SCOPES)
                  .setAccessType("offline").setApprovalPrompt("force").build();
        }
        return flow;
      }

      /**
       * Exchange an authorization code for OAuth 2.0 credentials.
       *
       * @param authorizationCode Authorization code to exchange for OAuth 2.0
       *        credentials.
       * @return OAuth 2.0 credentials.
       * @throws CodeExchangeException An error occurred.
       */
      static Credential exchangeCode(String authorizationCode)
          throws CodeExchangeException {
        try {
          GoogleAuthorizationCodeFlow flow = getFlow();
          GoogleTokenResponse response =
              flow.newTokenRequest(authorizationCode).setRedirectUri(REDIRECT_URI).execute();
          return flow.createAndStoreCredential(response, null);
        } catch (IOException e) {
          System.err.println("An error occurred: " + e);
          throw new CodeExchangeException(null);
        }
      }

      /**
       * Send a request to the UserInfo API to retrieve the user's information.
       *
       * @param credentials OAuth 2.0 credentials to authorize the request.
       * @return User's information.
       * @throws NoUserIdException An error occurred.
       */
      static Userinfo getUserInfo(Credential credentials)
          throws NoUserIdException {
        Oauth2 userInfoService =
            new Oauth2.Builder(new NetHttpTransport(), new JacksonFactory(), credentials).build();
        Userinfo userInfo = null;
        try {
          userInfo = userInfoService.userinfo().get().execute();
        } catch (IOException e) {
          System.err.println("An error occurred: " + e);
        }
        if (userInfo != null && userInfo.getId() != null) {
          return userInfo;
        } else {
          throw new NoUserIdException();
        }
      }

      /**
       * Retrieve the authorization URL.
       *
       * @param emailAddress User's e-mail address.
       * @param state State for the authorization URL.
       * @return Authorization URL to redirect the user to.
       * @throws IOException Unable to load client_secrets.json.
       */
      public static String getAuthorizationUrl(String emailAddress, String state) throws IOException {
        GoogleAuthorizationCodeRequestUrl urlBuilder =
            getFlow().newAuthorizationUrl().setRedirectUri(REDIRECT_URI).setState(state);
        urlBuilder.set("user_id", emailAddress);
        return urlBuilder.build();
      }

      /**
       * Retrieve credentials using the provided authorization code.
       *
       * This function exchanges the authorization code for an access token and
       * queries the UserInfo API to retrieve the user's e-mail address. If a
       * refresh token has been retrieved along with an access token, it is stored
       * in the application database using the user's e-mail address as key. If no
       * refresh token has been retrieved, the function checks in the application
       * database for one and returns it if found or throws a NoRefreshTokenException
       * with the authorization URL to redirect the user to.
       *
       * @param authorizationCode Authorization code to use to retrieve an access
       *        token.
       * @param state State to set to the authorization URL in case of error.
       * @return OAuth 2.0 credentials instance containing an access and refresh
       *         token.
       * @throws NoRefreshTokenException No refresh token could be retrieved from
       *         the available sources.
       * @throws IOException Unable to load client_secrets.json.
       */
      public static Credential getCredentials(String authorizationCode, String state)
          throws CodeExchangeException, NoRefreshTokenException, IOException {
        String emailAddress = "";
        try {
          Credential credentials = exchangeCode(authorizationCode);
          Userinfo userInfo = getUserInfo(credentials);
          String userId = userInfo.getId();
          emailAddress = userInfo.getEmail();
          if (credentials.getRefreshToken() != null) {
            storeCredentials(userId, credentials);
            return credentials;
          } else {
            credentials = getStoredCredentials(userId);
            if (credentials != null && credentials.getRefreshToken() != null) {
              return credentials;
            }
          }
        } catch (CodeExchangeException e) {
          e.printStackTrace();
          // Drive apps should try to retrieve the user and credentials for the current
          // session.
          // If none is available, redirect the user to the authorization URL.
          e.setAuthorizationUrl(getAuthorizationUrl(emailAddress, state));
          throw e;
        } catch (NoUserIdException e) {
          e.printStackTrace();
        }
        // No refresh token has been retrieved.
        String authorizationUrl = getAuthorizationUrl(emailAddress, state);
        throw new NoRefreshTokenException(authorizationUrl);
      }


}

Also,am i passing the correct client_secret.json file in my server side code(in MyClass)--which is of android client.

Please help!!! Thanx in advance.

Hans Z.
  • 50,496
  • 12
  • 102
  • 115
Arsalan Gundroo
  • 145
  • 1
  • 11

2 Answers2

1

Use from this:

import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson.JacksonFactory;


private final val TRANSPORT: HttpTransport = new NetHttpTransport()
    private final val JSON_FACTORY: JacksonFactory = new JacksonFactory()


GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(TRANSPORT, JSON_FACTORY,
                CLIENT_ID, CLIENT_SECRET, code, "postmessage").execute();

GoogleIdToken idToken = tokenResponse.parseIdToken();
String gplusId = idToken.getPayload().getSubject();

you must replace your values of the client_id,client_secret and code with above related variables.

more information is in the flowing link: https://github.com/googleplus/gplus-quickstart-java/blob/master/src/com/google/plus/samples/quickstart/Signin.java

Also you can get the Google API libraries from this link:

http://repo1.maven.org/maven2/com/google/

Parisa Taherian
  • 501
  • 5
  • 10
0

You need to exchange the code for an access token indeed. The id_token is meant for your client only and self-contained so it does not need to be exchanged for anything else. The redirect_uri that you need to send along with the code in the exchange request to the token endpoint is the exact same redirect_uri value that you sent originally in the authorization request that went to the authorization endpoint (and that is registered for your client in the Google API Console), so looking at your code it is the one retrieved with clientSecrets.getDetails().getRedirectUris().get(0);

Hans Z.
  • 50,496
  • 12
  • 102
  • 115
  • thanx @Hans.. i got that right but now the problem is that it returns me the value null for the acces token,but it returns me the correct email address...What can be the reason for the access and refresh token values being null? – Arsalan Gundroo Dec 29 '14 at 20:52
  • In the method 'public static String getAuthorizationUrl(String emailAddress, String state)' in MyClass class written above, what should be the value of "state" parameter and for what is it used? – Arsalan Gundroo Dec 29 '14 at 21:03
  • on the `state` parameter, paraphrased from http://www.thread-safe.com/2014/05/the-correct-use-of-state-parameter-in.html: the state parameter is used to stop Cross Site Request Forgery (XRSF) by including something in the request that the client can verify in the response but that an attacker could not know. An example of this would be a hash of the session cookie or a random value stored in the server linked to the session. – Hans Z. Dec 29 '14 at 21:22
  • I still receive the access token as "null". :( – Arsalan Gundroo Dec 30 '14 at 07:31