首页 > 解决方案 > 通过 Google 获取访问令牌 OAuth 时引发 FlowExchangeError

问题描述

我想向网站添加“通过 GMail 登录”功能。我创建login.htmlproject.py处理响应。

我添加一个按钮login.html

function renderButton() {
      gapi.signin2.render('my-signin2', {
        'scope': 'profile email',
        'width': 240,
        'height': 50,
        'longtitle': true,
        'theme': 'dark',
        'onsuccess': signInCallback,
        'onfailure': signInCallback
      });
    };

我有一个回调函数。在浏览器控制台中,我可以看到响应包含access_token, id_token(有什么区别?)和我的用户配置文件详细信息(姓名、电子邮件等),因此请求本身必须成功,但是,error函数被调用,因为response返回我的gconnect处理程序是401

    function signInCallback(authResult) {
      var access_token = authResult['wc']['access_token'];
      if (access_token) {
        // Hide the sign-in button now that the user is authorized
        $('#my-signin2').attr('style', 'display: none');

        // Send the one-time-use code to the server, if the server responds, write a 'login successful' message to the web page and then redirect back to the main restaurants page
        $.ajax({
          type: 'POST',
          url: '/gconnect?state={{STATE}}',
          processData: false,
          data: access_token,
          contentType: 'application/octet-stream; charset=utf-8',
          success: function(result) 
          {
               ....
          },
          error: function(result) 
          {
              if (result) 
              {
               // THIS CASE IS EXECUTED, although authResult['error'] is undefined
               console.log('Logged in successfully as: ' + authResult['error']);
              } else if (authResult['wc']['error']) 
              {
                 ....
              } else 
              {
                ....
             }//else
            }//error function
      });//ajax
  };//if access token
};//callback

处理对 Google 的 ajax 请求的代码在尝试获取时抛出 FlowExchangeError credentials = oauth_flow.step2_exchange(code)

@app.route('/gconnect', methods=['POST'])
def gconnect():
    if request.args.get('state') != login_session['state']:
        response = make_response(json.dumps('Invalid state parameter.'), 401)
        response.headers['Content-Type'] = 'application/json'
        return response
    # Obtain authorization code
    code = request.data
    try:
        # Upgrade the authorization code into a credentials object
        oauth_flow = flow_from_clientsecrets('client_secrets.json', scope='')
        oauth_flow.redirect_uri = 'postmessage'
        ##### THROWS EXCEPTION HERE #####
        credentials = oauth_flow.step2_exchange(code)
    except FlowExchangeError:
        response = make_response(
            json.dumps('Failed to upgrade the authorization code.'), 401)
        response.headers['Content-Type'] = 'application/json'
        return response

    # Check that the access token is valid.
    access_token = credentials.access_token
    url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s'
           % access_token)
    h = httplib2.Http()
    result = json.loads(h.request(url, 'GET')[1])
    # If there was an error in the access token info, abort.
    if result.get('error') is not None:
        response = make_response(json.dumps(result.get('error')), 500)
        response.headers['Content-Type'] = 'application/json'
        return response

    # Verify that the access token is used for the intended user.
    gplus_id = credentials.id_token['sub']
    if result['user_id'] != gplus_id:
        response = make_response(
            json.dumps("Token's user ID doesn't match given user ID."), 401)
        response.headers['Content-Type'] = 'application/json'
        return response

    # Verify that the access token is valid for this app.
    if result['issued_to'] != CLIENT_ID:
        response = make_response(
            json.dumps("Token's client ID does not match app's."), 401)
        print "Token's client ID does not match app's."
        response.headers['Content-Type'] = 'application/json'
        return response

    stored_access_token = login_session.get('access_token')
    stored_gplus_id = login_session.get('gplus_id')
    if stored_access_token is not None and gplus_id == stored_gplus_id:
        response = make_response(json.dumps('Current user is already connected.'),
                                 200)
        response.headers['Content-Type'] = 'application/json'
        return response

    # Store the access token in the session for later use.
    login_session['access_token'] = credentials.access_token
    login_session['gplus_id'] = gplus_id

    # Get user info
    userinfo_url = "https://www.googleapis.com/oauth2/v1/userinfo"
    params = {'access_token': credentials.access_token, 'alt': 'json'}
    answer = requests.get(userinfo_url, params=params)

    data = answer.json()

    login_session['username'] = data['name']
    login_session['picture'] = data['picture']
    login_session['email'] = data['email']

    output = ''
    output += '<h1>Welcome, '
    output += login_session['username']
    return output

我检查了从 Google API 获得的 client_secrets.json,看起来还可以,我需要更新它吗?

