From 877aee0bb54923fe66bae608b3562213a9383296 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Thu, 19 May 2016 07:34:48 +0200 Subject: [PATCH 1/5] git-ignore vim swap files. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index edb4c63..c096ade 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ docs/_build/ target/ google.json fitbit.yaml + +# vim swap files +.*.swp From c3e5dd4d1967be71d8a9d7f02c1f4cef9a73edae Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Thu, 19 May 2016 09:06:41 +0200 Subject: [PATCH 2/5] Switch to OAuth2 for Fitbit. --- auth_fitbit.py | 141 +++++++++++++++++++++++++++++------------------ requirements.txt | 5 +- 2 files changed, 89 insertions(+), 57 deletions(-) diff --git a/auth_fitbit.py b/auth_fitbit.py index a1a591e..ad01d7a 100755 --- a/auth_fitbit.py +++ b/auth_fitbit.py @@ -1,78 +1,109 @@ #!/usr/bin/env python """ -This was taken, and modified from python-fitbit/gather_keys_cli.py, +This was taken, and modified from python-fitbit/gather_keys_oauth2.py, License reproduced below. -------------------------- -The MIT License +Copyright 2012-2015 ORCAS -Copyright (c) 2007 Leah Culver + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Example consumer. This is not recommended for production. -Instead, you'll want to create your own subclass of OAuthClient -or find one that works with your web framework. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. """ import os import sys +import threading +import traceback import webbrowser import yaml -from fitbit.api import FitbitOauthClient +from base64 import b64encode +import cherrypy +from fitbit.api import FitbitOauth2Client +from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError +from requests_oauthlib import OAuth2Session -def gather_keys(): - # setup - client = FitbitOauthClient(CLIENT_KEY, CLIENT_SECRET) +class OAuth2Server: + def __init__(self, client_id, client_secret, + redirect_uri='http://localhost:8080/'): + """ Initialize the FitbitOauth2Client """ + self.redirect_uri = redirect_uri + self.success_html = """ +

You are now authorized to access the Fitbit API!

+

You can close this window

""" + self.failure_html = """ +

ERROR: %s


You can close this window

