blob: 6392533ed81f9b45b62dcb39351188b44078c9a7 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Joe Gregorio695fdc12011-01-16 16:46:55 -050014
Joe Gregorio9da2ad82011-09-11 14:04:44 -040015"""An OAuth 2.0 client.
Joe Gregorio695fdc12011-01-16 16:46:55 -050016
Joe Gregorio9da2ad82011-09-11 14:04:44 -040017Tools for interacting with OAuth 2.0 protected resources.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
22import copy
23import datetime
24import httplib2
25import logging
26import urllib
27import urlparse
28
Joe Gregorio9da2ad82011-09-11 14:04:44 -040029try: # pragma: no cover
Joe Gregorio695fdc12011-01-16 16:46:55 -050030 import simplejson
Joe Gregorio9da2ad82011-09-11 14:04:44 -040031except ImportError: # pragma: no cover
Joe Gregorio695fdc12011-01-16 16:46:55 -050032 try:
33 # Try to import from django, should work on App Engine
34 from django.utils import simplejson
35 except ImportError:
36 # Should work for Python2.6 and higher.
37 import json as simplejson
38
39try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040040 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050041except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040042 from cgi import parse_qsl
43
44logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050045
Joe Gregorio562b7312011-09-15 09:06:38 -040046# Expiry is stored in RFC3339 UTC format
Joe Gregorio1daa71b2011-09-15 18:12:14 -040047EXPIRY_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
Joe Gregorio562b7312011-09-15 09:06:38 -040048
Joe Gregorio695fdc12011-01-16 16:46:55 -050049
50class Error(Exception):
51 """Base error for this module."""
52 pass
53
54
Joe Gregorioccc79542011-02-19 00:05:26 -050055class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050056 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050057 pass
58
59
60class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050061 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050062 pass
63
64
Joe Gregorio3b79fa82011-02-17 11:47:17 -050065class AccessTokenCredentialsError(Error):
66 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050067 pass
68
69
70def _abstract():
71 raise NotImplementedError('You need to override this function')
72
73
74class Credentials(object):
75 """Base class for all Credentials objects.
76
Joe Gregorio562b7312011-09-15 09:06:38 -040077 Subclasses must define an authorize() method that applies the credentials to
78 an HTTP transport.
79
80 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
81 string as input and returns an instaniated Crentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -050082 """
83
Joe Gregorio562b7312011-09-15 09:06:38 -040084 NON_SERIALIZED_MEMBERS = ['store']
85
Joe Gregorio695fdc12011-01-16 16:46:55 -050086 def authorize(self, http):
87 """Take an httplib2.Http instance (or equivalent) and
88 authorizes it for the set of credentials, usually by
89 replacing http.request() with a method that adds in
90 the appropriate headers and then delegates to the original
91 Http.request() method.
92 """
93 _abstract()
94
Joe Gregorio562b7312011-09-15 09:06:38 -040095 def _to_json(self, strip):
96 """Utility function for creating a JSON representation of an instance of Credentials.
97
98 Args:
99 strip: array, An array of names of members to not include in the JSON.
100
101 Returns:
102 string, a JSON representation of this instance, suitable to pass to
103 from_json().
104 """
105 t = type(self)
106 d = copy.copy(self.__dict__)
107 for member in strip:
108 del d[member]
109 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
110 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
111 # Add in information we will need later to reconsistitue this instance.
112 d['_class'] = t.__name__
113 d['_module'] = t.__module__
114 return simplejson.dumps(d)
115
116 def to_json(self):
117 """Creating a JSON representation of an instance of Credentials.
118
119 Returns:
120 string, a JSON representation of this instance, suitable to pass to
121 from_json().
122 """
123 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
124
125 @classmethod
126 def new_from_json(cls, s):
127 """Utility class method to instantiate a Credentials subclass from a JSON
128 representation produced by to_json().
129
130 Args:
131 s: string, JSON from to_json().
132
133 Returns:
134 An instance of the subclass of Credentials that was serialized with
135 to_json().
136 """
137 data = simplejson.loads(s)
138 # Find and call the right classmethod from_json() to restore the object.
139 module = data['_module']
140 m = __import__(module)
141 for sub_module in module.split('.')[1:]:
142 m = getattr(m, sub_module)
143 kls = getattr(m, data['_class'])
144 from_json = getattr(kls, 'from_json')
145 return from_json(s)
146
JacobMoshenko8e905102011-06-20 09:53:10 -0400147
Joe Gregorio695fdc12011-01-16 16:46:55 -0500148class Flow(object):
149 """Base class for all Flow objects."""
150 pass
151
152
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500153class Storage(object):
154 """Base class for all Storage objects.
155
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400156 Store and retrieve a single credential. This class supports locking
157 such that multiple processes and threads can operate on a single
158 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500159 """
160
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400161 def acquire_lock(self):
162 """Acquires any lock necessary to access this Storage.
163
164 This lock is not reentrant."""
165 pass
166
167 def release_lock(self):
168 """Release the Storage lock.
169
170 Trying to release a lock that isn't held will result in a
171 RuntimeError.
172 """
173 pass
174
175 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500176 """Retrieve credential.
177
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400178 The Storage lock must be held when this is called.
179
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500180 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400181 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500182 """
183 _abstract()
184
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400185 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500186 """Write a credential.
187
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400188 The Storage lock must be held when this is called.
189
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500190 Args:
191 credentials: Credentials, the credentials to store.
192 """
193 _abstract()
194
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400195 def get(self):
196 """Retrieve credential.
197
198 The Storage lock must *not* be held when this is called.
199
200 Returns:
201 oauth2client.client.Credentials
202 """
203 self.acquire_lock()
204 try:
205 return self.locked_get()
206 finally:
207 self.release_lock()
208
209 def put(self, credentials):
210 """Write a credential.
211
212 The Storage lock must be held when this is called.
213
214 Args:
215 credentials: Credentials, the credentials to store.
216 """
217 self.acquire_lock()
218 try:
219 self.locked_put(credentials)
220 finally:
221 self.release_lock()
222
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500223
Joe Gregorio695fdc12011-01-16 16:46:55 -0500224class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400225 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500226
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500227 Credentials can be applied to an httplib2.Http object using the authorize()
228 method, which then signs each request from that object with the OAuth 2.0
229 access token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500230
231 OAuth2Credentials objects may be safely pickled and unpickled.
232 """
233
234 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400235 token_expiry, token_uri, user_agent):
236 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500237
238 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500239 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500240
241 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400242 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500243 client_id: string, client identifier.
244 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500245 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400246 token_expiry: datetime, when the access_token expires.
247 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500248 user_agent: string, The HTTP User-Agent to provide for this application.
249
Joe Gregorio695fdc12011-01-16 16:46:55 -0500250 Notes:
251 store: callable, a callable that when passed a Credential
252 will store the credential back to where it came from.
253 This is needed to store the latest access_token if it
254 has expired and been refreshed.
255 """
256 self.access_token = access_token
257 self.client_id = client_id
258 self.client_secret = client_secret
259 self.refresh_token = refresh_token
260 self.store = None
261 self.token_expiry = token_expiry
262 self.token_uri = token_uri
263 self.user_agent = user_agent
264
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500265 # True if the credentials have been revoked or expired and can't be
266 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400267 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500268
Joe Gregorio562b7312011-09-15 09:06:38 -0400269 def to_json(self):
270 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
271
272 @classmethod
273 def from_json(cls, s):
274 """Instantiate a Credentials object from a JSON description of it. The JSON
275 should have been produced by calling .to_json() on the object.
276
277 Args:
278 data: dict, A deserialized JSON object.
279
280 Returns:
281 An instance of a Credentials subclass.
282 """
283 data = simplejson.loads(s)
284 if 'token_expiry' in data and not isinstance(data['token_expiry'],
285 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400286 try:
287 data['token_expiry'] = datetime.datetime.strptime(
288 data['token_expiry'], EXPIRY_FORMAT)
289 except:
290 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400291 retval = OAuth2Credentials(
292 data['access_token'],
293 data['client_id'],
294 data['client_secret'],
295 data['refresh_token'],
296 data['token_expiry'],
297 data['token_uri'],
298 data['user_agent'])
299 retval.invalid = data['invalid']
300 return retval
301
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500302 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400303 def access_token_expired(self):
304 """True if the credential is expired or invalid.
305
306 If the token_expiry isn't set, we assume the token doesn't expire.
307 """
308 if self.invalid:
309 return True
310
311 if not self.token_expiry:
312 return False
313
Joe Gregorio562b7312011-09-15 09:06:38 -0400314 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400315 if now >= self.token_expiry:
316 logger.info('access_token is expired. Now: %s, token_expiry: %s',
317 now, self.token_expiry)
318 return True
319 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500320
Joe Gregorio695fdc12011-01-16 16:46:55 -0500321 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400322 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500323
324 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400325 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500326 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400327 has expired and been refreshed. This implementation uses
328 locking to check for updates before updating the
329 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500330 """
331 self.store = store
332
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400333 def _updateFromCredential(self, other):
334 """Update this Credential from another instance."""
335 self.__dict__.update(other.__getstate__())
336
Joe Gregorio695fdc12011-01-16 16:46:55 -0500337 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400338 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500339 d = copy.copy(self.__dict__)
340 del d['store']
341 return d
342
343 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400344 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500345 self.__dict__.update(state)
346 self.store = None
347
JacobMoshenko8e905102011-06-20 09:53:10 -0400348 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400349 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400350 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400351 'grant_type': 'refresh_token',
352 'client_id': self.client_id,
353 'client_secret': self.client_secret,
354 'refresh_token': self.refresh_token,
355 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400356 return body
357
358 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400359 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400360 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400361 'content-type': 'application/x-www-form-urlencoded',
362 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400363
364 if self.user_agent is not None:
365 headers['user-agent'] = self.user_agent
366
JacobMoshenko8e905102011-06-20 09:53:10 -0400367 return headers
368
Joe Gregorio695fdc12011-01-16 16:46:55 -0500369 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400370 """Refreshes the access_token.
371
372 This method first checks by reading the Storage object if available.
373 If a refresh is still needed, it holds the Storage lock until the
374 refresh is completed.
375 """
376 if not self.store:
377 self._do_refresh_request(http_request)
378 else:
379 self.store.acquire_lock()
380 try:
381 new_cred = self.store.locked_get()
382 if (new_cred and not new_cred.invalid and
383 new_cred.access_token != self.access_token):
384 logger.info('Updated access_token read from Storage')
385 self._updateFromCredential(new_cred)
386 else:
387 self._do_refresh_request(http_request)
388 finally:
389 self.store.release_lock()
390
391 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500392 """Refresh the access_token using the refresh_token.
393
394 Args:
395 http: An instance of httplib2.Http.request
396 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400397
398 Raises:
399 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500400 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400401 body = self._generate_refresh_request_body()
402 headers = self._generate_refresh_request_headers()
403
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400404 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500405 resp, content = http_request(
406 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500407 if resp.status == 200:
408 # TODO(jcgregorio) Raise an error if loads fails?
409 d = simplejson.loads(content)
410 self.access_token = d['access_token']
411 self.refresh_token = d.get('refresh_token', self.refresh_token)
412 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500413 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400414 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500415 else:
416 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400417 if self.store:
418 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500419 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400420 # An {'error':...} response body means the token is expired or revoked,
421 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400422 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500423 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500424 try:
425 d = simplejson.loads(content)
426 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500427 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400428 self.invalid = True
429 if self.store:
430 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500431 except:
432 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500433 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500434
435 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500436 """Authorize an httplib2.Http instance with these credentials.
437
Joe Gregorio695fdc12011-01-16 16:46:55 -0500438 Args:
439 http: An instance of httplib2.Http
440 or something that acts like it.
441
442 Returns:
443 A modified instance of http that was passed in.
444
445 Example:
446
447 h = httplib2.Http()
448 h = credentials.authorize(h)
449
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400450 You can't create a new OAuth subclass of httplib2.Authenication
451 because it never gets passed the absolute URI, which is needed for
452 signing. So instead we have to overload 'request' with a closure
453 that adds in the Authorization header and then calls the original
454 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500455 """
456 request_orig = http.request
457
458 # The closure that will replace 'httplib2.Http.request'.
459 def new_request(uri, method='GET', body=None, headers=None,
460 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
461 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400462 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400463 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400464 self._refresh(request_orig)
465
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400466 # Modify the request headers to add the appropriate
467 # Authorization header.
468 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500469 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500470 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400471
472 if self.user_agent is not None:
473 if 'user-agent' in headers:
474 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
475 else:
476 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400477
Joe Gregorio695fdc12011-01-16 16:46:55 -0500478 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500479 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400480
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500481 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400482 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500483 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500484 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500485 return request_orig(uri, method, body, headers,
486 redirections, connection_type)
487 else:
488 return (resp, content)
489
490 http.request = new_request
491 return http
492
493
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500494class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400495 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500496
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400497 Credentials can be applied to an httplib2.Http object using the
498 authorize() method, which then signs each request from that object
499 with the OAuth 2.0 access token. This set of credentials is for the
500 use case where you have acquired an OAuth 2.0 access_token from
501 another place such as a JavaScript client or another web
502 application, and wish to use it from Python. Because only the
503 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500504 expire.
505
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500506 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500507
508 Usage:
509 credentials = AccessTokenCredentials('<an access token>',
510 'my-user-agent/1.0')
511 http = httplib2.Http()
512 http = credentials.authorize(http)
513
514 Exceptions:
515 AccessTokenCredentialsExpired: raised when the access_token expires or is
516 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500517 """
518
519 def __init__(self, access_token, user_agent):
520 """Create an instance of OAuth2Credentials
521
522 This is one of the few types if Credentials that you should contrust,
523 Credentials objects are usually instantiated by a Flow.
524
525 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000526 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500527 user_agent: string, The HTTP User-Agent to provide for this application.
528
529 Notes:
530 store: callable, a callable that when passed a Credential
531 will store the credential back to where it came from.
532 """
533 super(AccessTokenCredentials, self).__init__(
534 access_token,
535 None,
536 None,
537 None,
538 None,
539 None,
540 user_agent)
541
Joe Gregorio562b7312011-09-15 09:06:38 -0400542
543 @classmethod
544 def from_json(cls, s):
545 data = simplejson.loads(s)
546 retval = AccessTokenCredentials(
547 data['access_token'],
548 data['user_agent'])
549 return retval
550
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500551 def _refresh(self, http_request):
552 raise AccessTokenCredentialsError(
553 "The access_token is expired or invalid and can't be refreshed.")
554
JacobMoshenko8e905102011-06-20 09:53:10 -0400555
556class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400557 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400558
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400559 This credential does not require a flow to instantiate because it
560 represents a two legged flow, and therefore has all of the required
561 information to generate and refresh its own access tokens. It must
562 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400563
564 AssertionCredentials objects may be safely pickled and unpickled.
565 """
566
567 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400568 token_uri='https://accounts.google.com/o/oauth2/token',
569 **unused_kwargs):
570 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400571
572 Args:
573 assertion_type: string, assertion type that will be declared to the auth
574 server
575 user_agent: string, The HTTP User-Agent to provide for this application.
576 token_uri: string, URI for token endpoint. For convenience
577 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
578 """
579 super(AssertionCredentials, self).__init__(
580 None,
581 None,
582 None,
583 None,
584 None,
585 token_uri,
586 user_agent)
587 self.assertion_type = assertion_type
588
589 def _generate_refresh_request_body(self):
590 assertion = self._generate_assertion()
591
592 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400593 'assertion_type': self.assertion_type,
594 'assertion': assertion,
595 'grant_type': 'assertion',
596 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400597
598 return body
599
600 def _generate_assertion(self):
601 """Generate the assertion string that will be used in the access token
602 request.
603 """
604 _abstract()
605
606
Joe Gregorio695fdc12011-01-16 16:46:55 -0500607class OAuth2WebServerFlow(Flow):
608 """Does the Web Server Flow for OAuth 2.0.
609
610 OAuth2Credentials objects may be safely pickled and unpickled.
611 """
612
613 def __init__(self, client_id, client_secret, scope, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400614 auth_uri='https://accounts.google.com/o/oauth2/auth',
615 token_uri='https://accounts.google.com/o/oauth2/token',
616 **kwargs):
617 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500618
619 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500620 client_id: string, client identifier.
621 client_secret: string client secret.
622 scope: string, scope of the credentials being requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500623 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500624 auth_uri: string, URI for authorization endpoint. For convenience
625 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
626 token_uri: string, URI for token endpoint. For convenience
627 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500628 **kwargs: dict, The keyword arguments are all optional and required
629 parameters for the OAuth calls.
630 """
631 self.client_id = client_id
632 self.client_secret = client_secret
633 self.scope = scope
634 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500635 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500636 self.token_uri = token_uri
637 self.params = kwargs
638 self.redirect_uri = None
639
640 def step1_get_authorize_url(self, redirect_uri='oob'):
641 """Returns a URI to redirect to the provider.
642
643 Args:
644 redirect_uri: string, Either the string 'oob' for a non-web-based
645 application, or a URI that handles the callback from
646 the authorization server.
647
648 If redirect_uri is 'oob' then pass in the
649 generated verification code to step2_exchange,
650 otherwise pass in the query parameters received
651 at the callback uri to step2_exchange.
652 """
653
654 self.redirect_uri = redirect_uri
655 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400656 'response_type': 'code',
657 'client_id': self.client_id,
658 'redirect_uri': redirect_uri,
659 'scope': self.scope,
660 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500661 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500662 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500663 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
664 parts[4] = urllib.urlencode(query)
665 return urlparse.urlunparse(parts)
666
Joe Gregorioccc79542011-02-19 00:05:26 -0500667 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500668 """Exhanges a code for OAuth2Credentials.
669
670 Args:
671 code: string or dict, either the code as a string, or a dictionary
672 of the query parameters to the redirect_uri, which contains
673 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500674 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500675 """
676
677 if not (isinstance(code, str) or isinstance(code, unicode)):
678 code = code['code']
679
680 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400681 'grant_type': 'authorization_code',
682 'client_id': self.client_id,
683 'client_secret': self.client_secret,
684 'code': code,
685 'redirect_uri': self.redirect_uri,
686 'scope': self.scope,
687 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500688 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400689 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500690 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400691
692 if self.user_agent is not None:
693 headers['user-agent'] = self.user_agent
694
Joe Gregorioccc79542011-02-19 00:05:26 -0500695 if http is None:
696 http = httplib2.Http()
JacobMoshenko8e905102011-06-20 09:53:10 -0400697 resp, content = http.request(self.token_uri, method='POST', body=body,
698 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500699 if resp.status == 200:
700 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
701 d = simplejson.loads(content)
702 access_token = d['access_token']
703 refresh_token = d.get('refresh_token', None)
704 token_expiry = None
705 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400706 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400707 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500708
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400709 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400710 return OAuth2Credentials(access_token, self.client_id,
711 self.client_secret, refresh_token, token_expiry,
712 self.token_uri, self.user_agent)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500713 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400714 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500715 error_msg = 'Invalid response %s.' % resp['status']
716 try:
717 d = simplejson.loads(content)
718 if 'error' in d:
719 error_msg = d['error']
720 except:
721 pass
722
723 raise FlowExchangeError(error_msg)