{"web":{"client_id":"blah blah blah.apps.googleusercontent.com","project_id":"blah","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"blah client secret","redirect_uris":["http://localhost:1234"],"javascript_origins":["http://localhost:1234"]}}

为什么credentials = oauth_flow.step2_exchange(code)失败?

这是我第一次实现这个,我正在学习Web和OAuth,所有的概念都很难一次掌握。我也在使用 Udacity OAuth 课程,但他们的代码很旧并且不起作用。我可能在这里缺少什么?

标签: pythonauthenticationflaskoauth-2.0google-oauth

解决方案


您需要关注服务器端应用程序的 Google Signin,它详细描述了授权代码流程的工作原理,以及前端、后端和用户之间的交互。

在服务器端,您oauth_flow.step2_exchange(code)在发送访问令牌时使用需要授权码。如上面的链接所述,在此处发送访问令牌不是授权代码流或一次性代码流的一部分:

您的服务器交换此一次性使用代码以从 Google 获取其自己的访问和刷新令牌,以便服务器能够进行自己的 API 调用,这可以在用户离线时完成。与纯服务器端流程和向您的服务器发送访问令牌相比,这种一次性代码流具有安全优势。

如果要使用此流程,则需要auth2.grantOfflineAccess()在前端使用:

auth2.grantOfflineAccess().then(signInCallback);

这样当用户单击按钮时,它将返回授权码 + 访问令牌:

Google 登录按钮提供访问令牌和授权代码。该代码是一次性代码,您的服务器可以与 Google 的服务器交换访问令牌。

如果您希望您的服务器代表您的用户访问 Google 服务,您只需要授权码

本教程中,它提供了以下示例,该示例应该适合您(您可以进行一些修改):

<html itemscope itemtype="http://schema.org/Article">
<head>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
  <script src="https://apis.google.com/js/client:platform.js?onload=start" async defer></script>
  <script>
    function start() {
      gapi.load('auth2', function() {
        auth2 = gapi.auth2.init({
          client_id: 'YOUR_CLIENT_ID.apps.googleusercontent.com',
          // Scopes to request in addition to 'profile' and 'email'
          //scope: 'additional_scope'
        });
      });
    }
  </script>
</head>
<body>
    <button id="signinButton">Sign in with Google</button>
    <script>
      $('#signinButton').click(function() {
        auth2.grantOfflineAccess().then(signInCallback);
      });
    </script>
    <script>
    function signInCallback(authResult) {
      if (authResult['code']) {

        // Hide the sign-in button now that the user is authorized, for example:
        $('#signinButton').attr('style', 'display: none');

        // Send the code to the server
        $.ajax({
          type: 'POST',
          url: 'http://example.com/storeauthcode',
          // Always include an `X-Requested-With` header in every AJAX request,
          // to protect against CSRF attacks.
          headers: {
            'X-Requested-With': 'XMLHttpRequest'
          },
          contentType: 'application/octet-stream; charset=utf-8',
          success: function(result) {
            console.log(result);
            // Handle or verify the server response.
          },
          processData: false,
          data: authResult['code']
        });
      } else {
        // There was an error.
      }
    }
    </script>
</body>
</html>

请注意,上面的答案假设您要使用授权代码流/一次性代码流,因为它是您在服务器端实现的。

也可以像您一样发送访问令牌(例如,保持客户端不变)并删除“获取授权码”部分:

# Obtain authorization code
code = request.data
try:
    # Upgrade the authorization code into a credentials object
    oauth_flow = flow_from_clientsecrets('client_secrets.json', scope='')
    oauth_flow.redirect_uri = 'postmessage'
    ##### THROWS EXCEPTION HERE #####
    credentials = oauth_flow.step2_exchange(code)
except FlowExchangeError:
    response = make_response(
        json.dumps('Failed to upgrade the authorization code.'), 401)
    response.headers['Content-Type'] = 'application/json'
    return response

反而 :

access_token = request.data

但这样做将不再是授权代码流/一次性代码流


access_token您已经问过和之间有什么区别id_token

  • 访问令牌是使您可以访问资源的令牌,在这种情况下是 Google 服务
  • id_token 是一个 JWT 令牌,用于将您标识为 Google 用户 - 例如经过身份验证的用户,它是一个通常在服务器端检查的令牌(检查 JWT 的签名和字段)以验证用户身份

id_token 将在服务器端用于识别连接的用户。签出第 7 步中的 Python 示例:

# Get profile info from ID token
userid = credentials.id_token['sub']
email = credentials.id_token['email']

请注意,还有其他流程,网站将其发送id_token到服务器,服务器对其进行检查,并对用户进行身份验证(服务器不关心此流程中的访问令牌/刷新令牌)。在授权码的情况下,只有临时代码在前端和后端之间共享。


还有一件事是关于refresh_token哪些是用于生成其他access_token. 访问令牌的生命周期有限(1 小时)。使用grantOfflineAccess生成的代码将在用户第一次进行身份验证时为您提供 access_token + refresh_token。如果您想存储它refresh_token以在后台访问 Google 服务,它属于您,这取决于您的需要


推荐阅读