annotate gcalendar/oauth.py @ 1201:fe10aea76cbd tip

Add 2023 MP3 compilation links
author Brian Neal <bgneal@gmail.com>
date Sun, 24 Mar 2024 14:50:23 -0500
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()