changeset 855:8743c566f712

WIP commit for converting to Google Calendar v3 API. This code should be enough to receive tokens from Google. See issue #80.
author Brian Neal <bgneal@gmail.com>
date Tue, 18 Nov 2014 21:09:24 -0600
parents 1583a41511cc
children c2dfd1b1323e
files gcalendar/admin.py gcalendar/oauth.py sg101/settings/base.py sg101/templates/gcalendar/google_sync.html
diffstat 4 files changed, 106 insertions(+), 90 deletions(-) [+]
line wrap: on
line diff
--- a/gcalendar/admin.py	Tue Nov 11 18:27:05 2014 -0600
+++ b/gcalendar/admin.py	Tue Nov 18 21:09:24 2014 -0600
@@ -11,8 +11,6 @@
 from django.http import HttpResponseRedirect
 from django.shortcuts import render
 
-import gdata.client
-
 from gcalendar.models import Event, AccessToken
 from gcalendar.calendar import Calendar, CalendarError
 from gcalendar import oauth
@@ -20,9 +18,6 @@
 import bio.badges
 
 
-SCOPES = ['https://www.google.com/calendar/feeds/']
-
-
 class EventAdmin(admin.ModelAdmin):
     list_display = ('what', 'user', 'start_date', 'where', 'date_submitted',
             'status', 'is_approved', 'google_html')
@@ -46,12 +41,12 @@
             url(r'^google_sync/$',
                 self.admin_site.admin_view(self.google_sync),
                 name="gcalendar-google_sync"),
-            url(r'^fetch_auth/$',
-                self.admin_site.admin_view(self.fetch_auth),
-                name="gcalendar-fetch_auth"),
-             url(r'^get_access_token/$',
-                self.admin_site.admin_view(self.get_access_token),
-                name="gcalendar-get_access_token"),
+            url(r'^authorize/$',
+                self.admin_site.admin_view(self.authorize),
+                name="gcalendar-authorize"),
+             url(r'^auth_return/$',
+                self.admin_site.admin_view(self.auth_return),
+                name="gcalendar-auth_return"),
         )
         return my_urls + urls
 
@@ -84,10 +79,10 @@
         # Get pending events
         events = Event.pending_events.all()
 
-        # Attempt to get saved access token to the Google calendar
-        access_token = AccessToken.objects.get_token().access_token()
+        # Check status of credentials file
+        cred_status = oauth.check_credentials_status()
 
-        messages = []
+        msgs = []
         err_msg = ''
         if request.method == 'POST':
             if access_token:
@@ -100,50 +95,37 @@
                     err_msg = e.msg
                     events = Event.pending_events.all()
                 else:
-                    messages.append('All events processed successfully.')
+                    msgs.append('All events processed successfully.')
                     events = Event.objects.none()
 
         return render(request, 'gcalendar/google_sync.html', {
             'current_app': self.admin_site.name,
-            'access_token': access_token,
-            'messages': messages,
+            'cred_status': cred_status,
+            'messages': msgs,
             'err_msg': err_msg,
             'events': events,
             })
 
-    def fetch_auth(self, request):
+    def authorize(self, request):
         """
-        This view fetches a request token and then redirects the user to
-        authorize it.
-
+        This view generates the authorization URL and redirects the user to it.
         """
         site = Site.objects.get_current()
         callback_url = 'http://%s%s' % (site.domain,
-                reverse('admin:gcalendar-get_access_token'))
-        try:
-            auth_url = oauth.fetch_auth(request, SCOPES, callback_url)
-        except gdata.client.Error, e:
-            messages.error(request, str(e))
-            return HttpResponseRedirect(reverse('admin:gcalendar-google_sync'))
-        else:
-            return HttpResponseRedirect(auth_url)
+                reverse('admin:gcalendar-auth_return'))
+        auth_url = oauth.get_auth_url(request, callback_url)
+        return HttpResponseRedirect(auth_url)
 
-    def get_access_token(self, request):
+    def auth_return(self, request):
         """
         This view is called by Google after the user has authorized us access to
-        their data. We call into the oauth module to upgrade the oauth token to
-        an access token. We then save the access token in the database and
-        redirect back to our admin Google sync view.
-
+        their data. We call into the oauth module to process the authorization
+        code and exchange it for tokens.
         """
         try:
