Working with Google and OAuth2

This post is more than 2 years old.

Warning: What follows should NOT be considered a guide. What follows is what worked for me after struggling to get things right. I do not understand this 100%. My only hope is that it may help others. Please take with large portion of salt.

Last night I began work on an update to my Google Calendar API to make use of the latest version of their API. In order to use this version, I had to switch to using OAuth. I've done a tiny bit of OAuth in the past, but never OAuth 2. I did a bit of digging into their docs and was able to get a working version. Here's what I came up with.

The first thing you have to do is create an "Application" registered with Google. This is done via their API Console and for the most part is a painless process. You begin by creating a new application.

Notice that - initially - you can't edit the Redirect URI. We're going to fix that in a second. Click "Create Client ID." When the page reloads, click to edit and change the redirect URI to be a real CFM. Note - you can use localhost here and it works fine. Just be sure to change it to http.

Close that dialog by hitting update. Back on the application list, make note of your client ID and client secret. Your code is going to need this. I set up a basic Application.cfc to store these in the Application scope:

component { this.name="googledocs3"; this.sessionManagement=true;

public boolean function onApplicationStart() {
	application.clientid="it's my pin, really";
	application.clientsecret="iwritephpatnight";
	application.callback="http://www.coldfusionjedi.com/demos/2012/dec/6/callback.cfm";
	return true;
}

}

Ok, so in order to start the OAuth process, we have to link to Google. The link to Google will include your client id, your redirect or callback url, and a scope. The scope is what you want to use at Google. Each service will have it's own scope. Here's the link I use for my demo:

<cfset authurl = "https://accounts.google.com/o/oauth2/auth?" & "client_id=#urlEncodedFormat(application.clientid)#" & "&redirect_uri=#urlEncodedFormat(application.callback)#" & "&scope=https://www.googleapis.com/auth/calendar&response_type=code">

<cfoutput> authurl=#authurl#<p> <a href="#authurl#">Login</a> </cfoutput>

I output it just so I can see what it looks like a bit easier. Note - most sites use a little popup window. That would work fine. The response_type=code is what you use for server side applications. At this point, you can start testing. If you click that link, you end up on a page like this:

At this point, if the user clicks "Allow access", Google is going to send you to your callback URL. In the URL will be a variable code. Now here comes the tricky part. Google sent you back a code. That code is like a ticket to ride. You need to give that code back to Google in order to get a token. The token is the real thing you want. I found this blog entry which nicely wraps up the call in a UDF:

<!--- http://www.sitekickr.com/blog/http-post-oauth-coldfusion ---> <cffunction name="getAccessToken"> <cfargument name="code" required="false" default="" type="string"> <cfset var postBody = "code=" & UrlEncodedFormat(arguments.code) & "&"> <cfset postBody = postBody & "client_id=" & UrlEncodedFormat(application.clientid) & "&"> <cfset postBody = postBody & "client_secret=" & UrlEncodedFormat(application.clientsecret) & "&"> <cfset postBody = postBody & "redirect_uri=" & UrlEncodedFormat(application.callback) & "&"> <cfset postBody = postBody & "grant_type=authorization_code"> <cfhttp method="post" url="https://accounts.google.com/o/oauth2/token"> <cfhttpparam name="Content-Type" type="header" value="application/x-www-form-urlencoded"> <cfhttpparam type="body" value="#postBody#"> </cfhttp> <cfreturn deserializeJSON(cfhttp.filecontent.tostring())> </cffunction>

<cfset session.token = getAccessToken(code)> <cfdump var="#session.token#">

<a href="test.cfm">TEST</a>

The result is that now you have a session token. That session token gives you access to the scope you requested earlier. Here is what that token looks like:

