无法在跨客户端谷歌 oauth2.0 中交换访问令牌和刷新令牌的授权码

Posted

技术标签:

【中文标题】无法在跨客户端谷歌 oauth2.0 中交换访问令牌和刷新令牌的授权码【英文标题】:Unable to exchange authorization code for access token and refresh token in Cross Client google oauth2.0 【发布时间】:2015-02-25 21:29:08 【问题描述】:

我在我的 android 应用上实现 Google Play 服务登录并将授权代码传递到我的后端服务器时遇到问题,因此服务器将交换代码以获取访问令牌和刷新令牌。

首先让我写几行已经尝试/阅读的内容:

在 code.google.com/apis/console 我创建了一个新项目,有两个客户端(WEB 客户端和 Android 安装客户端) 阅读https://developers.google.com/+/mobile/android/sign-in#cross-platform_single_sign_on和http://android-developers.blogspot.com/2013/01/verifying-back-end-calls-from-android.html上的文章

这是我用于检索授权代码和 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;


上面的代码给了我两个代码: 1.RetrieveExchangeCodeAsyncTask 返回的一个-命名代码。 2.Second由RetrieveJwtAsyncTask类返回,名为IdToken。

现在首先我很困惑我需要将上面的哪个发送到我的网络服务器,在那里进行交换。 我尝试使用第一个(以“4/....”开头的那个)在我的服务器端进行交换,但得到了空指针异常。 另请指定我需要使用的重定向 URI。

这是我用于交换的服务器端代码:

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);
      



另外,我是否在我的服务器端代码(在 MyClass 中)中传递了正确的 client_secret.json 文件——它是 android 客户端的。

请帮忙!!! 提前谢谢。

【问题讨论】:

【参考方案1】:

从此使用:

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();

您必须将您的client_id、client_secret 和code 的值替换为上述相关变量。

更多信息在流动链接中: https://github.com/googleplus/gplus-quickstart-java/blob/master/src/com/google/plus/samples/quickstart/Signin.java

您还可以从此链接获取 Google API 库:

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

【讨论】:

【参考方案2】:

您确实需要用code 交换访问令牌。 id_token 仅适用于您的客户并且是独立的,因此不需要交换其他任何东西。您需要在交换请求中与 code 一起发送到令牌端点的 redirect_uri 与您最初在发送到授权端点的授权请求中发送的 redirect_uri 值完全相同(并且已注册在 Google API 控制台中为您的客户提供服务),因此查看您的代码,它是使用 clientSecrets.getDetails().getRedirectUris().get(0); 检索到的代码

【讨论】:

thanx @Hans.. 我明白了,但现在问题是它返回我访问令牌的 null 值,但它返回我正确的电子邮件地址......可能是什么原因访问和刷新令牌值是否为空? 在上面写的MyClass类的方法'public static String getAuthorizationUrl(String emailAddress, String state)'中,“state”参数的值应该是什么,它的用途是什么? state 参数上,从thread-safe.com/2014/05/… 解释:状态参数用于通过在请求中包含客户端可以在响应中验证但攻击者无法知道的。这方面的一个例子是会话 cookie 的哈希或存储在与会话链接的服务器中的随机值。 我仍然收到“null”的访问令牌。 :(

以上是关于无法在跨客户端谷歌 oauth2.0 中交换访问令牌和刷新令牌的授权码的主要内容,如果未能解决你的问题,请参考以下文章

Rails Google Client API - 无法用刷新令牌交换访问令牌

oauth2.0授予Web应用访问用户的Google spreadhseets所需的最小范围是多少?

使用 OAuth2 的 API 网关的客户端凭据 [关闭]

使用Spring Security登录认证,通过Oauth2.0开发第三方授授权访问资源项目详解

Oauth2.0应用场景-用户互信

OAUTH2.0 不记名令牌不工作