diff gcalendar/oauth.py @ 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 ee87ea74d46b
children 9165edfb1709
line wrap: on
line diff
--- 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):