Gaugette

Building gauges and gadgets with Arduinos, Raspberry Pis and Switec stepper motors.

Nov 6, 2012 - 3 minute read - Comments - Raspberry Pi

Using Google OAuth2 for Devices

I’ve been working on a Raspberry Pi based device that needs to log data to a Google Docs spreadsheet. There are some fine classes like gdata and gspread that do just this.

However if you read the tutorial at Adafruit which uses the gspread library you will see that it uses the ClientLogin API for authentication, and so somewhere you need to include your Google username and password in plain text. There are several problems with this in the context of an embedded device:

  • You need to provide a way for the user to enter their credentials on the device which may have no keyboard or GUI,
  • The credientials are stored in a non-secure way,
  • The ClientLogin API is officially deprecated,
  • I personally live in fear of accidentally committing code with plain-text credentials to GitHub.

So I don’t want to use ClientLogin.

It turns out the Google’s OAuth2 implementation has a great alternative for embedded applications; OAuth 2.0 for Devices. Here’s the high-level overview of how the authentication works:

  1. Just once, register your application with the Google APIs console and get a client_key and client_secret,
  2. When your application runs for the first time, it sends an authentication request to Google which returns a short plain-text user_code,
  3. Your device shows the user_code to the user and gives them instructions to enter that code at http://www.google.com/device.
  4. Your application starts polling a Google web service waiting for confirmation that the user has approved the application,
  5. The user enters the code to Google’s form, and confirms the level of permissions the application has requested,
  6. Your application gets a response from the web service it has been polling with an access_token and a refresh_token.
  7. You pass this access_token in the header of all API requests

The access_token will expire after a relatively short time (1 hour?). When the token expires, it can be refreshed by making another API call using the refresh_token. This returns a fresh access_token but does not return an udpated refresh_token. The original refresh_token does not expire.

I could not find any documentation explaining how to use the access_token with any of the existing API libraries. As far as I can tell Google’s python gdata library doesn’t have explicit support for this form of authentication, but it turns out you can make it work if you send an appropriately formatted Authorization header to the service constructor like this:

headers = {
  "Authorization": "%s %s" % (token['token_type'], token['access_token'])  
}
client = gdata.spreadsheet.service.SpreadsheetsService(additional_headers=headers)

The extra header will be something like Authorization: Bearer 1/fFBGRNJru1FQd44AzqT3Zg. I’ve wrapped up what I’ve figured out so far into an oauth class in the python gaugette library “py-gaugette”.

Here’s an example of how it can be used to authenticate and add a row to the end of a spreadsheet. The library saves the authentication tokens to the local file system so that the user only has to authenticate the device once. The explicit token-expiration handling isn’t great. In ruby I could encapsulate that nicely using block parameters, but I haven’t yet figured out a clean abstraction for python. The user can revoke access to the device via this Google page.

import gaugette.oauth
import datetime
import gdata.service
import sys

CLIENT_ID       = 'your client_id here'
CLIENT_SECRET   = 'your client secret here'
SPREADSHEET_KEY = 'your spreadsheet key here'

oauth = gaugette.oauth.OAuth(CLIENT_ID, CLIENT_SECRET)
if not oauth.has_token():
    user_code = oauth.get_user_code()
    print "Go to %s and enter the code %s" % (oauth.verification_url, user_code)
    oauth.get_new_token()

gd_client = oauth.spreadsheet_service()
spreadsheet_id = SPREADSHEET_KEY
try:
    worksheets_feed = gd_client.GetWorksheetsFeed(spreadsheet_id)
except gdata.service.RequestError as error:
    if (error[0]['status'] == 401):
        oauth.refresh_token()
        gd_client = oauth.spreadsheet_service()
        worksheets_feed = gd_client.GetWorksheetsFeed(spreadsheet_id)
    else:
        raise

worksheet_id = worksheets_feed.entry[0].id.text.rsplit('/',1)[1]

now = datetime.datetime.now().isoformat(' ')
row = {
    'project': 'datatest',
    'start'  : now,
    'finish' : now
    }

try:
    gd_client.InsertRow(row, spreadsheet_id, worksheet_id)
except gdata.service.RequestError as error:
    if (error[0]['status'] == 401):
        oauth.refresh_token()
        gd_client = oauth.spreadsheet_service()
        gd_client.InsertRow(row, spreadsheet_id, worksheet_id)
    else:
        raise

print "done"