1

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

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

Sign up to request clarification or add additional context in comments.

5 Comments

What I try to implement is "hybrid" (not purely client-side or server-side auth), I don't know how it is called exactly:1. user authenticates via a "Google account" pop-up 2. Google sends a one-time code to the user i.e. the client 3. Client forwards the code to the server [up until this my code works, I think] 4. Server used one-time code to obtain the access_token - here my code fails: credentials = oauth_flow.step2_exchange(code)
What you have just described is authorization code flow. Did you update your code with the beginning of the answer (client side with grantOffline ) and send the code and not the access token to the server ?
Not yet, I am going through the links you have posted. Could you please clarify: authorization code flow is neither what people refer to as "server-side" nor "client-side" flow?
Authorization code flow is just about receiving a code that willbe used to exchange token. The entity that receives the code should have access to the client_secret in order to resuest access token (and refresh token). Even if you receive the code in the frontend it’s still called the authorization code flow. Maybe you meant by server side, the authentication/authorization part which use the google library in your usecase and authenticate the user directly in client side.
Or if you want, you receive the response from google in the client side. Maybe what you mean by server side is when you have an authentication middleware server side that will redirect the user to google authentication page (oauth page) if user is not logged in

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.