-            access_token = oauth.get_access_token(request)
-        except gdata.client.Error, e:
-            messages.error(request, str(e))
-        else:
-            token = AccessToken.objects.get_token()
-            token.update(access_token)
-            token.save()
+            oauth.auth_return(request)
+        except oauth.OAuthError as e:
+            self.message_user(request, str(e), level=messages.ERROR)
 
         return HttpResponseRedirect(reverse('admin:gcalendar-google_sync'))
 
--- a/gcalendar/oauth.py	Tue Nov 11 18:27:05 2014 -0600
+++ b/gcalendar/oauth.py	Tue Nov 18 21:09:24 2014 -0600
@@ -2,84 +2,113 @@
 This module handles the OAuth integration with Google.
 
 """
-from __future__ import with_statement
+import datetime
 import logging
+import os
 
-import gdata.gauth
-from gdata.calendar_resource.client import CalendarResourceClient
-
+from oauth2client.client import OAuth2WebServerFlow, FlowExchangeError
+from oauth2client.file import Storage
 from django.conf import settings
+from django.core.cache import cache
 
 
 logger = logging.getLogger(__name__)
-USER_AGENT = 'surfguitar101-gcalendar-v1'
-REQ_TOKEN_SESSION_KEY = 'gcalendar oauth request token'
+FLOW_CACHE_KEY = 'gcalendar-oauth-flow'
+FLOW_CACHE_TIMEOUT = 60
+SCOPE = 'https://www.googleapis.com/auth/calendar'
 
 
-def fetch_auth(request, scopes, callback_url):
+class OAuthError(Exception):
+    """Base exception for errors raised by the oauth module"""
+
+
+def check_credentials_status():
+    """Performs a stat() on the credentials file and returns the last modified
+    time as a datetime object. If an error occurs, None is returned.
     """
-    This function fetches a request token from Google and stores it in the
-    session. It then returns the authorization URL as a string.
+    try:
+        status = os.stat(settings.GCAL_CREDENTIALS_PATH)
+        return datetime.datetime.fromtimestamp(status.st_mtime)
+    except OSError:
+        return None
+
+
+def get_auth_url(request, callback_url):
+    """
+    This function creates an OAuth flow object and uses it to generate an
+    authorization URL which is returned to the caller.
 
     request - the HttpRequest object for the user requesting the token. The
     token is stored in the session object attached to this request.
 
-    scopes - a list of scope strings that the request token is for. See
-    http://code.google.com/apis/gdata/faq.html#AuthScopes
-
     callback_url - a string that is the URL that Google should redirect the user
     to after the user has authorized our application access to their data.
 
-    This function only supports RSA-SHA1 authentication. Settings in the Django
-    settings module determine the consumer key and path to the RSA private key.
     """
-    logger.info("fetch_auth started; callback url='%s'", callback_url)
-    client = CalendarResourceClient(None, source=USER_AGENT)
+    logger.info("get_auth_url started; callback url='%s'", callback_url)
 
-    with open(settings.GOOGLE_OAUTH_PRIVATE_KEY_PATH, 'r') as f:
-        rsa_key = f.read()
-    logger.info("read RSA key; now getting request token")
+    flow = OAuth2WebServerFlow(client_id=settings.GCAL_CLIENT_ID,
+                               client_secret=settings.GCAL_CLIENT_SECRET,
+                               scope=SCOPE,
+                               redirect_uri=callback_url)
 
-    request_token = client.GetOAuthToken(
-            scopes,
-            callback_url,
-            settings.GOOGLE_OAUTH_CONSUMER_KEY,
-            rsa_private_key=rsa_key)
+    auth_url = flow.step1_get_authorize_url()
+    logger.info("auth url: '%s'", auth_url)
 
-    logger.info("received token")
-    request.session[REQ_TOKEN_SESSION_KEY] = request_token
+    # Save the flow in the cache so we can use it when Google calls the
+    # callback. The expiration time also lets us check to make sure someone
+    # isn't spoofing google if we are called at some random time.
+    # Note: using the session might seem like a more obvious choice, but flow
+    # objects are not JSON-serializable, and we don't want to use pickelable
+    # sessions for security reasons.
 
-    auth_url = request_token.generate_authorization_url()
-    logger.info("generated auth url '%s'", str(auth_url))
+    cache.set(FLOW_CACHE_KEY, flow, FLOW_CACHE_TIMEOUT)
 
-    return str(auth_url)
+    return auth_url
 
 
-def get_access_token(request):
+def auth_return(request):
     """
     This function should be called after Google has sent the user back to us
