2
votes

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 
4
Hey Michael, did you ever get this figured out? If so, do you mind pointing out what solved your problem so others, like me for instance, can benefit? :) Thanks! - jmort253

4 Answers

2
votes

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

1
votes

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.

1
votes

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.

1
votes

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.