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 return datetime.datetime.fromtimestamp(status.st_mtime)
|
bgneal@855
|
32 except OSError:
|
bgneal@855
|
33 return None
|
bgneal@855
|
34
|
bgneal@855
|
35
|
bgneal@855
|
36 def get_auth_url(request, 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 request - the HttpRequest object for the user requesting the token. The
|
bgneal@451
|
42 token is stored in the session object attached to this request.
|
bgneal@451
|
43
|
bgneal@451
|
44 callback_url - a string that is the URL that Google should redirect the user
|
bgneal@451
|
45 to after the user has authorized our application access to their data.
|
bgneal@451
|
46
|
bgneal@451
|
47 """
|
bgneal@855
|
48 logger.info("get_auth_url started; callback url='%s'", callback_url)
|
bgneal@451
|
49
|
bgneal@855
|
50 flow = OAuth2WebServerFlow(client_id=settings.GCAL_CLIENT_ID,
|
bgneal@855
|
51 client_secret=settings.GCAL_CLIENT_SECRET,
|
bgneal@855
|
52 scope=SCOPE,
|
bgneal@855
|
53 redirect_uri=callback_url)
|
bgneal@451
|
54
|
bgneal@855
|
55 auth_url = flow.step1_get_authorize_url()
|
bgneal@855
|
56 logger.info("auth url: '%s'", auth_url)
|
bgneal@451
|
57
|
bgneal@855
|
58 # Save the flow in the cache so we can use it when Google calls the
|
bgneal@855
|
59 # callback. The expiration time also lets us check to make sure someone
|
bgneal@855
|
60 # isn't spoofing google if we are called at some random time.
|
bgneal@855
|
61 # Note: using the session might seem like a more obvious choice, but flow
|
bgneal@855
|
62 # objects are not JSON-serializable, and we don't want to use pickelable
|
bgneal@855
|
63 # sessions for security reasons.
|
bgneal@451
|
64
|
bgneal@855
|
65 cache.set(FLOW_CACHE_KEY, flow, FLOW_CACHE_TIMEOUT)
|
bgneal@451
|
66
|
bgneal@855
|
67 return auth_url
|
bgneal@451
|
68
|
bgneal@451
|
69
|
bgneal@855
|
70 def auth_return(request):
|
bgneal@451
|
71 """
|
bgneal@451
|
72 This function should be called after Google has sent the user back to us
|
bgneal@855
|
73 after the user authorized us. We retrieve the authorization code from the
|
bgneal@855
|
74 request query parameters and then exchange it for access tokens. These
|
bgneal@855
|
75 tokens are stored in our credentials file.
|
bgneal@451
|
76
|
bgneal@855
|
77 If an error is encountered, an OAuthError is raised.
|
bgneal@451
|
78 """
|
bgneal@855
|
79 logger.info("auth_return called as '%s'", request.get_full_path())
|
bgneal@451
|
80
|
bgneal@855
|
81 # Try to retrieve the flow object from the cache
|
bgneal@855
|
82 flow = cache.get(FLOW_CACHE_KEY)
|
bgneal@855
|
83 if not flow:
|
bgneal@855
|
84 logger.warning("auth_return no flow in cache, bailing out")
|
bgneal@855
|
85 raise OAuthError("No flow found. Perhaps we timed out?")
|
bgneal@451
|
86
|
bgneal@855
|
87 # Delete flow out of cache to close our operational window
|
bgneal@855
|
88 cache.delete(FLOW_CACHE_KEY)
|
bgneal@451
|
89
|
bgneal@855
|
90 # Process the request
|
bgneal@855
|
91 error = request.GET.get('error')
|
bgneal@855
|
92 if error:
|
bgneal@855
|
93 logging.error("auth_return received error: %s", error)
|
bgneal@855
|
94 raise OAuthError("Error authorizing request: %s" % error)
|
bgneal@451
|
95
|
bgneal@855
|
96 code = request.GET.get('code')
|
bgneal@855
|
97 if not code:
|
bgneal@855
|
98 logging.error("auth_return missing code")
|
bgneal@855
|
99 raise OAuthError("No authorization code received")
|
bgneal@451
|
100
|
bgneal@855
|
101 logging.info("auth_return exchanging code for credentials")
|
bgneal@855
|
102 try:
|
bgneal@855
|
103 credentials = flow.step2_exchange(code)
|
bgneal@855
|
104 except FlowExchangeError as ex:
|
bgneal@855
|
105 logging.error("auth_return exchange failed: %s", ex)
|
bgneal@855
|
106 raise OAuthError(str(ex))
|
bgneal@855
|
107
|
bgneal@855
|
108 logging.info("auth_return storing credentials")
|
bgneal@855
|
109 storage = Storage(settings.GCAL_CREDENTIALS_PATH)
|
bgneal@855
|
110 storage.put(credentials)
|
bgneal@855
|
111 logging.info("auth_return completed successfully")
|
bgneal@458
|
112
|
bgneal@458
|
113
|
bgneal@458
|
114 def serialize_token(token):
|
bgneal@458
|
115 """
|
bgneal@458
|
116 This function turns a token into a string and returns it.
|
bgneal@458
|
117
|
bgneal@458
|
118 """
|
bgneal@458
|
119 return gdata.gauth.TokenToBlob(token)
|
bgneal@458
|
120
|
bgneal@458
|
121
|
bgneal@458
|
122 def deserialize_token(s):
|
bgneal@458
|
123 """
|
bgneal@458
|
124 This function turns a string into a token returns it. The string must have
|
bgneal@458
|
125 previously been created with serialize_token().
|
bgneal@458
|
126
|
bgneal@458
|
127 """
|
bgneal@458
|
128 return gdata.gauth.TokenFromBlob(s)
|