blob: 2b97d4dde83b3ef44ca36b00e550c64455447864 [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
47EXPIRY_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
48
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):
286 data['token_expiry'] = datetime.datetime.strptime(
287 data['token_expiry'], EXPIRY_FORMAT)
288 retval = OAuth2Credentials(
289 data['access_token'],
290 data['client_id'],
291 data['client_secret'],
292 data['refresh_token'],
293 data['token_expiry'],
294 data['token_uri'],
295 data['user_agent'])
296 retval.invalid = data['invalid']
297 return retval
298
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500299 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400300 def access_token_expired(self):
301 """True if the credential is expired or invalid.
302
303 If the token_expiry isn't set, we assume the token doesn't expire.
304 """
305 if self.invalid:
306 return True
307
308 if not self.token_expiry:
309 return False
310
Joe Gregorio562b7312011-09-15 09:06:38 -0400311 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400312 if now >= self.token_expiry:
313 logger.info('access_token is expired. Now: %s, token_expiry: %s',
314 now, self.token_expiry)
315 return True
316 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500317
Joe Gregorio695fdc12011-01-16 16:46:55 -0500318 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400319 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500320
321 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400322 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500323 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400324 has expired and been refreshed. This implementation uses
325 locking to check for updates before updating the
326 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500327 """
328 self.store = store
329
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400330 def _updateFromCredential(self, other):
331 """Update this Credential from another instance."""
332 self.__dict__.update(other.__getstate__())
333
Joe Gregorio695fdc12011-01-16 16:46:55 -0500334 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400335 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500336 d = copy.copy(self.__dict__)
337 del d['store']
338 return d
339
340 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400341 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500342 self.__dict__.update(state)
343 self.store = None
344
JacobMoshenko8e905102011-06-20 09:53:10 -0400345 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400346 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400347 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400348 'grant_type': 'refresh_token',
349 'client_id': self.client_id,
350 'client_secret': self.client_secret,
351 'refresh_token': self.refresh_token,
352 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400353 return body
354
355 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400356 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400357 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400358 'content-type': 'application/x-www-form-urlencoded',
359 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400360
361 if self.user_agent is not None:
362 headers['user-agent'] = self.user_agent
363
JacobMoshenko8e905102011-06-20 09:53:10 -0400364 return headers
365
Joe Gregorio695fdc12011-01-16 16:46:55 -0500366 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400367 """Refreshes the access_token.
368
369 This method first checks by reading the Storage object if available.
370 If a refresh is still needed, it holds the Storage lock until the
371 refresh is completed.
372 """
373 if not self.store:
374 self._do_refresh_request(http_request)
375 else:
376 self.store.acquire_lock()
377 try:
378 new_cred = self.store.locked_get()
379 if (new_cred and not new_cred.invalid and
380 new_cred.access_token != self.access_token):
381 logger.info('Updated access_token read from Storage')
382 self._updateFromCredential(new_cred)
383 else:
384 self._do_refresh_request(http_request)
385 finally:
386 self.store.release_lock()
387
388 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500389 """Refresh the access_token using the refresh_token.
390
391 Args:
392 http: An instance of httplib2.Http.request
393 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400394
395 Raises:
396 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500397 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400398 body = self._generate_refresh_request_body()
399 headers = self._generate_refresh_request_headers()
400
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400401 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500402 resp, content = http_request(
403 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500404 if resp.status == 200:
405 # TODO(jcgregorio) Raise an error if loads fails?
406 d = simplejson.loads(content)
407 self.access_token = d['access_token']
408 self.refresh_token = d.get('refresh_token', self.refresh_token)
409 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500410 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400411 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500412 else:
413 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400414 if self.store:
415 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500416 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400417 # An {'error':...} response body means the token is expired or revoked,
418 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400419 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500420 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500421 try:
422 d = simplejson.loads(content)
423 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500424 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400425 self.invalid = True
426 if self.store:
427 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500428 except:
429 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500430 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500431
432 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500433 """Authorize an httplib2.Http instance with these credentials.
434
Joe Gregorio695fdc12011-01-16 16:46:55 -0500435 Args:
436 http: An instance of httplib2.Http
437 or something that acts like it.
438
439 Returns:
440 A modified instance of http that was passed in.
441
442 Example:
443
444 h = httplib2.Http()
445 h = credentials.authorize(h)
446
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400447 You can't create a new OAuth subclass of httplib2.Authenication
448 because it never gets passed the absolute URI, which is needed for
449 signing. So instead we have to overload 'request' with a closure
450 that adds in the Authorization header and then calls the original
451 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500452 """
453 request_orig = http.request
454
455 # The closure that will replace 'httplib2.Http.request'.
456 def new_request(uri, method='GET', body=None, headers=None,
457 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
458 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400459 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400460 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400461 self._refresh(request_orig)
462
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400463 # Modify the request headers to add the appropriate
464 # Authorization header.
465 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500466 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500467 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400468
469 if self.user_agent is not None:
470 if 'user-agent' in headers:
471 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
472 else:
473 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400474
Joe Gregorio695fdc12011-01-16 16:46:55 -0500475 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500476 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400477
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500478 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400479 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500480 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500481 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500482 return request_orig(uri, method, body, headers,
483 redirections, connection_type)
484 else:
485 return (resp, content)
486
487 http.request = new_request
488 return http
489
490
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500491class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400492 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500493
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400494 Credentials can be applied to an httplib2.Http object using the
495 authorize() method, which then signs each request from that object
496 with the OAuth 2.0 access token. This set of credentials is for the
497 use case where you have acquired an OAuth 2.0 access_token from
498 another place such as a JavaScript client or another web
499 application, and wish to use it from Python. Because only the
500 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500501 expire.
502
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500503 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500504
505 Usage:
506 credentials = AccessTokenCredentials('<an access token>',
507 'my-user-agent/1.0')
508 http = httplib2.Http()
509 http = credentials.authorize(http)
510
511 Exceptions:
512 AccessTokenCredentialsExpired: raised when the access_token expires or is
513 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500514 """
515
516 def __init__(self, access_token, user_agent):
517 """Create an instance of OAuth2Credentials
518
519 This is one of the few types if Credentials that you should contrust,
520 Credentials objects are usually instantiated by a Flow.
521
522 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000523 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500524 user_agent: string, The HTTP User-Agent to provide for this application.
525
526 Notes:
527 store: callable, a callable that when passed a Credential
528 will store the credential back to where it came from.
529 """
530 super(AccessTokenCredentials, self).__init__(
531 access_token,
532 None,
533 None,
534 None,
535 None,
536 None,
537 user_agent)
538
Joe Gregorio562b7312011-09-15 09:06:38 -0400539
540 @classmethod
541 def from_json(cls, s):
542 data = simplejson.loads(s)
543 retval = AccessTokenCredentials(
544 data['access_token'],
545 data['user_agent'])
546 return retval
547
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500548 def _refresh(self, http_request):
549 raise AccessTokenCredentialsError(
550 "The access_token is expired or invalid and can't be refreshed.")
551
JacobMoshenko8e905102011-06-20 09:53:10 -0400552
553class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400554 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400555
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400556 This credential does not require a flow to instantiate because it
557 represents a two legged flow, and therefore has all of the required
558 information to generate and refresh its own access tokens. It must
559 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400560
561 AssertionCredentials objects may be safely pickled and unpickled.
562 """
563
564 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400565 token_uri='https://accounts.google.com/o/oauth2/token',
566 **unused_kwargs):
567 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400568
569 Args:
570 assertion_type: string, assertion type that will be declared to the auth
571 server
572 user_agent: string, The HTTP User-Agent to provide for this application.
573 token_uri: string, URI for token endpoint. For convenience
574 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
575 """
576 super(AssertionCredentials, self).__init__(
577 None,
578 None,
579 None,
580 None,
581 None,
582 token_uri,
583 user_agent)
584 self.assertion_type = assertion_type
585
586 def _generate_refresh_request_body(self):
587 assertion = self._generate_assertion()
588
589 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400590 'assertion_type': self.assertion_type,
591 'assertion': assertion,
592 'grant_type': 'assertion',
593 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400594
595 return body
596
597 def _generate_assertion(self):
598 """Generate the assertion string that will be used in the access token
599 request.
600 """
601 _abstract()
602
603
Joe Gregorio695fdc12011-01-16 16:46:55 -0500604class OAuth2WebServerFlow(Flow):
605 """Does the Web Server Flow for OAuth 2.0.
606
607 OAuth2Credentials objects may be safely pickled and unpickled.
608 """
609
610 def __init__(self, client_id, client_secret, scope, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400611 auth_uri='https://accounts.google.com/o/oauth2/auth',
612 token_uri='https://accounts.google.com/o/oauth2/token',
613 **kwargs):
614 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500615
616 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500617 client_id: string, client identifier.
618 client_secret: string client secret.
619 scope: string, scope of the credentials being requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500620 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500621 auth_uri: string, URI for authorization endpoint. For convenience
622 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
623 token_uri: string, URI for token endpoint. For convenience
624 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500625 **kwargs: dict, The keyword arguments are all optional and required
626 parameters for the OAuth calls.
627 """
628 self.client_id = client_id
629 self.client_secret = client_secret
630 self.scope = scope
631 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500632 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500633 self.token_uri = token_uri
634 self.params = kwargs
635 self.redirect_uri = None
636
637 def step1_get_authorize_url(self, redirect_uri='oob'):
638 """Returns a URI to redirect to the provider.
639
640 Args:
641 redirect_uri: string, Either the string 'oob' for a non-web-based
642 application, or a URI that handles the callback from
643 the authorization server.
644
645 If redirect_uri is 'oob' then pass in the
646 generated verification code to step2_exchange,
647 otherwise pass in the query parameters received
648 at the callback uri to step2_exchange.
649 """
650
651 self.redirect_uri = redirect_uri
652 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400653 'response_type': 'code',
654 'client_id': self.client_id,
655 'redirect_uri': redirect_uri,
656 'scope': self.scope,
657 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500658 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500659 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500660 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
661 parts[4] = urllib.urlencode(query)
662 return urlparse.urlunparse(parts)
663
Joe Gregorioccc79542011-02-19 00:05:26 -0500664 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500665 """Exhanges a code for OAuth2Credentials.
666
667 Args:
668 code: string or dict, either the code as a string, or a dictionary
669 of the query parameters to the redirect_uri, which contains
670 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500671 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500672 """
673
674 if not (isinstance(code, str) or isinstance(code, unicode)):
675 code = code['code']
676
677 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400678 'grant_type': 'authorization_code',
679 'client_id': self.client_id,
680 'client_secret': self.client_secret,
681 'code': code,
682 'redirect_uri': self.redirect_uri,
683 'scope': self.scope,
684 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500685 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400686 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500687 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400688
689 if self.user_agent is not None:
690 headers['user-agent'] = self.user_agent
691
Joe Gregorioccc79542011-02-19 00:05:26 -0500692 if http is None:
693 http = httplib2.Http()
JacobMoshenko8e905102011-06-20 09:53:10 -0400694 resp, content = http.request(self.token_uri, method='POST', body=body,
695 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500696 if resp.status == 200:
697 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
698 d = simplejson.loads(content)
699 access_token = d['access_token']
700 refresh_token = d.get('refresh_token', None)
701 token_expiry = None
702 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400703 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400704 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500705
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400706 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400707 return OAuth2Credentials(access_token, self.client_id,
708 self.client_secret, refresh_token, token_expiry,
709 self.token_uri, self.user_agent)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500710 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400711 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500712 error_msg = 'Invalid response %s.' % resp['status']
713 try:
714 d = simplejson.loads(content)
715 if 'error' in d:
716 error_msg = d['error']
717 except:
718 pass
719
720 raise FlowExchangeError(error_msg)