annotate gcalendar/oauth.py @ 861:e4f8d87c3d30

Configure Markdown logger to reduce noise in logs. Markdown is logging at the INFO level whenever it loads an extension. This looks like it has been fixed in master at GitHub. But until then we will explicitly configure the MARKDOWN logger to log at WARNING or higher.
author Brian Neal <bgneal@gmail.com>
date Mon, 01 Dec 2014 18:36:27 -0600
parents 9165edfb1709
children
rev   line source
bgneal@451 1 """
bgneal@451 2 This module handles the OAuth integration with Google.
bgneal@451 3
bgneal@451 4 """
bgneal@855 5 import datetime
bgneal@451 6 import logging
bgneal@855 7 import os
bgneal@451 8
bgneal@855 9 from oauth2client.client import OAuth2WebServerFlow, FlowExchangeError
bgneal@855 10 from oauth2client.file import Storage
bgneal@451 11 from django.conf import settings
bgneal@855 12 from django.core.cache import cache
bgneal@451 13
bgneal@451 14
bgneal@451 15 logger = logging.getLogger(__name__)
bgneal@855 16 FLOW_CACHE_KEY = 'gcalendar-oauth-flow'
bgneal@855 17 FLOW_CACHE_TIMEOUT = 60
bgneal@855 18 SCOPE = 'https://www.googleapis.com/auth/calendar'
bgneal@451 19
bgneal@451 20
bgneal@855 21 class OAuthError(Exception):
bgneal@855 22 """Base exception for errors raised by the oauth module"""
bgneal@855 23
bgneal@855 24
bgneal@855 25 def check_credentials_status():
bgneal@855 26 """Performs a stat() on the credentials file and returns the last modified
bgneal@855 27 time as a datetime object. If an error occurs, None is returned.
bgneal@451 28 """
bgneal@855 29 try:
bgneal@855 30 status = os.stat(settings.GCAL_CREDENTIALS_PATH)
bgneal@855 31 except OSError:
bgneal@855 32 return None
bgneal@857 33 return datetime.datetime.fromtimestamp(status.st_mtime)
bgneal@855 34
bgneal@855 35
bgneal@857 36 def get_auth_url(callback_url):
bgneal@855 37 """
bgneal@855 38 This function creates an OAuth flow object and uses it to generate an
bgneal@855 39 authorization URL which is returned to the caller.
bgneal@451 40
bgneal@451 41 callback_url - a string that is the URL that Google should redirect the user
bgneal@451 42 to after the user has authorized our application access to their data.
bgneal@451 43
bgneal@451 44 """
bgneal@855 45 logger.info("get_auth_url started; callback url='%s'", callback_url)
bgneal@451 46
bgneal@855 47 flow = OAuth2WebServerFlow(client_id=settings.GCAL_CLIENT_ID,
bgneal@855 48 client_secret=settings.GCAL_CLIENT_SECRET,
bgneal@855 49 scope=SCOPE,
bgneal@855 50 redirect_uri=callback_url)
bgneal@451 51
bgneal@855 52 auth_url = flow.step1_get_authorize_url()
bgneal@855 53 logger.info("auth url: '%s'", auth_url)
bgneal@451 54
bgneal@855 55 # Save the flow in the cache so we can use it when Google calls the
bgneal@855 56 # callback. The expiration time also lets us check to make sure someone
bgneal@855 57 # isn't spoofing google if we are called at some random time.
bgneal@855 58 # Note: using the session might seem like a more obvious choice, but flow
bgneal@855 59 # objects are not JSON-serializable, and we don't want to use pickelable
bgneal@855 60 # sessions for security reasons.
bgneal@451 61
bgneal@855 62 cache.set(FLOW_CACHE_KEY, flow, FLOW_CACHE_TIMEOUT)
bgneal@451 63
bgneal@855 64 return auth_url
bgneal@451 65
bgneal@451 66
bgneal@855 67 def auth_return(request):
bgneal@451 68 """
bgneal@857 69 This function should be called after Google has redirected the user
bgneal@855 70 after the user authorized us. We retrieve the authorization code from the
bgneal@855 71 request query parameters and then exchange it for access tokens. These
bgneal@855 72 tokens are stored in our credentials file.
bgneal@451 73
bgneal@855 74 If an error is encountered, an OAuthError is raised.
bgneal@451 75 """
bgneal@855 76 logger.info("auth_return called as '%s'", request.get_full_path())
bgneal@451 77
bgneal@855 78 # Try to retrieve the flow object from the cache
bgneal@855 79 flow = cache.get(FLOW_CACHE_KEY)
bgneal@855 80 if not flow:
bgneal@855 81 logger.warning("auth_return no flow in cache, bailing out")
bgneal@855 82 raise OAuthError("No flow found. Perhaps we timed out?")
bgneal@451 83
bgneal@855 84 # Delete flow out of cache to close our operational window
bgneal@855 85 cache.delete(FLOW_CACHE_KEY)
bgneal@451 86
bgneal@855 87 # Process the request
bgneal@855 88 error = request.GET.get('error')
bgneal@855 89 if error:
bgneal@855 90 logging.error("auth_return received error: %s", error)
bgneal@855 91 raise OAuthError("Error authorizing request: %s" % error)
bgneal@451 92
bgneal@855 93 code = request.GET.get('code')
bgneal@855 94 if not code:
bgneal@855 95 logging.error("auth_return missing code")
bgneal@855 96 raise OAuthError("No authorization code received")
bgneal@451 97
bgneal@855 98 logging.info("auth_return exchanging code for credentials")
bgneal@855 99 try:
bgneal@855 100 credentials = flow.step2_exchange(code)
bgneal@855 101 except FlowExchangeError as ex:
bgneal@855 102 logging.error("auth_return exchange failed: %s", ex)
bgneal@855 103 raise OAuthError(str(ex))
bgneal@855 104
bgneal@855 105 logging.info("auth_return storing credentials")
bgneal@855 106 storage = Storage(settings.GCAL_CREDENTIALS_PATH)
bgneal@855 107 storage.put(credentials)
bgneal@855 108 logging.info("auth_return completed successfully")
bgneal@458 109
bgneal@458 110
bgneal@857 111 def get_credentials():
bgneal@857 112 """Obtain the stored credentials if available, or None if they are not."""
bgneal@857 113 storage = Storage(settings.GCAL_CREDENTIALS_PATH)
bgneal@857 114 return storage.get()