Google+ API error - Invalid Credentials

10,469

Solution 1

There's a definite problem with at least one header:

local.http.addParam(type="header", name="Authorization", value="Authorization: Bearer " & session.access_token);

You will have authorization in there twice, should be.

local.http.addParam(type="header", name="Authorization", value="Bearer " & session.access_token);

What's probably happening with the people.get call is it is picking up the API key and returning publicly rather than as the signed in user.

As Brett says though, to write an app activity you need to a &request_visible_actions=http://schemas.google.com/AddActivity to the auth URL you are sending users to in the first place (if not using the JS sign in button), in order to have the user approve you to write that kind of action (it's like another type of scope).

Solution 2

How exactly are you getting the Access Token? Are you using OAuth?

Are you using new Access Token each time? Not the one stored once in the DB? You need to keep in mind that tokens can expire quite quickly.

This bit: "reason": "authError", "message": "Invalid Credentials", "locationType": "header", "location": "Authorization"

may mean that your Authorization header is incorrect - not existing I suspect, as I can't see sending any signature in the bit of code you provided.

I haven't used the G+ API, but it seems you need to use proper OAuth 2.0 to get any data: https://developers.google.com/+/api/latest/people/get#examples

There's a CF library for OAuth here http://oauth.riaforge.org/

UPDATE:

If you go here: https://developers.google.com/+/api/latest/moments/insert#try-it and try running the example (using "me") as userid you'll see that the request looks like this:

POST https://www.googleapis.com/plus/v1/people/me/moments/vault?debug=true&key={YOUR_API_KEY}

Content-Type:  application/json
Authorization:  Bearer ya29.XXXXXXXXXXXXXXXXXXXXXXXXXXXX

You need to provide the "Authorization" header.

Solution 3

Although I'm not completely familiar with ColdFusion, it looks like you may be mixing two concepts in here that can't be mixed. The access_token is usually specified either in the HTTP header, or as a URL query parameter. It is not typically set as a POST parameter, and that may be part of the problem.

If done in a header, the HTTP header should be

Authorization: Bearer 1/fFBGRNJru1FQd44AzqT3Zg

and if done with the URL query parameter, your URL might need to look something like

https://www.googleapis.com/plus/v1/people/123456789012345/moments/vault?access_token=1/fFBGRNJru1FQd44AzqT3Zg

See https://developers.google.com/accounts/docs/OAuth2UserAgent#callinganapi for details

Not sure if this is related or not, although it might be, but I'm not sure if the body of your POST is correct either. You're not setting a content-type for the body, and I don't know if CF will use something like "application/www-form-url-encoded" or "multipart/form-data", neither of which is valid. The body should be "application/json".

Update

So given your changed code, you probably want the setUrl line to be something like

local.http.setUrl("https://www.googleapis.com/plus/v1/people/" & session.user.sourceid & "/moments/vault?access_token=" & session.access_token );

The URL I provided above gives the definitive documentation for OAuth2 at Google. I'm not sure what other parameters you were trying to add to the URL, or why, but if they are essential you can add them after the access token in this way.

Solution 4

Edited in response to update 2

This sounds like a problem with setting up the OAuth flow. You appear to be missing the Based on your OAuth generated URL, you are missing the request_visible_actions parameter either being missing or malformed.

Add the following query parameter to your OAuth URL and this should start working:

&request_visible_actions=http%3A%2F%2Fschemas.google.com%2FAddActivity

Another problem with your request is that you should remove the userinfo.profile scope from your request. That scope is not necessary because you have plus.login included and can result in some weird issues.

Lastly, ensure that your page at the target URL has the minimal level schema.org metadata that is required for the AddActivity moment type. This isn't your problem now, but could be the next problem you run into.

Share:
10,469
Michael Giovanni Pumo
Author by

Michael Giovanni Pumo

Front-end web developer skilled in HTML5, SCSS, JavaScript, Vue.js, jQuery, NPM, Gulp, Webpack and WordPress. Find out more about me on my portfolio: https://michaelpumo.com

Updated on July 13, 2022

