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.