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:
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;
} }
component {
this.name="googledocs3";
this.sessionManagement=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:
<cfoutput>
authurl=#authurl#<p>
<a href="#authurl#">Login</a>
</cfoutput>
<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">
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:
<cfset session.token = getAccessToken(code)>
<cfdump var="#session.token#"> <a href="test.cfm">TEST</a>
<!---
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>
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:
- First, the access_token is the key to using the services. You want to remember that in the session scope.
- Second, it doesn't last forever. You can see the timeout there.
- 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.
Archived Comments
When you click the link for "TEST" and goes to test.cfm it throws an error.
It would help if I uploaded the file. Done.
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.
@Chris - I would like to get some information, you can add me on Google+ here to talk: https://plus.google.com/u/0...
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.
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.
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.
I started a thread: https://groups.google.com/f...
I already have received one reply that validates my discovery and offers some insite.
You mind posting back here when you get a final resolution?
Monday, November 14, 2011
Upcoming changes to OAuth 2.0 endpoint
By Justin Smith, Senior Product Manager
http://googlecode.blogspot....
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.
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.
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...
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
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>
According to the manual (https://stage.wepay.com/dev...
you should try:
<cfhttpparam type="header" name="Authorization" value="Bearer #Application.WepayToken#" />
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.
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.
Is your oauth 2.0 / coldfusion / gcal api project on hold or forthcoming ?
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.
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.
Looks like I did not post the demo. Best I can suggest is using the code in the example above.
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!
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.)
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
You are most welcome.
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?
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?
+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.
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.
Could you take the XML, save it, and send me a copy?
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.
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?
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.
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.
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?
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?
When do you get it?
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.
Nice!
@Rick Smith: I honestly don't know. :(
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.
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.
Raymond, thanks for your answer.
How do i get the #session.token.access_token# ?
I find the Oauth2 proces very hard.
In the guide above I describe how this is set. Please reread it.
Thank you!
Awesome!
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.
Thanks for sharing that, Allan.
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"
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 ... :)
I'm getting this too. Did you figure it out?