comparison gcalendar/oauth.py @ 855:8743c566f712

WIP commit for converting to Google Calendar v3 API. This code should be enough to receive tokens from Google. See issue #80.
author Brian Neal <bgneal@gmail.com>
date Tue, 18 Nov 2014 21:09:24 -0600
parents ee87ea74d46b
children 9165edfb1709
comparison
equal deleted inserted replaced
854:1583a41511cc 855:8743c566f712
1 """ 1 """
2 This module handles the OAuth integration with Google. 2 This module handles the OAuth integration with Google.
3 3
4 """ 4 """
5 from __future__ import with_statement 5 import datetime
6 import logging 6 import logging
7 import os
7 8
8 import gdata.gauth 9 from oauth2client.client import OAuth2WebServerFlow, FlowExchangeError
9 from gdata.calendar_resource.client import CalendarResourceClient 10 from oauth2client.file import Storage
10
11 from django.conf import settings 11 from django.conf import settings
12 from django.core.cache import cache
12 13
13 14
14 logger = logging.getLogger(__name__) 15 logger = logging.getLogger(__name__)
15 USER_AGENT = 'surfguitar101-gcalendar-v1' 16 FLOW_CACHE_KEY = 'gcalendar-oauth-flow'
16 REQ_TOKEN_SESSION_KEY = 'gcalendar oauth request token' 17 FLOW_CACHE_TIMEOUT = 60
18 SCOPE = 'https://www.googleapis.com/auth/calendar'
17 19
18 20
19 def fetch_auth(request, scopes, callback_url): 21 class OAuthError(Exception):
22 """Base exception for errors raised by the oauth module"""
23
24
25 def check_credentials_status():
26 """Performs a stat() on the credentials file and returns the last modified
27 time as a datetime object. If an error occurs, None is returned.
20 """ 28 """
21 This function fetches a request token from Google and stores it in the 29 try:
22 session. It then returns the authorization URL as a string. 30 status = os.stat(settings.GCAL_CREDENTIALS_PATH)
31 return datetime.datetime.fromtimestamp(status.st_mtime)
32 except OSError:
33 return None
34
35
36 def get_auth_url(request, callback_url):
37 """
38 This function creates an OAuth flow object and uses it to generate an
39 authorization URL which is returned to the caller.
23 40
24 request - the HttpRequest object for the user requesting the token. The 41 request - the HttpRequest object for the user requesting the token. The
25 token is stored in the session object attached to this request. 42 token is stored in the session object attached to this request.
26 43
27 scopes - a list of scope strings that the request token is for. See
28 http://code.google.com/apis/gdata/faq.html#AuthScopes
29
30 callback_url - a string that is the URL that Google should redirect the user 44 callback_url - a string that is the URL that Google should redirect the user
31 to after the user has authorized our application access to their data. 45 to after the user has authorized our application access to their data.
32 46
33 This function only supports RSA-SHA1 authentication. Settings in the Django
34 settings module determine the consumer key and path to the RSA private key.
35 """ 47 """
36 logger.info("fetch_auth started; callback url='%s'", callback_url) 48 logger.info("get_auth_url started; callback url='%s'", callback_url)
37 client = CalendarResourceClient(None, source=USER_AGENT)
38 49
39 with open(settings.GOOGLE_OAUTH_PRIVATE_KEY_PATH, 'r') as f: 50 flow = OAuth2WebServerFlow(client_id=settings.GCAL_CLIENT_ID,
40 rsa_key = f.read() 51 client_secret=settings.GCAL_CLIENT_SECRET,
41 logger.info("read RSA key; now getting request token") 52 scope=SCOPE,
53 redirect_uri=callback_url)
42 54
43 request_token = client.GetOAuthToken( 55 auth_url = flow.step1_get_authorize_url()
44 scopes, 56 logger.info("auth url: '%s'", auth_url)
45 callback_url,
46 settings.GOOGLE_OAUTH_CONSUMER_KEY,
47 rsa_private_key=rsa_key)
48 57
49 logger.info("received token") 58 # Save the flow in the cache so we can use it when Google calls the
50 request.session[REQ_TOKEN_SESSION_KEY] = request_token 59 # callback. The expiration time also lets us check to make sure someone
60 # isn't spoofing google if we are called at some random time.
61 # Note: using the session might seem like a more obvious choice, but flow
62 # objects are not JSON-serializable, and we don't want to use pickelable
63 # sessions for security reasons.
51 64
52 auth_url = request_token.generate_authorization_url() 65 cache.set(FLOW_CACHE_KEY, flow, FLOW_CACHE_TIMEOUT)
53 logger.info("generated auth url '%s'", str(auth_url))
54 66
55 return str(auth_url) 67 return auth_url
56 68
57 69
58 def get_access_token(request): 70 def auth_return(request):
59 """ 71 """
60 This function should be called after Google has sent the user back to us 72 This function should be called after Google has sent the user back to us
61 after the user authorized us. We retrieve the oauth token from the request 73 after the user authorized us. We retrieve the authorization code from the
62 URL and then upgrade it to an access token. We then return the access token. 74 request query parameters and then exchange it for access tokens. These
75 tokens are stored in our credentials file.
63 76
77 If an error is encountered, an OAuthError is raised.
64 """ 78 """
65 logger.info("get_access_token called as '%s'", request.get_full_path()) 79 logger.info("auth_return called as '%s'", request.get_full_path())
66 80
67 saved_token = request.session.get(REQ_TOKEN_SESSION_KEY) 81 # Try to retrieve the flow object from the cache
68 if saved_token is None: 82 flow = cache.get(FLOW_CACHE_KEY)
69 logger.error("saved request token not found in session!") 83 if not flow:
70 return None 84 logger.warning("auth_return no flow in cache, bailing out")
85 raise OAuthError("No flow found. Perhaps we timed out?")
71 86
72 logger.info("extracting token...") 87 # Delete flow out of cache to close our operational window
73 request_token = gdata.gauth.AuthorizeRequestToken(saved_token, 88 cache.delete(FLOW_CACHE_KEY)
74 request.build_absolute_uri())
75 89
76 logger.info("upgrading to access token...") 90 # Process the request
91 error = request.GET.get('error')
92 if error:
93 logging.error("auth_return received error: %s", error)
94 raise OAuthError("Error authorizing request: %s" % error)
77 95
78 client = CalendarResourceClient(None, source=USER_AGENT) 96 code = request.GET.get('code')
79 access_token = client.GetAccessToken(request_token) 97 if not code:
98 logging.error("auth_return missing code")
99 raise OAuthError("No authorization code received")
80 100
81 logger.info("upgraded to access token...") 101 logging.info("auth_return exchanging code for credentials")
82 return access_token 102 try:
103 credentials = flow.step2_exchange(code)
104 except FlowExchangeError as ex:
105 logging.error("auth_return exchange failed: %s", ex)
106 raise OAuthError(str(ex))
107
108 logging.info("auth_return storing credentials")
109 storage = Storage(settings.GCAL_CREDENTIALS_PATH)
110 storage.put(credentials)
111 logging.info("auth_return completed successfully")
83 112
84 113
85 def serialize_token(token): 114 def serialize_token(token):
86 """ 115 """
87 This function turns a token into a string and returns it. 116 This function turns a token into a string and returns it.