Monday, 11 January 2016

Fitbit API Access Using OAUTH2.0 and Raspberry Pi

Previously I've blogged on accessing the Fitbit API to do sleep analysis, a sleep infographic and to get per minute data.

This used the OAUTH1.0 authentication method but as of March 2016 that was deprecated.  The requirement is to move to using OAUTH2.0 instead so this post is about how I rolled up my sleeves and did that!

The Fitbit developer site has excellent documentation on using OAUTH2.0 to access the API.  I basically followed this step by step in order to gain access.  I'll describe what I actually did and give code examples below.

Note this is for the general hobbiest, not for someone trying to do this professionally!

Step 1 - Register App
Go to step 1 of my original Fitbit API post and carry out the steps to register you application.  Then come back here.

To do OAUTH2.0 you also need to go onto the Fitbit developer site and:

  • Select "MANAGE MY APPS", select your app and log your "OAuth 2.0 Client ID".  (I assume you've already got your "Client (Consumer) Secret" logged).
  • Then select "Edit Application Settings" and set your "OAuth 2.0 Application Type" to "Personal".

...you're all set to go!

Step 2 - Get an Authorisation Code
Carry out the steps under "Authorization Page" section of the OAUTH2.0 documentation.  This simply means forming a URL, pasting it into a browser and then following the steps on the resulting web page to authorise the app to use your data.

https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=22942C&redirect_uri=http%3A%2F%2Fexample.com%2Fcallback&scope=activity%20nutrition%20heartrate%20location%20nutrition%20profile%20settings%20sleep%20social%20weight

The example URL above is from the Fitbit documentation.  Simply change the client_id to the one you logged above to do it for you.  "response_type=code" means your request type is "Authorization Code Flow".  From reading the documentation this means you get both an access token and a refresh token for using the API; more on this later.

This will result in Fitbit redirecting to the callback URL you specified when registering your application and appending an "Authorization Code" to the end of the URL.  Log this authorisation code for later use.

Step 3 - Get Access and Refresh Tokens
You now need to use your authorisation code to obtain your first access and refresh tokens.  You need to do this within 10 mins of step 2 above.

How to do this is specified in the section "Access Token Request" of the Fitbit OAUTH2.0 page.  To follow these steps I wrote some Python script on my Raspberry Pi to put the parameters together and execute the HTTP POST that you need to do.  Simply take the code below and enter your client ID, your consumer secret, your authorisation code and your redirect URL.

If it works the result will be a JSON response printed to screen that shows your first access token and refresh token.  Log these for use in the next step!

import base64
import urllib2
import urllib

#These are the secrets etc from Fitbit developer
OAuthTwoClientID = "Your_ID_Here"
ClientOrConsumerSecret = "Your_Secret_Here"

#This is the Fitbit URL
TokenURL = "https://api.fitbit.com/oauth2/token"

#I got this from the first verifier part when authorising my application
AuthorisationCode = "Your_Code_Here"

#Form the data payload
BodyText = {'code' : AuthorisationCode,
            'redirect_uri' : 'http://pdwhomeautomation.blogspot.co.uk/',
            'client_id' : OAuthTwoClientID,
            'grant_type' : 'authorization_code'}

BodyURLEncoded = urllib.urlencode(BodyText)
print BodyURLEncoded

#Start the request
req = urllib2.Request(TokenURL,BodyURLEncoded)

#Add the headers, first we base64 encode the client id and client secret with a : inbetween and create the authorisation header
req.add_header('Authorization', 'Basic ' + base64.b64encode(OAuthTwoClientID + ":" + ClientOrConsumerSecret))
req.add_header('Content-Type', 'application/x-www-form-urlencoded')

#Fire off the request
try:
  response = urllib2.urlopen(req)

  FullResponse = response.read()

  print "Output >>> " + FullResponse
except urllib2.URLError as e:
  print e.code
  print e.read()

Step 4 - Make and API Call and Refresh Tokens
This follows the steps described under the "Making Requests" and "Refreshing Tokens" section of the Fitbit OAUTH2.0 document.

In simple terms, you make a request using the access token.  This has a limited lifetime (one hour) so when it runs out you use the refresh token to get a new access token (to use now) and a new refresh token (to get the next access token).

To get data from the API and get new tokens (if required) I used the code pasted in below.  In simple terms this:

  • Reads the current tokens from a text file.  I created this text file on my Raspberry Pi, pasted in my access token, pressed return then pasted in the refresh token.  (Both tokens from step 3 above).
  • Makes a HTTP GET to the API URL using the access token.  If this works, happy days.
  • If the HTTP GET doesn't work, it does a HTTP POST using the refresh token and logs the new tokens to file ready for next time.

To make it work:

  • Edit the IniFile variable to specify where you have your file stored.
  • Enter your client ID and client secret.

import base64
import urllib2
import urllib
import sys
import json
import os

#This is the Fitbit URL to use for the API call
FitbitURL = "https://api.fitbit.com/1/user/-/profile.json"

#Use this URL to refresh the access token
TokenURL = "https://api.fitbit.com/oauth2/token"

#Get and write the tokens from here
IniFile = "/home/pi/fitbit/tokens.txt"

#From the developer site
OAuthTwoClientID = "Your_ID_Here"
ClientOrConsumerSecret = "Your_Secret_Here"

#Some contants defining API error handling responses
TokenRefreshedOK = "Token refreshed OK"
ErrorInAPI = "Error when making API call that I couldn't handle"

#Get the config from the config file.  This is the access and refresh tokens
def GetConfig():
  print "Reading from the config file"

  #Open the file
  FileObj = open(IniFile,'r')

  #Read first two lines - first is the access token, second is the refresh token
  AccToken = FileObj.readline()
  RefToken = FileObj.readline()

  #Close the file
  FileObj.close()

  #See if the strings have newline characters on the end.  If so, strip them
  if (AccToken.find("\n") > 0):
    AccToken = AccToken[:-1]
  if (RefToken.find("\n") > 0):
    RefToken = RefToken[:-1]

  #Return values
  return AccToken, RefToken

def WriteConfig(AccToken,RefToken):
  print "Writing new token to the config file"
  print "Writing this: " + AccToken + " and " + RefToken

  #Delete the old config file
  os.remove(IniFile)

  #Open and write to the file
  FileObj = open(IniFile,'w')
  FileObj.write(AccToken + "\n")
  FileObj.write(RefToken + "\n")
  FileObj.close()

#Make a HTTP POST to get a new
def GetNewAccessToken(RefToken):
  print "Getting a new access token"

  #Form the data payload
  BodyText = {'grant_type' : 'refresh_token',
              'refresh_token' : RefToken}
  #URL Encode it
  BodyURLEncoded = urllib.urlencode(BodyText)
  print "Using this as the body when getting access token >>" + BodyURLEncoded

  #Start the request
  tokenreq = urllib2.Request(TokenURL,BodyURLEncoded)

  #Add the headers, first we base64 encode the client id and client secret with a : inbetween and create the authorisation header
  tokenreq.add_header('Authorization', 'Basic ' + base64.b64encode(OAuthTwoClientID + ":" + ClientOrConsumerSecret))
  tokenreq.add_header('Content-Type', 'application/x-www-form-urlencoded')

  #Fire off the request
  try:
    tokenresponse = urllib2.urlopen(tokenreq)

    #See what we got back.  If it's this part of  the code it was OK
    FullResponse = tokenresponse.read()

    #Need to pick out the access token and write it to the config file.  Use a JSON manipluation module
    ResponseJSON = json.loads(FullResponse)

    #Read the access token as a string
    NewAccessToken = str(ResponseJSON['access_token'])
    NewRefreshToken = str(ResponseJSON['refresh_token'])
    #Write the access token to the ini file
    WriteConfig(NewAccessToken,NewRefreshToken)

    print "New access token output >>> " + FullResponse
  except urllib2.URLError as e:
    #Gettin to this part of the code means we got an error
    print "An error was raised when getting the access token.  Need to stop here"
    print e.code
    print e.read()
    sys.exit()

#This makes an API call.  It also catches errors and tries to deal with them
def MakeAPICall(InURL,AccToken,RefToken):
  #Start the request
  req = urllib2.Request(InURL)

  #Add the access token in the header
  req.add_header('Authorization', 'Bearer ' + AccToken)

  print "I used this access token " + AccToken
  #Fire off the request
  try:
    #Do the request
    response = urllib2.urlopen(req)
    #Read the response
    FullResponse = response.read()

    #Return values
    return True, FullResponse
  #Catch errors, e.g. A 401 error that signifies the need for a new access token
  except urllib2.URLError as e:
    print "Got this HTTP error: " + str(e.code)
    HTTPErrorMessage = e.read()
    print "This was in the HTTP error message: " + HTTPErrorMessage
    #See what the error was
    if (e.code == 401) and (HTTPErrorMessage.find("Access token invalid or expired") > 0):
      GetNewAccessToken(RefToken)
      return False, TokenRefreshedOK
    #Return that this didn't work, allowing the calling function to handle it
    return False, ErrorInAPI

#Main part of the code
#Declare these global variables that we'll use for the access and refresh tokens
AccessToken = ""
RefreshToken = ""

print "Fitbit API Test Code"

#Get the config
AccessToken, RefreshToken = GetConfig()

#Make the API call
APICallOK, APIResponse = MakeAPICall(FitbitURL, AccessToken, RefreshToken)

if APICallOK:
  print APIResponse
else:
  if (APIResponse == TokenRefreshedOK):
    print "Refreshed the access token.  Can go again"
  else:
   print ErrorInAPI