FlowExchangeError thrown when getting access token OAuth via Google

匆匆过客 提交于 2021-02-10 14:58:44

问题


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

I add a button to login.html:

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

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

The code that handles the ajax request to Google throws FlowExchangeError when trying to get 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

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

Why does credentials = oauth_flow.step2_exchange(code) fail?

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?


回答1:


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.

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 :

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.

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 :

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.

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

instead :

access_token = request.data

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


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

  • an access token is a token that gives you access to a resource, in this case Google services
  • an id_token is a JWT token that is used to identify you as a Google user - eg an authenticated user, it's a token usually checked server side (the signature and the fields of the JWT are checked) in order to authenticate a user

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

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.


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



来源:https://stackoverflow.com/questions/62345491/flowexchangeerror-thrown-when-getting-access-token-oauth-via-google

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!