%s""" + self.oauth = FitbitOauth2Client(client_id, client_secret) - # get request token - token = client.fetch_request_token() + def browser_authorize(self): + """ + Open a browser to the authorization url and spool up a CherryPy + server to accept the response + """ + url, _ = self.oauth.authorize_token_url(redirect_uri=self.redirect_uri) + # Open the web browser in a new thread for command-line browser support + threading.Timer(1, webbrowser.open, args=(url,)).start() + cherrypy.quickstart(self) - stderr = os.dup(2) - os.close(2) - os.open(os.devnull, os.O_RDWR) - webbrowser.open(client.authorize_token_url()) - os.dup2(stderr, 2) - try: - verifier = raw_input('Verifier: ') - except NameError: - # Python 3.x - verifier = input('Verifier: ') + @cherrypy.expose + def index(self, state, code=None, error=None): + """ + Receive a Fitbit response containing a verification code. Use the code + to fetch the access_token. + """ + error = None + if code: + try: + self.oauth.fetch_access_token(code, self.redirect_uri) + except MissingTokenError: + error = self._fmt_failure( + 'Missing access token parameter.
Please check that ' + 'you are using the correct client_secret') + except MismatchingStateError: + error = self._fmt_failure('CSRF Warning! Mismatching state') + else: + error = self._fmt_failure('Unknown error while authenticating') + # Use a thread to shutdown cherrypy so we can return HTML first + self._shutdown_cherrypy() + return error if error else self.success_html - # get access token - token = client.fetch_access_token(verifier) - return token + def _fmt_failure(self, message): + tb = traceback.format_tb(sys.exc_info()[2]) + tb_html = '
%s
' % ('\n'.join(tb)) if tb else '' + return self.failure_html % (message, tb_html) + def _shutdown_cherrypy(self): + """ Shutdown cherrypy in one second, if it's running """ + if cherrypy.engine.state == cherrypy.engine.states.STARTED: + threading.Timer(1, cherrypy.engine.exit).start() + + +def main(): + if not (len(sys.argv) == 3): + print("Arguments 'client ID', 'client secret' are required") + sys.exit(1) + client_id = sys.argv[1] + client_secret = sys.argv[2] + + server = OAuth2Server(client_id, client_secret) + server.browser_authorize() + + credentials = dict( + client_id=client_id, + client_secret=client_secret, + access_token=server.oauth.token['access_token'], + refresh_token=server.oauth.token['refresh_token']) + yaml.dump(credentials, open('fitbit.yaml', 'w')) if __name__ == '__main__': - if not (len(sys.argv) == 3): - print("Arguments 'client key', 'client secret' are required") - sys.exit(1) - CLIENT_KEY = sys.argv[1] - CLIENT_SECRET = sys.argv[2] - - keys = gather_keys() - credentials = dict( - client_key=CLIENT_KEY, - client_secret=CLIENT_SECRET, - resource_owner_key=keys['oauth_token'].encode('ascii'), - resource_owner_secret=keys['oauth_token_secret'].encode('ascii')) - yaml.dump(credentials, open('fitbit.yaml', 'w')) + main() diff --git a/requirements.txt b/requirements.txt index 5f5c972..60d43c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ PyYAML==3.11 -fitbit==0.1.2 +fitbit==0.2.2 google-api-python-client==1.3.1 httplib2==0.9 oauth2client==1.4.3 @@ -8,9 +8,10 @@ pyasn1==0.1.7 pyasn1-modules==0.0.5 python-dateutil==2.3 requests==2.5.0 -requests-oauthlib==0.4.2 +requests-oauthlib==0.6.1 rsa==3.1.4 simplejson==3.6.5 six==1.8.0 uritemplate==0.6 wsgiref==0.1.2 +cherrypy==5.4.0 From 24ce7bdf68433993b30f2a0ceba9d31494c4ce9f Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Thu, 19 May 2016 09:11:00 +0200 Subject: [PATCH 3/5] Update documentation for OAuth2. --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a39f655..abac786 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,22 @@ Use `pip` to install the required packages, ### Fitbit Credentials -[Register a Fitbit application](https://dev.fitbit.com/apps/new). Note the client key and secret. +[Register a Fitbit application](https://dev.fitbit.com/apps/new) with: + * OAuth 2.0 Application Type: `Personal` + * Callback URL: `http://localhost:8080/` + * Default Access Type: `Read-Only` + * any values you want for application name, website, organization, and organization website +Note the resulting `OAuth 2.0 Client ID` and `Client Secret`. Run `auth_fitbit.py` to get credentials for read access to a user's data, - $ python auth_fitbit.py [FITBIT CLIENT KEY] [FITBIT CLIENT SECRET] + $ python auth_fitbit.py [FITBIT CLIENT ID] [FITBIT CLIENT SECRET] -This scripts open a browser window where you can log in to your Fitbit account and authorize the app to "access your profile and data". When you accept, the site will give you a string to copy and paste back into the script, which then writes the credentials to a local file named `fitbit.yaml`. +This scripts open a browser window where you can log in to your Fitbit account and authorize the app to "access your profile and data". When you accept, the script writes the credentials to a local file named `fitbit.yaml`. ### Google Credentials -[Create a project in Google Developers Console and enable the fitness API](https://console.developers.google.com/flows/enableapi?apiid=fitness). Create OAuth client ID with Other/Desktop type. Note the client id and client secret. +[Create a project in Google Developers Console and enable the fitness API](https://console.developers.google.com/flows/enableapi?apiid=fitness). Create an OAuth client ID with `Other UI` type and select `User data`. Note the client id and client secret. Run `auth_google.py` to get credentials for write access to a user's body data, From 4777f02f4e53bff8474039a4b990ad51c4fcffa1 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Thu, 19 May 2016 09:25:19 +0200 Subject: [PATCH 4/5] Update required packages. --- requirements.txt | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/requirements.txt b/requirements.txt index 60d43c1..5a4e8d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,8 @@ -PyYAML==3.11 -fitbit==0.2.2 -google-api-python-client==1.3.1 -httplib2==0.9 -oauth2client==1.4.3 -oauthlib==0.7.2 -pyasn1==0.1.7 -pyasn1-modules==0.0.5 -python-dateutil==2.3 -requests==2.5.0 -requests-oauthlib==0.6.1 -rsa==3.1.4 -simplejson==3.6.5 -six==1.8.0 -uritemplate==0.6 -wsgiref==0.1.2 -cherrypy==5.4.0 +oauthlib == 1.0.3 +google_api_python_client == 1.5.0 +cherrypy == 5.4.0 +python_dateutil == 2.5.3 +fitbit == 0.2.2 +httplib2 == 0.9.2 +requests_oauthlib == 0.6.1 +PyYAML == 3.11 From 77c5ce0c4ec6a27cdb61883f6c0012dc8a720bad Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Thu, 19 May 2016 15:19:55 +0200 Subject: [PATCH 5/5] Indentation fixes (how did those tabs get in there?) --- auth_fitbit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth_fitbit.py b/auth_fitbit.py index ad01d7a..ac12dbb 100755 --- a/auth_fitbit.py +++ b/auth_fitbit.py @@ -101,9 +101,9 @@ def main(): credentials = dict( client_id=client_id, client_secret=client_secret, - access_token=server.oauth.token['access_token'], - refresh_token=server.oauth.token['refresh_token']) + access_token=server.oauth.token['access_token'], + refresh_token=server.oauth.token['refresh_token']) yaml.dump(credentials, open('fitbit.yaml', 'w')) if __name__ == '__main__': - main() + main()