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