通过Google获取访问令牌OAuth时抛出FlowExchangeError [英] FlowExchangeError thrown when getting access token OAuth via Google

查看:54
本文介绍了通过Google获取访问令牌OAuth时抛出FlowExchangeError的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想向网站添加通过GMail登录"功能.我创建 login.html project.py 来处理响应.

I want to add 'sign-in via GMail' functionality to a website. I create login.html and project.py to process the response.

我在 login.html 中添加一个按钮:

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

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

I have a callBack function. In the browser console, I can see that the response contains access_token, id_token (what is the difference?), and my user profile details (name, email, etc) so the request itself must have succeeded, however, error function is called because the response returned by my gconnect handler is 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

当尝试获取 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,看来还可以,我需要续订吗?

I have checked client_secrets.json I got from Google API, and it seems ok, do I need to renew it?

{"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"]}}

为什么凭证= oauth_flow.step2_exchange(代码)失败?

这是我第一次实现这一目标,而且我正在旅途中学习Web和OAuth,所有概念很难一次掌握.我也在使用Udacity OAuth课程,但是他们的代码很旧,无法正常工作.我在这里可能会缺少什么?

This is my first time implenting this, and I am learning Web and OAuth on-the-go, all the concepts are hard to grasp at once. I am also using the Udacity OAuth course but their code is old and does not work. What might I be missing here?

推荐答案

您需要遵循用于服务器端应用程序的Google登录,其中彻底描述了授权代码流的工作方式以及前端,后端和用户之间的交互.

You need to follow Google Signin for server side apps which describes thoroughly how the authorization code flow works, and the interactions between frontend, backend and user.

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

On server side, you use oauth_flow.step2_exchange(code) which expects an authorization code whereas you are sending an access token. Sending an access token here is not part of the authorization code flow or one-time-code flow as explained in the link above :

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

Your server exchanges this one-time-use code to acquire its own access and refresh tokens from Google for the server to be able to make its own API calls, which can be done while the user is offline. This one-time code flow has security advantages over both a pure server-side flow and over sending access tokens to your server.

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

If you want to use this flow, you need to use auth2.grantOfflineAccess() in the frontend :

auth2.grantOfflineAccess().then(signInCallback);

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

so that when the user clicks on the button it will return an authorization code + access token :

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

The Google Sign-In button provides both an access token and an authorization code. The code is a one-time code that your server can exchange with Google's servers for an access token.

仅当您希望服务器代表用户访问Google服务时,才需要授权代码

You only need the authorization code if you want your server to access Google services on behalf of your user

来自本教程它给出了以下示例,该示例应该对您有用(对您进行了一些修改):

From this tutorial it gives the following example that should work for you (with some modification on your side) :

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

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

Note that the answer above suppose that you want to use authorization code flow / one-time code flow as it was what you've implemented server side.

也可以按照您的方式发送访问令牌(例如,保持客户端不变),然后删除获取授权码"部分:

It's also possible to just send the access token as you did (eg leave the client side as is) and remove the "Obtain authorization code" part :

# 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

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

but doing this wouldn't be the authorization code flow / one-time-code flow anymore

您问过 access_token id_token 之间有什么区别:

You've asked what was the difference between access_token and id_token :

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

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

The id_token will be useful server-side to identify the connected user. Checkout the Python example in step 7 :

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

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

Note that there are other flow where the website send the id_token to the server, the server checks it, and authenticates the user(server doesn't care about access token/refresh token in this flow). In the case of authorization code, flow only the temporary code is shared between frontend and backend.

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

One more thing is about refresh_token which are token that are used to generate other access_token. Access tokens have limited lifetime (1 hour). Using grantOfflineAccess generate a code that will give you an access_token + refresh_token the first time user authenticates. It belongs to you if you want to store this refresh_token for accessing Google services in the background, it depends on your needs

这篇关于通过Google获取访问令牌OAuth时抛出FlowExchangeError的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