bgneal@451: """
bgneal@451: This module handles the OAuth integration with Google.
bgneal@451: 
bgneal@451: """
bgneal@855: import datetime
bgneal@451: import logging
bgneal@855: import os
bgneal@451: 
bgneal@855: from oauth2client.client import OAuth2WebServerFlow, FlowExchangeError
bgneal@855: from oauth2client.file import Storage
bgneal@451: from django.conf import settings
bgneal@855: from django.core.cache import cache
bgneal@451: 
bgneal@451: 
bgneal@451: logger = logging.getLogger(__name__)
bgneal@855: FLOW_CACHE_KEY = 'gcalendar-oauth-flow'
bgneal@855: FLOW_CACHE_TIMEOUT = 60
bgneal@855: SCOPE = 'https://www.googleapis.com/auth/calendar'
bgneal@451: 
bgneal@451: 
bgneal@855: class OAuthError(Exception):
bgneal@855:     """Base exception for errors raised by the oauth module"""
bgneal@855: 
bgneal@855: 
bgneal@855: def check_credentials_status():
bgneal@855:     """Performs a stat() on the credentials file and returns the last modified
bgneal@855:     time as a datetime object. If an error occurs, None is returned.
bgneal@451:     """
bgneal@855:     try:
bgneal@855:         status = os.stat(settings.GCAL_CREDENTIALS_PATH)
bgneal@855:     except OSError:
bgneal@855:         return None
bgneal@857:     return datetime.datetime.fromtimestamp(status.st_mtime)
bgneal@855: 
bgneal@855: 
bgneal@857: def get_auth_url(callback_url):
bgneal@855:     """
bgneal@855:     This function creates an OAuth flow object and uses it to generate an
bgneal@855:     authorization URL which is returned to the caller.
bgneal@451: 
bgneal@451:     callback_url - a string that is the URL that Google should redirect the user
bgneal@451:     to after the user has authorized our application access to their data.
bgneal@451: 
bgneal@451:     """
bgneal@855:     logger.info("get_auth_url started; callback url='%s'", callback_url)
bgneal@451: 
bgneal@855:     flow = OAuth2WebServerFlow(client_id=settings.GCAL_CLIENT_ID,
bgneal@855:                                client_secret=settings.GCAL_CLIENT_SECRET,
bgneal@855:                                scope=SCOPE,
bgneal@855:                                redirect_uri=callback_url)
bgneal@451: 
bgneal@855:     auth_url = flow.step1_get_authorize_url()
bgneal@855:     logger.info("auth url: '%s'", auth_url)
bgneal@451: 
bgneal@855:     # Save the flow in the cache so we can use it when Google calls the
bgneal@855:     # callback. The expiration time also lets us check to make sure someone
bgneal@855:     # isn't spoofing google if we are called at some random time.
bgneal@855:     # Note: using the session might seem like a more obvious choice, but flow
bgneal@855:     # objects are not JSON-serializable, and we don't want to use pickelable
bgneal@855:     # sessions for security reasons.
bgneal@451: 
bgneal@855:     cache.set(FLOW_CACHE_KEY, flow, FLOW_CACHE_TIMEOUT)
bgneal@451: 
bgneal@855:     return auth_url
bgneal@451: 
bgneal@451: 
bgneal@855: def auth_return(request):
bgneal@451:     """
bgneal@857:     This function should be called after Google has redirected the user
bgneal@855:     after the user authorized us. We retrieve the authorization code from the
bgneal@855:     request query parameters and then exchange it for access tokens. These
bgneal@855:     tokens are stored in our credentials file.
bgneal@451: 
bgneal@855:     If an error is encountered, an OAuthError is raised.
bgneal@451:     """
bgneal@855:     logger.info("auth_return called as '%s'", request.get_full_path())
bgneal@451: 
bgneal@855:     # Try to retrieve the flow object from the cache
bgneal@855:     flow = cache.get(FLOW_CACHE_KEY)
bgneal@855:     if not flow:
bgneal@855:         logger.warning("auth_return no flow in cache, bailing out")
bgneal@855:         raise OAuthError("No flow found. Perhaps we timed out?")
bgneal@451: 
bgneal@855:     # Delete flow out of cache to close our operational window
bgneal@855:     cache.delete(FLOW_CACHE_KEY)
bgneal@451: 
bgneal@855:     # Process the request
bgneal@855:     error = request.GET.get('error')
bgneal@855:     if error:
bgneal@855:         logging.error("auth_return received error: %s", error)
bgneal@855:         raise OAuthError("Error authorizing request: %s" % error)
bgneal@451: 
bgneal@855:     code = request.GET.get('code')
bgneal@855:     if not code:
bgneal@855:         logging.error("auth_return missing code")
bgneal@855:         raise OAuthError("No authorization code received")
bgneal@451: 
bgneal@855:     logging.info("auth_return exchanging code for credentials")
bgneal@855:     try:
bgneal@855:         credentials = flow.step2_exchange(code)
bgneal@855:     except FlowExchangeError as ex:
bgneal@855:         logging.error("auth_return exchange failed: %s", ex)
bgneal@855:         raise OAuthError(str(ex))
bgneal@855: 
bgneal@855:     logging.info("auth_return storing credentials")
bgneal@855:     storage = Storage(settings.GCAL_CREDENTIALS_PATH)
bgneal@855:     storage.put(credentials)
bgneal@855:     logging.info("auth_return completed successfully")
bgneal@458: 
bgneal@458: 
bgneal@857: def get_credentials():
bgneal@857:     """Obtain the stored credentials if available, or None if they are not."""
bgneal@857:     storage = Storage(settings.GCAL_CREDENTIALS_PATH)
bgneal@857:     return storage.get()