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()