There are three very important things here:

  1. First, the access_token is the key to using the services. You want to remember that in the session scope.
  2. Second, it doesn't last forever. You can see the timeout there.
  3. Third, the refresh_token, however, does last. (Forever, no. I think it lasts until the user blocks your app's access.) This I think you will want to store in a persistent location.

So given the token, you can now start hitting the API. So for example, to get a list of calendars...

<cfhttp url="https://www.googleapis.com/calendar/v3/users/me/calendarList"> <cfhttpparam type="header" name="Authorization" value="OAuth #session.token.access_token#" > </cfhttp>

Google's Calendar API is REST based, so basically, you just formulate the URL right and pass along the token via an Authorization here. You get nice JSON back so it's pretty easy to work with. If you run my demo (link will be below), and if you actually are a Google Calendar user, you should get a list of your calendars. I tested a few other parts of the API and it all works rather nicely.

Now I mentioned above that the token does not last forever. Remember that 'refresh' token you got? You can request a new access token using a modified form of the earlier blogger's UDF:

<cffunction name="getRefreshToken"> <cfargument name="refresh" required="false" default="" type="string"> <cfset var postBody = "client_id=" & UrlEncodedFormat(application.clientid) & "&"> <cfset postBody = postBody & "client_secret=" & UrlEncodedFormat(application.clientsecret) & "&"> <cfset postBody = postBody & "refresh_token=#arguments.refresh#&"> <cfset postBody = postBody & "grant_type=refresh_token"> <cfhttp method="post" url="https://accounts.google.com/o/oauth2/token"> <cfhttpparam name="Content-Type" type="header" value="application/x-www-form-urlencoded"> <cfhttpparam type="body" value="#postBody#"> </cfhttp> <cfreturn deserializeJSON(cfhttp.filecontent.tostring())> </cffunction>

Notice that this just slightly tweaks the values sent. In my testing, a call to this refresh service worked fine. I was able to get a new access token after the last one expired. You can try this yourself using the button below. I hope this code is helpful to others.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Derek Winstead posted on 12/7/2011 at 1:02 AM

When you click the link for "TEST" and goes to test.cfm it throws an error.

Comment 2 by Raymond Camden posted on 12/7/2011 at 1:06 AM

It would help if I uploaded the file. Done.

Comment 3 by Chris Walker posted on 12/7/2011 at 1:36 AM

I just did a whole suite of tools for a client. Took a while to get it running. The api spec is typically loaded with tons of developer assumptions about the audience. But once you make 100 or so mistakes the flow starts to make sense. I am in the process of scoping out all big google apis to oauth 2. Feel free to contact me for any hints.

Comment 4 by Derek Winstead posted on 12/7/2011 at 1:41 AM

@Chris - I would like to get some information, you can add me on Google+ here to talk: https://plus.google.com/u/0...

Comment 5 by Brian Swartzfager posted on 12/7/2011 at 1:47 AM

Good stuff (as usual). The whole "get ticket, then send ticket back to get token" routine is pretty much the same deal as what I have to do to connect my ColdFusion apps to our campus Jasig CAS (Common Authentication System) implementation.

Comment 6 by Chris Walker posted on 12/8/2011 at 8:34 AM

I'm not sure what's going on with Oauth 2 over at Google (https://accounts.google.com... but tonight, we are no longer receiving the refresh_token. It seems to be replaced with something called id_token.

My client noticed it this afternoon and about 8PM tonight I saw it.

Ray, to check, cfdump cfhttp.filecontent in method getAccessToken and see if the refresh token has gone away. No notes out there on Google about it.

I should note that i wrote my own a few weeks back, but its basically the same concept.

Comment 7 by Raymond Camden posted on 12/8/2011 at 5:33 PM

Not sure. My Googling didn't find anything definitive. It does seem to be related to OpenID. That's the best I can say. In my testing last night, I did not see this.

Comment 8 by Chris Walker posted on 12/8/2011 at 9:32 PM

I started a thread: https://groups.google.com/f...

I already have received one reply that validates my discovery and offers some insite.

Comment 9 by Raymond Camden posted on 12/8/2011 at 9:52 PM

You mind posting back here when you get a final resolution?

Comment 10 by Chris Walker posted on 12/8/2011 at 10:56 PM

Monday, November 14, 2011
Upcoming changes to OAuth 2.0 endpoint

By Justin Smith, Senior Product Manager

http://googlecode.blogspot....

Comment 11 by Chris Walker posted on 12/8/2011 at 11:20 PM

if you concat "&access_type=offline&approval_prompt=force" to the end of your login link, the Oauth process works exactly as it did before they changed the API.

Comment 12 by Raymond Camden posted on 12/9/2011 at 12:43 AM

Wow, thank you. And note:

UPDATE 11/14: After considering the feedback and timing, we have decided to delay the launch of the changes described in this post. The new date for these changes is December 7th, 2011. Once again, we expect these changes to have minimal impact. If you have any questions or comments, please post on the OAuth 2.0 Group. We will be actively monitoring that group and will work to respond quickly.

Comment 13 by Christopher Walker posted on 1/4/2012 at 7:05 AM

Please feel free to check out my Google Oauth2 harness for ColdFuion

The oAuth 2.0 Connector for ColdFusion 8 and 9 allows developers to quickly integrate the Google oAuth 2.0 API to their site. Available modules include: oAuth API connector, Contacts API connector, and Calendar API connector. Developers can view authenticated user's basic contact information, upload an event, view contact lists, add contacts and view calendar metas.

Great for kick starting your jQuery Mobile or Flash Builder/Android application.

http://www.appwise.org/goog...

Comment 14 by Chris Hampton posted on 4/22/2012 at 9:42 AM

I'm trying to integrate with WePay (who uses OAuth2) and I can't get the Authorization header to work. According to the WePay documentation, it needs to be send in the following format:
Authorization: Bearer <access-token>

I've tried several permutations of this with cfhttpparam, but none of them work. Has anyone tried this before? Am I missing something stupid?

I've tried

<cfhttpparam type="header" name="Authorization" value="Bearer #Application.WepayToken#" />

<cfhttpparam type="header" name="Authorization" value="Authorization: Bearer #Application.WepayToken#" />

<cfhttpparam type="header" name="Authorization" value="Bearer:#Application.WepayToken#" />

I'm pretty sure by the time I get this working, I won't have any hair left. If anyone has any ideas or thoughts, please let me know.

Thank you

Comment 15 by Christopher Walker posted on 4/22/2012 at 5:17 PM

Not quite sure what you're doing but here is how I return the bearer token (arguments.access_token) to Google in their oAuth2 process.
<code>
<cfhttp method="get" url="https://www.google.com/m8/f..." result="latestContacts">
<cfhttpparam type="header" name="GData-Version" value="3.0">
<cfhttpparam type="header" name="Authorization" value="GoogleLogin auth=#arguments.access_token#">

</cfhttp>
</code>

Comment 16 by Christopher Walker posted on 4/22/2012 at 6:39 PM

According to the manual (https://stage.wepay.com/dev...

you should try:

<cfhttpparam type="header" name="Authorization" value="Bearer #Application.WepayToken#" />

Comment 17 by Chris Hampton posted on 4/22/2012 at 8:10 PM

Thanks Christopher. There must be something else going one because that was the first combination I tried.

<cfhttp url="#Application.WepayApiUrl#" method="post">
<cfhttpparam type="header" name="Authorization" value="Bearer #Application.WepayToken#" />
</cfhttp>

I've tried as a post and as a get, but neither seem to make a difference. Each time it returns a statuscode 501.

At this point, I'm guessing the problem lies somewhere other than the cfhttpparam tag, so it's probably beyond the scope of this blog post. If I find an answer, I'll post it for those with morbid curiosity.

Comment 18 by Chris Hampton posted on 4/22/2012 at 10:41 PM

Well...

It looks like the default example they have shown to return your user info does not work, but the other API calls work just fine. So there was nothing wrong with what I was doing, but instead something with that particular example. Now I've got a basic model of an interface to their API working.

Thanks for the advice.

Comment 19 by Clayton Stone posted on 5/1/2012 at 8:20 AM

Is your oauth 2.0 / coldfusion / gcal api project on hold or forthcoming ?

Comment 20 by Raymond Camden posted on 5/1/2012 at 3:17 PM

On hold just because I do not have the time. If I need to github this so you can fork it/add to it, let me know.

Comment 21 by Clayton Stone posted on 5/2/2012 at 7:43 AM

I'm a beginner. No need to github it. I've been trying to implement on my site. Just hoping to peruse your code and glean what I could from it. Many thanks though.

Comment 22 by Raymond Camden posted on 5/2/2012 at 3:52 PM

Looks like I did not post the demo. Best I can suggest is using the code in the example above.

Comment 23 by Michael Ennis posted on 5/8/2012 at 9:57 PM

Thanks for this Ray! I've got the auth working and can use token to hit API. Could you suggest the best way to automagically retrieve a new access token using a refresh token stored in a db when the access token expires?

Thx!

Comment 24 by Raymond Camden posted on 5/9/2012 at 1:14 AM

I don't have an example of it handy - but I believe you pass the refresh token back to the API. I remember playing with it back when I did this blog post. It _should_ be simple. (Speak up if not.)

Comment 25 by Clayton Stone posted on 6/16/2012 at 8:53 AM

Just wanted to say thanks for all the info, tutorials and suggestions that you and other developers put out freely on the web. Using your demo and other bits of info from coldfusion blogs like yours, I've been able to start working on my own udfs for the Google Calendar API. What I have so far just performs very simple operations, but it works and I've learned a lot in the process.
Thanks again,
CRS

Comment 26 by Raymond Camden posted on 6/16/2012 at 6:29 PM

You are most welcome.

Comment 27 by Randy Johnson posted on 7/13/2012 at 6:57 AM

I wanted to clarify:

On the getRefreshToken, I am guessing you would check an error message being returned from Google stating the token has expired and then you would call that function and try again? Or is there a better way to do that?

Comment 28 by Randy Johnson posted on 7/13/2012 at 7:04 AM

I also noticed that when using your example that the perform these operations when I am not using this application is missing. From what I can see you have to choose another option and it gives you a private key to download. Anyone have any sample code for that?

Comment 29 by Raymond Camden posted on 7/13/2012 at 11:19 PM

+Randy1 - yea - If I remember right you would get an error that made it clear you needed to refresh the token. I recommend checking the docs to be sure.

+Randy2 - sorry - no idea what your second comment is saying.

Comment 30 by Tom Ridings posted on 8/18/2012 at 7:28 AM

Hi Ray,

Thanks so much for all the work you have done on the coldfusion google API stuff, has been a massive help over the last couple of weeks.

I have managed to successfully authorize my app for logging into the api however I am running into the infamous XMLparse error when trying to parse the contents of the returned XXX.filecontent

Dumping XXX i can see the data but am still getting the parsing errors.

Have tried xmlparse( trim( XXXX ) ) but with no success

Woud seem unusual for google to have incorrectly formatted their XML though.

Thanks in advance for any help you can give.

Comment 31 by Raymond Camden posted on 8/18/2012 at 6:12 PM

Could you take the XML, save it, and send me a copy?

Comment 32 by Craig Larson posted on 10/11/2012 at 5:22 AM

When running the function getAccessToken, I am receiving an error: "JSON parsing failure at character 1:'C' in Connection Failure". the only thing I can think of is that I do not have a cert installed for my site. Is this a requirement for doing the cfhttp call? Looking at your demo, it does not look like you have a cert installed.

Comment 33 by Raymond Camden posted on 10/11/2012 at 5:26 AM

Hmm - I don't remember adding a cert for google. That being said, it _could_ be an issue for you. Also - it may be a general network issue. If you RDP to the machine (or rlogin), can you hit accounts.google.com from the server itself?

Comment 34 by Craig Larson posted on 10/11/2012 at 5:45 AM

Unfortunately, I am trying to get this to work on a hosted site on Hostek. No access to RDP into the machine itself. It could very well be something on their side. I will try to contact them and see what's up.

Thanks for a quick reply. If I can get any information I'll share it here for others who may run into the error.

Comment 35 by Raymond Camden posted on 10/11/2012 at 5:58 AM

I'd recommend building a simple CFM with only a cfhttp to the url. Dump the result. That way it is easier to show them the connection issue.

Comment 36 by Rick Smith posted on 11/6/2012 at 8:44 AM

Hey Ray, just went to riaforge to checkout your Google Cal ater reading this post... is that the "updated" version or the old version? If not, do you plan on updating it?

Comment 37 by Grant posted on 11/7/2012 at 10:36 PM

I have followed this line by line and I keep getting "HTTP/1.1 405 Method Not Allowed Allow: POST Connection: close Expires: Wed, 07 Nov 2012 17:35:19 GMT Date: Wed, 07 Nov 2012 17:35:19 GMT Server: GSE X-Frame-Options: SAMEORIGIN Cache-Control: private, max-age=0 X-XSS-Protection: 1; mode=block Content-Type: text/html; charset=UTF-8 X-Content-Type-Options: nosniff"

I have been researching this for over 5 hours and the closest lead I have is something related to CORS - totally frustrated. Any ideas?

Comment 38 by Raymond Camden posted on 11/8/2012 at 12:27 AM

When do you get it?

Comment 39 by Grant posted on 11/8/2012 at 3:11 AM

I was getting it when I was trying to get the Token from the Code.
I wiped out all my code and started over and it works now.
Thanks.

Comment 40 by Raymond Camden posted on 11/8/2012 at 3:33 AM

Nice!

Comment 41 by Raymond Camden posted on 11/9/2012 at 2:42 AM

@Rick Smith: I honestly don't know. :(

Comment 42 by Marc de Ruijter posted on 12/5/2012 at 10:12 PM

Hi Raymond,

Last year you said that this blogpost was not a guide. I found another guide, $30, updated last year but no one commented or liked so i won't buy it.

I have a small SMS app, actually personal for me to send easy, free SMS from the keyboard, and i would like to integrate this small webapp with Google contacts (i want to send an sms to, say, 30% of my contacts).

Any change of you explaining how i can easily, simple, few lines of code, can integrate with g.contacts ?

Regards.

Comment 43 by Raymond Camden posted on 12/10/2012 at 4:02 AM

Marc, I just did this and it was rather easy.

First, you have to change the SCOPE value to use the one for Google Contacts. According to the Contacts API docs, that is:

https://www.google.com/m8/f...

That's it. Literally. I skimmed the API docs and saw that this URL represents the currently authenticated user and their complete contact list:

https://www.google.com/m8/f...

So I used this:

<cfhttp url="https://www.google.com/m8/f...">
<cfhttpparam type="header" name="Authorization" value="OAuth #session.token.access_token#" >
</cfhttp>
<cfdump var="#xmlParse(cfhttp.filecontent)#">

and it dumped my contacts.

Comment 44 by Marc posted on 12/15/2012 at 9:04 PM

Raymond, thanks for your answer.

How do i get the #session.token.access_token# ?
I find the Oauth2 proces very hard.

Comment 45 by Raymond Camden posted on 12/15/2012 at 11:01 PM

In the guide above I describe how this is set. Please reread it.

Comment 46 by JP posted on 12/18/2012 at 8:38 PM

Thank you!
Awesome!

Comment 47 by Allan Johnson posted on 12/12/2013 at 9:48 PM

Just an FYI the concat "&accesstype=offline&approvalprompt=force" to the end of your login link mentioned above should have underscores:

"&access_type=offline&approval_prompt=force"

In order to get refresh token.

Comment 48 by Raymond Camden posted on 12/12/2013 at 9:49 PM

Thanks for sharing that, Allan.

Comment 49 by Allan Johnson posted on 12/12/2013 at 9:50 PM

Not sure why, but it removed the underscores in my post (Or I left them out). Try that again:

"&access_UNDERSCORE_type=offline&approval_UNDERSCORE_prompt=force"

Comment 50 by Ian Finch posted on 10/29/2014 at 7:50 AM

In case it may help someone else - I struggled with the HTTP header for the access token, getting 401 errors - turns out that "AuthoriZation" is not the same as "AuthoriSation". Two hours of needless pain ... :)

Comment 51 (In reply to #32) by Bern e Bern posted on 11/1/2018 at 1:48 PM

I'm getting this too. Did you figure it out?