-    after the user authorized us. We retrieve the oauth token from the request
-    URL and then upgrade it to an access token. We then return the access token.
+    after the user authorized us. We retrieve the authorization code from the
+    request query parameters and then exchange it for access tokens. These
+    tokens are stored in our credentials file.
 
+    If an error is encountered, an OAuthError is raised.
     """
-    logger.info("get_access_token called as '%s'", request.get_full_path())
+    logger.info("auth_return called as '%s'", request.get_full_path())
 
-    saved_token = request.session.get(REQ_TOKEN_SESSION_KEY)
-    if saved_token is None:
-        logger.error("saved request token not found in session!")
-        return None
+    # Try to retrieve the flow object from the cache
+    flow = cache.get(FLOW_CACHE_KEY)
+    if not flow:
+        logger.warning("auth_return no flow in cache, bailing out")
+        raise OAuthError("No flow found. Perhaps we timed out?")
 
-    logger.info("extracting token...")
-    request_token = gdata.gauth.AuthorizeRequestToken(saved_token,
-                        request.build_absolute_uri())
+    # Delete flow out of cache to close our operational window
+    cache.delete(FLOW_CACHE_KEY)
 
-    logger.info("upgrading to access token...")
+    # Process the request
+    error = request.GET.get('error')
+    if error:
+        logging.error("auth_return received error: %s", error)
+        raise OAuthError("Error authorizing request: %s" % error)
 
-    client = CalendarResourceClient(None, source=USER_AGENT)
-    access_token = client.GetAccessToken(request_token)
+    code = request.GET.get('code')
+    if not code:
+        logging.error("auth_return missing code")
+        raise OAuthError("No authorization code received")
 
-    logger.info("upgraded to access token...")
-    return access_token
+    logging.info("auth_return exchanging code for credentials")
+    try:
+        credentials = flow.step2_exchange(code)
+    except FlowExchangeError as ex:
+        logging.error("auth_return exchange failed: %s", ex)
+        raise OAuthError(str(ex))
+
+    logging.info("auth_return storing credentials")
+    storage = Storage(settings.GCAL_CREDENTIALS_PATH)
+    storage.put(credentials)
+    logging.info("auth_return completed successfully")
 
 
 def serialize_token(token):
--- a/sg101/settings/base.py	Tue Nov 11 18:27:05 2014 -0600
+++ b/sg101/settings/base.py	Tue Nov 18 21:09:24 2014 -0600
@@ -272,9 +272,10 @@
 # GCalendar settings
 GCAL_CALENDAR_ID = 'i81lu3fkh57sgqqenogefd9v78@group.calendar.google.com'
 
-# Google OAuth settings
-GOOGLE_OAUTH_CONSUMER_KEY = 'surfguitar101.com'
-GOOGLE_OAUTH_PRIVATE_KEY_PATH = SECRETS['GOOGLE_KEY_PATH']
+# GCalendar/Google OAuth settings
+GCAL_CLIENT_ID = SECRETS['GCAL_CLIENT_ID']
+GCAL_CLIENT_SECRET = SECRETS['GCAL_CLIENT_SECRET']
+GCAL_CREDENTIALS_PATH = SECRETS['GCAL_CREDENTIALS_PATH']
 
 # MoinMoin Wiki integration settings
 WIKI_COOKIE_NAME = 'sg101_wiki'
--- a/sg101/templates/gcalendar/google_sync.html	Tue Nov 11 18:27:05 2014 -0600
+++ b/sg101/templates/gcalendar/google_sync.html	Tue Nov 18 21:09:24 2014 -0600
@@ -16,7 +16,11 @@
 </ul>
 {% endif %}
 
-<p>Access token status: {% bool_icon access_token %} &mdash; <a href="{% url 'admin:gcalendar-fetch_auth' %}">Request new access token</a></p>
+<p>Credentials file status: {% bool_icon cred_status %}
+{% if cred_status %}
+&mdash; <em>Last modified: {{ cred_status|date:"r" }}</em>
+{% endif %}
+&mdash; <a href="{% url 'admin:gcalendar-authorize' %}">Request new credentials</a></p>
 
 {% if events %}
 <p>The following pending events have been approved and are ready to be synchronized with the Google calendar.</p>