Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google+ login - Server side flow - Python - Google App Engine

I am building an app on Google App Engine using Flask. I am implementing Google+ login from the server-side flow described in https://developers.google.com/+/web/signin/server-side-flow. Before switching to App Engine, I had a very similar flow working. Perhaps I have introduced an error since then. Or maybe it is an issue with my implementation in App Engine.

I believe the url redirected to by the Google login flow should have a GET argument set "gplus_id", however, I am not receiving this parameter.

I have a login button created by:

(function() {
  var po = document.createElement('script');
  po.type = 'text/javascript'; po.async = true;
  po.src = 'https://plus.google.com/js/client:plusone.js?onload=render';
  var s = document.getElementsByTagName('script')[0];
  s.parentNode.insertBefore(po, s);
})();

function render() {
  gapi.signin.render('gplusBtn', {
    'callback': 'onSignInCallback',
    'clientid': '{{ CLIENT_ID }}',
    'cookiepolicy': 'single_host_origin',
    'requestvisibleactions': 'http://schemas.google.com/AddActivity',
    'scope': 'https://www.googleapis.com/auth/plus.login',
    'accesstype': 'offline',
    'width': 'iconOnly'
  });
}

In the javascript code for the page I have a function to initiate the flow:

var helper = (function() {
  var authResult = undefined;

  return {
    onSignInCallback: function(authResult) {
      if (authResult['access_token']) {
        // The user is signed in
        this.authResult = authResult;
        helper.connectServer();
      } else if (authResult['error']) {
        // There was an error, which means the user is not signed in.
        // As an example, you can troubleshoot by writing to the console:
        console.log('GPlus: There was an error: ' + authResult['error']);
      }
      console.log('authResult', authResult);
    },
    connectServer: function() {
      $.ajax({
        type: 'POST',
        url: window.location.protocol + '//' + window.location.host + '/connect?state={{ STATE }}',
        contentType: 'application/octet-stream; charset=utf-8',
        success: function(result) {
          // After we load the Google+ API, send login data.
          gapi.client.load('plus','v1',helper.otherLogin);
        },
        processData: false,
        data: this.authResult.code,
        error: function(e) {
          console.log("connectServer: error: ", e);
        }
      });
    }
  }
})();

/**
 * Calls the helper method that handles the authentication flow.
 *
 * @param {Object} authResult An Object which contains the access token and
 *   other authentication information.
 */
function onSignInCallback(authResult) {
  helper.onSignInCallback(authResult);
}

This initiates the flow at "/connect" (See step 8. referenced in the above doc):

@app.route('/connect', methods=['GET', 'POST'])
def connect():
    # Ensure that this is no request forgery going on, and that the user
    # sending us this connect request is the user that was supposed to.
    if request.args.get('state', '') != session.get('state', ''):
        response = make_response(json.dumps('Invalid state parameter.'), 401)
        response.headers['Content-Type'] = 'application/json'
        return response

    # Normally the state would be a one-time use token, however in our
    # simple case, we want a user to be able to connect and disconnect
    # without reloading the page.  Thus, for demonstration, we don't
    # implement this best practice.
    session.pop('state')

    gplus_id = request.args.get('gplus_id')
    code = request.data

    try:
        # Upgrade the authorization code into a credentials object
        oauth_flow = client.flow_from_clientsecrets('client_secrets.json', scope='')
        oauth_flow.redirect_uri = 'postmessage'
        credentials = oauth_flow.step2_exchange(code)
    except client.FlowExchangeError:
        app.logger.debug("connect: Failed to upgrade the authorization code")
        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.
    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
    ...

However, the flow stops at if result['user_id'] != gplus_id:, saying "Token's user ID doesn't match given user ID.". result['user_id'] is a valid users ID, but gplus_id is None.

The line gplus_id = request.args.get('gplus_id') is expecting the GET args to contain 'gplus_id', but they only contain 'state'. Is this a problem with my javascript connectServer function? Should I include 'gplus_id' there? Surely I don't know it at that point. Or something else?

like image 895
Jon G Avatar asked Apr 07 '14 13:04

Jon G


1 Answers

Similar to this question, I believe this is an issue with incomplete / not up to date / inconsistent documentation.

Where https://developers.google.com/+/web/signin/server-side-flow suggests that gplus_id will be returned in the GET arguments, this is not the case for the flow I was using.

I found my answer in https://github.com/googleplus/gplus-quickstart-python/blob/master/signin.py, which includes this snippet:

# An ID Token is a cryptographically-signed JSON object encoded in base 64.
# Normally, it is critical that you validate an ID Token before you use it,
# but since you are communicating directly with Google over an
# intermediary-free HTTPS channel and using your Client Secret to
# authenticate yourself to Google, you can be confident that the token you
# receive really comes from Google and is valid. If your server passes the
# ID Token to other components of your app, it is extremely important that
# the other components validate the token before using it.
gplus_id = credentials.id_token['sub']
like image 193
Jon G Avatar answered Nov 14 '22 22:11

Jon G