【问题标题】:FlowExchangeError thrown when getting access token OAuth via Google通过 Google 获取访问令牌 OAuth 时引发 FlowExchangeError
【发布时间】:2020-10-02 08:13:32
【问题描述】:

我想向网站添加“通过 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_tokenid_token(有什么区别?)和我的用户配置文件详细信息(姓名、电子邮件等),所以请求本身一定是成功的,但是,@调用 987654327@ 函数是因为我的 gconnect 处理程序返回的 response401

    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 请求的代码在尝试获取 credentials = oauth_flow.step2_exchange(code) 时抛出 FlowExchangeError :

@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 课程,但他们的代码很旧并且不起作用。我可能在这里遗漏了什么?

【问题讨论】:

    标签: python authentication flask oauth-2.0 google-oauth


    【解决方案1】:

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

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

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

    如果你想使用这个流程,你需要在前端使用auth2.grantOfflineAccess()

    auth2.grantOfflineAccess().then(signInCallback);
    

    这样当用户点击按钮时,它会返回一个授权码 + 访问令牌:

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

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

    来自this tutorial,它提供了以下示例,应该适合您(您需要进行一些修改):

    <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_tokenid_token 有什么区别:

    • 访问令牌是允许您访问资源(在本例中为 Google 服务)的令牌
    • id_token 是一个 JWT 令牌,用于将您标识为 Google 用户 - 例如,经过身份验证的用户,它是一个通常在服务器端检查的令牌(检查 JWT 的签名和字段),以便对用户进行身份验证

    id_token 将在服务器端用于识别连接的用户。查看step 7 中的 Python 示例:

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

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


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

    【讨论】:

    • 我尝试实现的是“混合”(不是纯粹的客户端或服务器端身份验证),我不知道它是如何准确调用的:1。用户通过“Google 帐户”弹出窗口进行身份验证 2. Google 向用户(即客户端)发送一次性代码 3. 客户端将代码转发到服务器[直到我的代码工作,我认为] 4. 使用的服务器获取 access_token 的一次性代码 - 这里我的代码失败:credentials = oauth_flow.step2_exchange(code)
    • 你刚才描述的是授权码流程。您是否使用答案的开头更新了代码(客户端使用 grantOffline )并将代码而不是访问令牌发送到服务器?
    • 还没有,我正在浏览您发布的链接。您能否澄清一下:授权代码流既不是人们所说的“服务器端”也不是“客户端”流?
    • 授权码流程只是接收一个用于交换令牌的代码。接收代码的实体应该有权访问 client_secret 以重新获取访问令牌(和刷新令牌)。即使您在前端收到代码,它仍然称为授权代码流。也许您的意思是服务器端,即在您的用例中使用 google 库并直接在客户端对用户进行身份验证的身份验证/授权部分。
    • 或者,如果您愿意,您可以在客户端收到来自 google 的响应。也许您所说的服务器端的意思是当您有一个身份验证中间件服务器端时,如果用户未登录,它将将用户重定向到谷歌身份验证页面(oauth 页面)
    猜你喜欢
    • 1970-01-01
    • 2023-03-27
    • 2012-08-03
    • 2012-02-12
    • 1970-01-01
    • 1970-01-01
    • 2013-05-26
    • 2015-09-04
    • 2017-06-23
    相关资源
    最近更新 更多