|
|
|
|
|
Seems like a simple request: Display events from Google calendar on a website. It also seems like good skill for a web developer that can add value for customers. But I spent quite a bit of time hacking my way to these objectives. And if anyone else is motivated to undertake this effort, perhaps my experience can help.
|
|
Google uses REST and OAuth2. The good thing about REST is that examples can be presented as a single curl command. But Google is quite fussy about implementation details, and doesn't provide many examples. Google's examples all rely on their libraries and only a handful of languages. So it's hard to distill the actual REST queries. The examples below are written in curl. Curl is universal and anything unfamiliar has a straightforward explanation on the curl man page.
|
|
Most of the difficulty seems to lie in the OAuth2 implementation. Perhaps a solid understanding would have made my experience easier. Provided Google adheres to the standard, this explanation probably illuminates that protocol as well.
|
|
My website's role is an information consumer, and Google calendar owners are the information providers. In order to establish credentials, the consumer needs to be a registered Google app. The registration form is mysteriously called the API Console. And since Google already knows everything about everybody, the form is quite short.
|
|
Subsequently, Google assigns credentials, which consist of a Client ID and a Client Secret. There's also a Developer Key which I haven't found a use for yet.
|
|
A token is required to access the goods from an information provider. A token represents permission granted from one provider to one consumer. If your application is a mashup of calendars from a number of accounts, it will need to manage a number of tokens.
|
|
In order to generate the token, the user, a real live human being who has an account at Google, needs to approve a request based on the credentials. The finer points of social engineering are left as an exercise for the reader, but it boils down to encouraging users to click a link. The link's URL contains the consumer application's credentials from Step 1 in an appended search string including a callback URI. (The search segment of a URL is everything after the question mark.) The callback URL and scope are defined using the API console, and at least the callback URI needs to match.
|
|
base url: https://accounts.google.com/o/oauth2/auth
|
|
Credential arguments passed in the search string:
|
|
redirect_uri => http://www.tqis.com/pen/misc/googleauth.htm client_id => 51357231285.apps.googleusercontent.com
|
|
Also include the following:
|
|
response_type => code approval_prompt => force access_type => offline scope => https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.readonly
|
|
The instructions for building a search string and appending can be found in the curl man page. Ultimately, the result should look like this:
|
|
https://accounts.google.com/o/oauth2/auth?response_type=code&approval_prompt=force&redirect_uri=http://www.tqis.com/pen/misc/googleauth.htm&client_id=51357231285.apps.googleusercontent.com&access_type=offline&scope=https://www.googleapis.com/auth/calendar+https://www.googleapis.com/auth/calendar.readonly
|
|
From a user's point of view, this mysterious link takes them to a Google log-in screen. Upon login, they see some version of your credentials, some scope details (in this case, a chance to peek at their calendars), and two buttons to approve or disapprove access. If access is approved, the user is redirected to the specified callback URL. Most of this discussion is how to build a client that can request information from Google. But at this point, your app needs to act like a server to retrieve an external request from a user.
|
|
The difficulty at this point is meaningful content for someone who thinks s/he can trust you. Whether you forswear mischief or pop up some legalese that eternally indemnifies you, the whole point is to retrieve a code appended as a search string to the callback URL. My technically neutral recommendation is to recover the search string from the server logs. Each time the user performs this sequence, a different code is generated. And only the first code will work. Once you're in production, you can process this code request immediately, cache the resulting token, and ignore any subsequent requests.
|
|
But at this point, it'll be hard enough to avoid overwriting the only working code response. So maybe using the server logs is the best approach.
|
|
In Step 2, you carefully constructed a GET request for the Google server. In this step, you send the same credentials as a POST request directly to the Google server, along with the grant code. Google only gives you one chance, so take your time preparing this request.
|
|
Base URL: https://accounts.google.com/o/oauth2/token
|
|
Credential Arguments:
|
|
redirect_uri => http://www.tqis.com/pen/misc/googleauth.htm client_id => 51357231285.apps.googleusercontent.com client_secret => xAdefghijklmnopqrstuvR-a
|
|
Results from Step 3:
|
|
code => 4/p-5LIsWNAG9jW0Yxh-_2CY386j0T.4t412yQtB8IauJJVnL49Cc-kYXnvdwI
|
|
And the following:
|
|
grant_type => authorization_code
|
|
Google is also fussy about the HTTP headers. I find I need to override the defaults. Here is the resulting curl command:
|
|
curl -d "redirect_uri=http%3A%2F%2Fwww.tqis.com%2Fpen%2Fmisc%2Fgoogleauth.htm&client_id =51357231285.apps.googleusercontent.com&client_secret=xAdefghijklmnopqrstuvR%2Da&code=4%2Fp%2D5LIsWNAG9jW0Yxh%2D%5F2CY386j0T.4t412yQtB8IauJJVnL49Cc%2DkYXnvdwI&grant_type=authorization%5Fcode" -H "Content-Type: application/x-www-form-urlencoded" https://accounts.google.com/o/oauth2/token
|
|
If everything goes well, you should receive a token as plaintext. The results, in JSON format, are easy enough to read:
|
|
{ "access_token" : "ya29.AHES6ZabcdefghijklmnopqrstuvwxyzGDox2Bkc9nXLe5-fmher4w", "token_type" : "Bearer", "expires_in" : 3600, "refresh_token" : "1/QfPe3-abcdefghijklmnopqrstuvwxyztTF35HBv8KY" }
|
|
A common error is Invalid Grant. Grant in this case is synonymous with code. The code can only be used once. And subsequent codes won't work until the first code expires, probably an hour. I ended up using the Google accounts of everyone I know trying to complete this step.
|
|
I use the term token to refer to everything in the response shown above. This token should last indefinitely. So keep it in a safe place. (Unlike the PHP sample provided by Google where the token is kept in a SESSION variable.) Here are the components of the token:
|
|
access_token
|
Use this token string when querying Google
|
|
refresh_token
|
Use this token string to renew the access_token
|
|
token_type
|
The Bearer value is probably constant
|
|
expires_in
|
The lifespan of the access_token, apparently an hour
|
|
|
|
|
Ultimately, you need to know when the access token expires. But because your local time may not be synchronized with the Google server, the expiration time should be based on your local time. That is, add the expires_in component to the local time of either the request or the response and save the result as the expiration of your token. The token will need to be updated after every expiration.
|
|
The hour lifetime of the access_token is not very long. Step 6 will demonstrate how to use the access_token, but if you're not moving along at a good clip, you may need to skip to Step 7 and update the access_token first.
|
|
As defined earlier, a token establishes a relationship between an information provider and yourself, an information consumer. You may need to manage multiple tokens for multiple providers. This example will demonstrate a query to identify a Google account based on a token.
|
|
The access token is incorporated into the REST request as a HTTP header field named authorization. The field value is a concatenation of the token_type and access_token components separated by a space. This is a single argument to the curl command, and it looks like this:
|
|
-H "authorization: Bearer ya29.AHES6ZabcdefghijklmnopqrstuvwxyzGDox2Bkc9nXLe5-fmher4w"
|
|
You'll probably also need to override the content-type header:
|
|
-H "Content-Type: application/http"
|
|
The URL https://www.googleapis.com/calendar/v3/users/me/calendarList includes the component me that instructs Google to look up the user based on the access token. Here's the entire curl command:
|
|
curl -H "authorization: Bearer ya29.AHES6ZabcdefghijklmnopqrstuvwxyzGDox2Bkc9nXLe5-fmher4w" -H "Content-Type: application/http" https://www.googleapis.com/calendar/v3/users/me/calendarList
|
|
Provided the token hasn't expired, you should get the following response:
|
|
{ "kind": "calendar#calendarList", "etag": "\"fi3QRdJKQhyHAzX8fKt0yeht0TY/7I78BlLGM44SQkrLe5Z_RD2dKOI\"", "items": [ { "kind": "calendar#calendarListEntry", "etag": "\"fi3QRdJKQhyHAzX8fKt0yeht0TY/tYaU_Io8Yc6gQPNcKx1mI3TluhI\"", "id": "user@gmail.com", "summary": "user@gmail.com", "timeZone": "UTC", "colorId": "17", "backgroundColor": "#9a9cff", "foregroundColor": "#000000", "selected": true, "accessRole": "owner", "defaultReminders": [ { "method": "popup", "minutes": 30 }, { "method": "email", "minutes": 30 } ] } ] }
|
|
You'll need a good JSON parser to automatically pull the id user@gmail.com out of this response. And it'll be helpful to save this id component inside the token.
|
|
Google's documentation doesn't specifically refer to a token. In this discussion, I've used the term token to describe an object with the string components described above: access_token, refresh_token, etc. When the token expires, the access_token needs to be replaced, but the refresh_token remains intact. Updating the token involves querying Google for a new access_token and replacing that component in the token object.
|
|
The token lasts indefinitely due to the persistence of the refresh_token. But the token expires with the access_token, an hour after being updated. It may be simpler to ignore the expiration and simply acquire a new access_token for each query. In which case the refresh_token used in the query below is the only necessary component. This query is similar to Step 4.
|
|
Credential Arguments:
|
|
client_id => 51357231285.apps.googleusercontent.com client_secret => xAdefghijklmnopqrstuvR-a
|
|
Refresh Token:
|
|
refresh_token => 1/QfPe3-abcdefghijklmnopqrstuvwxyztTF35HBv8KY
|
|
And the following:
|
|
grant_type => refresh_token
|
|
The curl command looks like this:
|
|
curl -d "grant_type=refresh_token&refresh_token=1%2FQfPe3%2DabcdefghijklmnopqrstuvwxyztTF35HBv8KY&client_secret=xAdefghijklmnopqrstuvR%2Da&client_id=51357231285.apps.googleusercontent.com" -H "Content-Type: application/x-www-form-urlencoded" https://accounts.google.com/o/oauth2/token
|
|
This request works more reliably, and returns the following:
|
|
{ "access_token" : "ya29.AHES6ZSXNoYabcdefghijklmnopqrstuvwxyzzPKr0kRjJY", "token_type" : "Bearer", "expires_in" : 3600 }
|
|
|
|
Here's a curl example that returns upcoming events from a Google calendar, using the the following parameters and the new access token from Step 7:
|
|
singleEvents => true orderBy => startTime timeMin => 2012-12-30T00:00:00-05:00
|
|
curl -H "authorization: Bearer ya29.AHES6ZSXNoYabcdefghijklmnopqrstuvwxyzzPKr0kRjJY" -H "Content-Type: application/http" https://www.googleapis.com/calendar/v3/calendars/user@google.com/events?singleEvents=true&orderBy=startTime&timeMin=2012%2D12%2D30T00%3A00%3A00%2D05%3A00
|
|
|
 |