Comments

  • Michael Giovanni Pumo
    Michael Giovanni Pumo almost 2 years

    I am signing users in using their Google+ account. I sign them in and grab basic information and store it in the database. In this process, I store the access_token in the session and move on.

    However, today I am trying to write a script that allows me to post to their 'moments' on Google+ using their in session access_token.

    I am getting an error and the response looks like:

    { "error": { "errors": [ { "domain": "global", "reason": "authError", "message": "Invalid Credentials", "locationType": "header", "location": "Authorization" } ], "code": 401, "message": "Invalid Credentials" } } 
    

    I am not sure why this is happening, below is the code I am using to make the request (it is in ColdFusion script, but you should be able to see the principles behind it even if you do not know this syntax).

    local.http = new http();
    local.http.setMethod("post"); 
    local.http.setCharset("utf-8"); 
    local.http.setUseragent(cgi.http_user_agent);
    local.http.setResolveurl(true);
    local.http.setTimeout(20);
    local.http.setUrl("https://www.googleapis.com/plus/v1/people/" & session.user.sourceid & "/moments/vault");
    
    local.target = {};
    local.target["kind"]        = "plus##moment";
    local.target["type"]        = "http://schemas.google.com/AddActivity";
    local.target["description"] = params.pin["description"];
    local.target["image"]       = session.user.image;
    local.target["name"]        = params.pin["title"];
    local.target["url"]         = URLfor(route="pinShow", key=obfuscateParam(pin.id), onlyPath=false);
    local.target["latitude"]    = session.user.latitude;
    local.target["longitude"]   = session.user.longitude;
    
    local.http.addParam(type="formField", name="target", value=serialize(local.target));
    local.http.addParam(type="formField", name="kind", value="plus##moment");
    local.http.addParam(type="formField", name="type", value="http://schemas.google.com/AddActivity");
    local.http.addParam(type="formField", name="access_token", value=session.access_token);
    local.result = local.http.send().getPrefix();
    

    As you can see, it all seems straight-forward.

    I have test this straight-after signing in and despite that, it says the token is invalid in the header response:

    HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="https://www.google.com/accounts/AuthSubRequest", error=invalid_token Content-Type: application/json; charset=UTF-8 Content-Encoding: gzip Date: Wed, 11 Sep 2013 13:12:28 GMT Expires: Wed, 11 Sep 2013 13:12:28 GMT Cache-Control: private, max-age=0 X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block Content-Length: 162 Server: GSE Alternate-Protocol: 443:quic 
    

    Does anyone have any idea why this is happening and how to solve it?

    I am not using any kind of library, as there isn't one for ColdFusion. In addition, I didn't want to, as my needs are only very basic and I wanted to see how it all worked.

    Am I missing something obvious here?

    Any help would be greatly appreciated as it's driving me nuts!

    Thanks, Mikey.

    PS - I have removed the app from my account, cleared all cookies and sessions and then signed in again granting all permissions, so it seems that side of it has been eliminated.

    UPDATE 1:

    After some light shone from other users here, it turns out I should be posting a JSON response in the HTTP body, to make the request. So I changed my code to this:

    local.request = {}
    local.request["kind"]       = "plus##moment";
    local.request["type"]       = "http://schemas.google.com/AddActivity";
    local.request["target"]     = {};
    local.request.target["kind"]        = "plus##itemScope";
    local.request.target["type"]        = "http://schemas.google.com/AddActivity";
    local.request.target["description"] = params.pin["description"];
    local.request.target["image"]       = session.user.image;
    local.request.target["name"]        = params.pin["title"];
    local.request.target["url"]         = URLfor(route="pinShow", key=obfuscateParam(pin.id), onlyPath=false);
    local.request.target["latitude"]    = session.user.latitude;
    local.request.target["longitude"]   = session.user.longitude;
    
    local.http = new http();
    local.http.setMethod("post"); 
    local.http.setCharset("utf-8"); 
    local.http.setUseragent(cgi.http_user_agent);
    local.http.setResolveurl(true);
    local.http.setTimeout(20);
    local.http.setUrl("https://www.googleapis.com/plus/v1/people/" & session.user.sourceid & "/moments/vault?debug=true&fields=kind%2Ctype%2Cdescription%2Cimage%2Curl&key={GOOLE_API_KEY}" );
    local.http.addParam(type="header", name="Content-Type", value="application/json");
    local.http.addParam(type="header", name="Authorization", value="Authorization: Bearer " & session.access_token);
    local.http.addParam(type="body", value=serializeJSON(local.request));
    
    local.result = local.http.send().getPrefix();
    

    However, now I get another error (401 unauthorized):

    { "error": { "errors": [ { "domain": "global", "reason": "required", "message": "Login Required", "locationType": "header", "location": "Authorization" } ], "code": 401, "message": "Login Required" } } 
    

    Does anybody know how I can pass the access_token using my new method above?

    UPDATE 2

    It has been highlighted to me that this could originate from my original OAuth 2 process. I have removed the app from my Google+ account and started the confirmation / signin process again. Here is the generated URL:

    https://accounts.google.com/ServiceLogin?service=lso&passive=1209600&continue=https://accounts.google.com/o/oauth2/auth?response_type%3Dcode%26scope%3Dhttps://www.googleapis.com/auth/userinfo.email%2Bhttps://www.googleapis.com/auth/userinfo.profile%2Bhttps://www.googleapis.com/auth/plus.login%26redirect_uri%3Dhttp://{MY_DOMAIN}.com/oauth/google/?do%253Dredirect%26state%3D2BFFBC14-29F9-4488-ABBF661C0E9E53DB%26client_id%3D{MY_CLIENT_ID}%26hl%3Den-GB%26from_login%3D1%26as%3D-593afcc82466f5f&ltmpl=popup&shdf=CnALEhF0aGlyZFBhcnR5TG9nb1VybBoADAsSFXRoaXJkUGFydHlEaXNwbGF5TmFtZRoIVW5pYmFuZHMMCxIGZG9tYWluGghVbmliYW5kcwwLEhV0aGlyZFBhcnR5RGlzcGxheVR5cGUaB0RFRkFVTFQMEgNsc28iFOyetn24YRlbdWKLAKGXFCH5C1p9KAEyFPquOHBH18K6iV1GTAg_P9zB2x60&sarp=1&scc=1

    Am I missing something here? Is it a scope that is missing that should allow me to post to their AddActivity stream?

    UPDATE 3

    My OAuth login URL (I've tried to split on to new lines to improve readability):

    https://accounts.google.com/o/oauth2/auth? scope= https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+ https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+ https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.login& request_visible_actions= https%3A%2F%2Fschemas.google.com%2FAddActivity& state= 65B4A4D1-0C49-4C65-9B46163E67D88EAD& redirect_uri= http%3A%2F%2FXXXXXXX.com%2Foauth%2Fgoogle%2F%3Fdo%3Dredirect& response_type= code& client_id= XXXXXXXX.apps.googleusercontent.com

    And on the permissions screen on Google+, this is what I see:

    enter image description here

    When I try to post an addActivity now, I get a bad request error as before.

    The error JSON returned:

    { "error": { "errors": [ { "domain": "global", "reason": "invalid", "message": "Invalid Value" } ], "code": 400, "message": "Invalid Value" } }
    

    The header returned:

    HTTP/1.1 400 Bad Request Content-Type: application/json; charset=UTF-8 Content-Encoding: gzip Date: Fri, 13 Sep 2013 11:38:20 GMT Expires: Fri, 13 Sep 2013 11:38:20 GMT Cache-Control: private, max-age=0 X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block Content-Length: 123 Server: GSE Alternate-Protocol: 443:quic 
    
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    The oauth is done on sogn in using oauth 2. It all works great and I use the access token given by Google to grab their basic info. So, I know that works. I don't store the token in the DB but I do place it in the session. If I need a new token even after just 1 minute, that's surprising; but even so, how would I request a new one without having to get the user to sign in again?
  • Lucas
    Lucas over 10 years
    You can get new valid token using refresh token developers.google.com/accounts/docs/OAuth2 I don't know the G+ API, but when using Contacts API I need to check the token very often (I'm actually checking it before every API request).
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    Ok, what you wrote led me on another path. I realised that I should be posting my request as JSON in the body, using POST. While I have now done this...it is giving me a different error saying that: { "error": { "errors": [ { "domain": "global", "reason": "required", "message": "Login Required", "locationType": "header", "location": "Authorization" } ], "code": 401, "message": "Login Required" } } But, nowhere in the docs on Google do I see how I can pass the access_token ? Any ideas?
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    these are the docs I am referring to: developers.google.com/+/api/latest/moments/insert
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    Thanks for all your help. I have realised I should be posting my data as JSON in the body of the HTTP. I have some new code, as seen in my UPDATE 1 above, but now I'm getting a new error that Login is required. I'm not sure how to pass the access_token in this method. Any further ideas? Thanks for all your help so far!
  • Prisoner
    Prisoner over 10 years
    The docs I linked to were for the OAuth2 portion. I've updated my reply
  • Lucas
    Lucas over 10 years
    Updated my original answer. The tools Google provides are quite useful as you can see what exactly they are sending. You need to provide the Authorization header param.
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    Thanks Lucas, doing that, now brings me back to an invalid credentials error. Strange thing is, if I use their API explorer, i get the same error! Is this thing just bust??!
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    Hmm, you could be on to something there. I have removed the app from my Google+ account and now started the OAuth process again. Here is the URL that is built after being redirected to Google+ ...I am using totally server-side solution. No JS or library of any kind. The URL is too long for this comment, so I will post it in my original question above under UPDATE 2. Please take a look - thanks!!
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    Hi Ian! Thanks. I will try this. With regard to the header...I was aware this was 'incorrect', and tried both. But, in my research, I found that some PHP users having a similar issue tried the prior 'incorrect' version and it seemed to work for them. A quirk perhaps.
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    I have tried the request_visible_actions in the OAuth process, but now I get an error saying there's a 'bad request 400'. Check this screen shot for the request details: i.imgur.com/G0qx9d9.png
  • BrettJ
    BrettJ over 10 years
    Updated, this is definitely the issue you're encountering.
  • Michael Giovanni Pumo
    Michael Giovanni Pumo over 10 years
    Thanks for all of your help, but I'm still getting a bad request error. This stuff is driving me nuts! I am going to update my question with UPDATE 3. If you wouldn't mind taking a look at that I'd really appreciate it. In essence, I have used the request_visible_actions in my OAuth and I need the scope userinfo.profile for their basic info. plus.login does not give me this information. I will copy the OAuth URL sent to Google and I will also screenshot what I see on the permissions screen. Thanks!
  • Ian Barber
    Ian Barber over 10 years
    Looks like it's complaining at the format of the moment being inserted. Try stripping it down to the minimum contents (type and target, with target containing just URL) and see if you still have the problem - then build up and you should be able to identify the bad field.
  • BrettJ
    BrettJ over 10 years
    Can your print out what your JSON request object looks like in string format?
  • BrettJ
    BrettJ over 10 years
    Also, you should not request both userinfo.profile and plus.login. You only need plus.login as it automatically includes access to the userinfo REST endpoint, but causes confusion on the consent dialog as well as it might be introducing issues with the token.