blob: 33e1ae0f8bbad773f1ca0b460dac0d7142b7b414 [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
Joe Gregoriof08a4982011-10-07 13:11:16 -040022import clientsecrets
Joe Gregorio695fdc12011-01-16 16:46:55 -050023import copy
24import datetime
25import httplib2
26import logging
Joe Gregoriof08a4982011-10-07 13:11:16 -040027import sys
Joe Gregorio695fdc12011-01-16 16:46:55 -050028import urllib
29import urlparse
30
Joe Gregorio9da2ad82011-09-11 14:04:44 -040031try: # pragma: no cover
Joe Gregorio695fdc12011-01-16 16:46:55 -050032 import simplejson
Joe Gregorio9da2ad82011-09-11 14:04:44 -040033except ImportError: # pragma: no cover
Joe Gregorio695fdc12011-01-16 16:46:55 -050034 try:
35 # Try to import from django, should work on App Engine
36 from django.utils import simplejson
37 except ImportError:
38 # Should work for Python2.6 and higher.
39 import json as simplejson
40
41try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040042 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050043except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040044 from cgi import parse_qsl
45
46logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050047
Joe Gregorio562b7312011-09-15 09:06:38 -040048# Expiry is stored in RFC3339 UTC format
Joe Gregorio1daa71b2011-09-15 18:12:14 -040049EXPIRY_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
Joe Gregorio562b7312011-09-15 09:06:38 -040050
Joe Gregorio695fdc12011-01-16 16:46:55 -050051
52class Error(Exception):
53 """Base error for this module."""
54 pass
55
56
Joe Gregorioccc79542011-02-19 00:05:26 -050057class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050058 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050059 pass
60
61
62class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050063 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050064 pass
65
Joe Gregoriof08a4982011-10-07 13:11:16 -040066class UnknownClientSecretsFlowError(Error):
67 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
68 pass
69
Joe Gregorio695fdc12011-01-16 16:46:55 -050070
Joe Gregorio3b79fa82011-02-17 11:47:17 -050071class AccessTokenCredentialsError(Error):
72 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050073 pass
74
75
76def _abstract():
77 raise NotImplementedError('You need to override this function')
78
79
80class Credentials(object):
81 """Base class for all Credentials objects.
82
Joe Gregorio562b7312011-09-15 09:06:38 -040083 Subclasses must define an authorize() method that applies the credentials to
84 an HTTP transport.
85
86 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
87 string as input and returns an instaniated Crentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -050088 """
89
Joe Gregorio562b7312011-09-15 09:06:38 -040090 NON_SERIALIZED_MEMBERS = ['store']
91
Joe Gregorio695fdc12011-01-16 16:46:55 -050092 def authorize(self, http):
93 """Take an httplib2.Http instance (or equivalent) and
94 authorizes it for the set of credentials, usually by
95 replacing http.request() with a method that adds in
96 the appropriate headers and then delegates to the original
97 Http.request() method.
98 """
99 _abstract()
100
Joe Gregorio562b7312011-09-15 09:06:38 -0400101 def _to_json(self, strip):
102 """Utility function for creating a JSON representation of an instance of Credentials.
103
104 Args:
105 strip: array, An array of names of members to not include in the JSON.
106
107 Returns:
108 string, a JSON representation of this instance, suitable to pass to
109 from_json().
110 """
111 t = type(self)
112 d = copy.copy(self.__dict__)
113 for member in strip:
114 del d[member]
115 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
116 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
117 # Add in information we will need later to reconsistitue this instance.
118 d['_class'] = t.__name__
119 d['_module'] = t.__module__
120 return simplejson.dumps(d)
121
122 def to_json(self):
123 """Creating a JSON representation of an instance of Credentials.
124
125 Returns:
126 string, a JSON representation of this instance, suitable to pass to
127 from_json().
128 """
129 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
130
131 @classmethod
132 def new_from_json(cls, s):
133 """Utility class method to instantiate a Credentials subclass from a JSON
134 representation produced by to_json().
135
136 Args:
137 s: string, JSON from to_json().
138
139 Returns:
140 An instance of the subclass of Credentials that was serialized with
141 to_json().
142 """
143 data = simplejson.loads(s)
144 # Find and call the right classmethod from_json() to restore the object.
145 module = data['_module']
146 m = __import__(module)
147 for sub_module in module.split('.')[1:]:
148 m = getattr(m, sub_module)
149 kls = getattr(m, data['_class'])
150 from_json = getattr(kls, 'from_json')
151 return from_json(s)
152
JacobMoshenko8e905102011-06-20 09:53:10 -0400153
Joe Gregorio695fdc12011-01-16 16:46:55 -0500154class Flow(object):
155 """Base class for all Flow objects."""
156 pass
157
158
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500159class Storage(object):
160 """Base class for all Storage objects.
161
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400162 Store and retrieve a single credential. This class supports locking
163 such that multiple processes and threads can operate on a single
164 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500165 """
166
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400167 def acquire_lock(self):
168 """Acquires any lock necessary to access this Storage.
169
170 This lock is not reentrant."""
171 pass
172
173 def release_lock(self):
174 """Release the Storage lock.
175
176 Trying to release a lock that isn't held will result in a
177 RuntimeError.
178 """
179 pass
180
181 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500182 """Retrieve credential.
183
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400184 The Storage lock must be held when this is called.
185
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500186 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400187 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500188 """
189 _abstract()
190
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400191 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500192 """Write a credential.
193
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400194 The Storage lock must be held when this is called.
195
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500196 Args:
197 credentials: Credentials, the credentials to store.
198 """
199 _abstract()
200
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400201 def get(self):
202 """Retrieve credential.
203
204 The Storage lock must *not* be held when this is called.
205
206 Returns:
207 oauth2client.client.Credentials
208 """
209 self.acquire_lock()
210 try:
211 return self.locked_get()
212 finally:
213 self.release_lock()
214
215 def put(self, credentials):
216 """Write a credential.
217
218 The Storage lock must be held when this is called.
219
220 Args:
221 credentials: Credentials, the credentials to store.
222 """
223 self.acquire_lock()
224 try:
225 self.locked_put(credentials)
226 finally:
227 self.release_lock()
228
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500229
Joe Gregorio695fdc12011-01-16 16:46:55 -0500230class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400231 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500232
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500233 Credentials can be applied to an httplib2.Http object using the authorize()
234 method, which then signs each request from that object with the OAuth 2.0
235 access token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500236
237 OAuth2Credentials objects may be safely pickled and unpickled.
238 """
239
240 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400241 token_expiry, token_uri, user_agent):
242 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500243
244 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500245 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500246
247 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400248 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500249 client_id: string, client identifier.
250 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500251 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400252 token_expiry: datetime, when the access_token expires.
253 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500254 user_agent: string, The HTTP User-Agent to provide for this application.
255
Joe Gregorio695fdc12011-01-16 16:46:55 -0500256 Notes:
257 store: callable, a callable that when passed a Credential
258 will store the credential back to where it came from.
259 This is needed to store the latest access_token if it
260 has expired and been refreshed.
261 """
262 self.access_token = access_token
263 self.client_id = client_id
264 self.client_secret = client_secret
265 self.refresh_token = refresh_token
266 self.store = None
267 self.token_expiry = token_expiry
268 self.token_uri = token_uri
269 self.user_agent = user_agent
270
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500271 # True if the credentials have been revoked or expired and can't be
272 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400273 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500274
Joe Gregorio562b7312011-09-15 09:06:38 -0400275 def to_json(self):
276 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
277
278 @classmethod
279 def from_json(cls, s):
280 """Instantiate a Credentials object from a JSON description of it. The JSON
281 should have been produced by calling .to_json() on the object.
282
283 Args:
284 data: dict, A deserialized JSON object.
285
286 Returns:
287 An instance of a Credentials subclass.
288 """
289 data = simplejson.loads(s)
290 if 'token_expiry' in data and not isinstance(data['token_expiry'],
291 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400292 try:
293 data['token_expiry'] = datetime.datetime.strptime(
294 data['token_expiry'], EXPIRY_FORMAT)
295 except:
296 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400297 retval = OAuth2Credentials(
298 data['access_token'],
299 data['client_id'],
300 data['client_secret'],
301 data['refresh_token'],
302 data['token_expiry'],
303 data['token_uri'],
304 data['user_agent'])
305 retval.invalid = data['invalid']
306 return retval
307
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500308 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400309 def access_token_expired(self):
310 """True if the credential is expired or invalid.
311
312 If the token_expiry isn't set, we assume the token doesn't expire.
313 """
314 if self.invalid:
315 return True
316
317 if not self.token_expiry:
318 return False
319
Joe Gregorio562b7312011-09-15 09:06:38 -0400320 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400321 if now >= self.token_expiry:
322 logger.info('access_token is expired. Now: %s, token_expiry: %s',
323 now, self.token_expiry)
324 return True
325 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500326
Joe Gregorio695fdc12011-01-16 16:46:55 -0500327 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400328 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500329
330 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400331 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500332 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400333 has expired and been refreshed. This implementation uses
334 locking to check for updates before updating the
335 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500336 """
337 self.store = store
338
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400339 def _updateFromCredential(self, other):
340 """Update this Credential from another instance."""
341 self.__dict__.update(other.__getstate__())
342
Joe Gregorio695fdc12011-01-16 16:46:55 -0500343 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400344 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500345 d = copy.copy(self.__dict__)
346 del d['store']
347 return d
348
349 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400350 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500351 self.__dict__.update(state)
352 self.store = None
353
JacobMoshenko8e905102011-06-20 09:53:10 -0400354 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400355 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400356 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400357 'grant_type': 'refresh_token',
358 'client_id': self.client_id,
359 'client_secret': self.client_secret,
360 'refresh_token': self.refresh_token,
361 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400362 return body
363
364 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400365 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400366 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400367 'content-type': 'application/x-www-form-urlencoded',
368 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400369
370 if self.user_agent is not None:
371 headers['user-agent'] = self.user_agent
372
JacobMoshenko8e905102011-06-20 09:53:10 -0400373 return headers
374
Joe Gregorio695fdc12011-01-16 16:46:55 -0500375 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400376 """Refreshes the access_token.
377
378 This method first checks by reading the Storage object if available.
379 If a refresh is still needed, it holds the Storage lock until the
380 refresh is completed.
381 """
382 if not self.store:
383 self._do_refresh_request(http_request)
384 else:
385 self.store.acquire_lock()
386 try:
387 new_cred = self.store.locked_get()
388 if (new_cred and not new_cred.invalid and
389 new_cred.access_token != self.access_token):
390 logger.info('Updated access_token read from Storage')
391 self._updateFromCredential(new_cred)
392 else:
393 self._do_refresh_request(http_request)
394 finally:
395 self.store.release_lock()
396
397 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500398 """Refresh the access_token using the refresh_token.
399
400 Args:
401 http: An instance of httplib2.Http.request
402 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400403
404 Raises:
405 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500406 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400407 body = self._generate_refresh_request_body()
408 headers = self._generate_refresh_request_headers()
409
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400410 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500411 resp, content = http_request(
412 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500413 if resp.status == 200:
414 # TODO(jcgregorio) Raise an error if loads fails?
415 d = simplejson.loads(content)
416 self.access_token = d['access_token']
417 self.refresh_token = d.get('refresh_token', self.refresh_token)
418 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500419 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400420 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500421 else:
422 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400423 if self.store:
424 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500425 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400426 # An {'error':...} response body means the token is expired or revoked,
427 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400428 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500429 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500430 try:
431 d = simplejson.loads(content)
432 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500433 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400434 self.invalid = True
435 if self.store:
436 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500437 except:
438 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500439 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500440
441 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500442 """Authorize an httplib2.Http instance with these credentials.
443
Joe Gregorio695fdc12011-01-16 16:46:55 -0500444 Args:
445 http: An instance of httplib2.Http
446 or something that acts like it.
447
448 Returns:
449 A modified instance of http that was passed in.
450
451 Example:
452
453 h = httplib2.Http()
454 h = credentials.authorize(h)
455
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400456 You can't create a new OAuth subclass of httplib2.Authenication
457 because it never gets passed the absolute URI, which is needed for
458 signing. So instead we have to overload 'request' with a closure
459 that adds in the Authorization header and then calls the original
460 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500461 """
462 request_orig = http.request
463
464 # The closure that will replace 'httplib2.Http.request'.
465 def new_request(uri, method='GET', body=None, headers=None,
466 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
467 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400468 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400469 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400470 self._refresh(request_orig)
471
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400472 # Modify the request headers to add the appropriate
473 # Authorization header.
474 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500475 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500476 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400477
478 if self.user_agent is not None:
479 if 'user-agent' in headers:
480 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
481 else:
482 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400483
Joe Gregorio695fdc12011-01-16 16:46:55 -0500484 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500485 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400486
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500487 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400488 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500489 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500490 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500491 return request_orig(uri, method, body, headers,
492 redirections, connection_type)
493 else:
494 return (resp, content)
495
496 http.request = new_request
497 return http
498
499
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500500class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400501 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500502
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400503 Credentials can be applied to an httplib2.Http object using the
504 authorize() method, which then signs each request from that object
505 with the OAuth 2.0 access token. This set of credentials is for the
506 use case where you have acquired an OAuth 2.0 access_token from
507 another place such as a JavaScript client or another web
508 application, and wish to use it from Python. Because only the
509 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500510 expire.
511
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500512 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500513
514 Usage:
515 credentials = AccessTokenCredentials('<an access token>',
516 'my-user-agent/1.0')
517 http = httplib2.Http()
518 http = credentials.authorize(http)
519
520 Exceptions:
521 AccessTokenCredentialsExpired: raised when the access_token expires or is
522 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500523 """
524
525 def __init__(self, access_token, user_agent):
526 """Create an instance of OAuth2Credentials
527
528 This is one of the few types if Credentials that you should contrust,
529 Credentials objects are usually instantiated by a Flow.
530
531 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000532 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500533 user_agent: string, The HTTP User-Agent to provide for this application.
534
535 Notes:
536 store: callable, a callable that when passed a Credential
537 will store the credential back to where it came from.
538 """
539 super(AccessTokenCredentials, self).__init__(
540 access_token,
541 None,
542 None,
543 None,
544 None,
545 None,
546 user_agent)
547
Joe Gregorio562b7312011-09-15 09:06:38 -0400548
549 @classmethod
550 def from_json(cls, s):
551 data = simplejson.loads(s)
552 retval = AccessTokenCredentials(
553 data['access_token'],
554 data['user_agent'])
555 return retval
556
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500557 def _refresh(self, http_request):
558 raise AccessTokenCredentialsError(
559 "The access_token is expired or invalid and can't be refreshed.")
560
JacobMoshenko8e905102011-06-20 09:53:10 -0400561
562class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400563 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400564
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400565 This credential does not require a flow to instantiate because it
566 represents a two legged flow, and therefore has all of the required
567 information to generate and refresh its own access tokens. It must
568 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400569
570 AssertionCredentials objects may be safely pickled and unpickled.
571 """
572
573 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400574 token_uri='https://accounts.google.com/o/oauth2/token',
575 **unused_kwargs):
576 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400577
578 Args:
579 assertion_type: string, assertion type that will be declared to the auth
580 server
581 user_agent: string, The HTTP User-Agent to provide for this application.
582 token_uri: string, URI for token endpoint. For convenience
583 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
584 """
585 super(AssertionCredentials, self).__init__(
586 None,
587 None,
588 None,
589 None,
590 None,
591 token_uri,
592 user_agent)
593 self.assertion_type = assertion_type
594
595 def _generate_refresh_request_body(self):
596 assertion = self._generate_assertion()
597
598 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400599 'assertion_type': self.assertion_type,
600 'assertion': assertion,
601 'grant_type': 'assertion',
602 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400603
604 return body
605
606 def _generate_assertion(self):
607 """Generate the assertion string that will be used in the access token
608 request.
609 """
610 _abstract()
611
612
Joe Gregorio695fdc12011-01-16 16:46:55 -0500613class OAuth2WebServerFlow(Flow):
614 """Does the Web Server Flow for OAuth 2.0.
615
616 OAuth2Credentials objects may be safely pickled and unpickled.
617 """
618
Joe Gregoriof08a4982011-10-07 13:11:16 -0400619 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400620 auth_uri='https://accounts.google.com/o/oauth2/auth',
621 token_uri='https://accounts.google.com/o/oauth2/token',
622 **kwargs):
623 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500624
625 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500626 client_id: string, client identifier.
627 client_secret: string client secret.
628 scope: string, scope of the credentials being requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500629 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500630 auth_uri: string, URI for authorization endpoint. For convenience
631 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
632 token_uri: string, URI for token endpoint. For convenience
633 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500634 **kwargs: dict, The keyword arguments are all optional and required
635 parameters for the OAuth calls.
636 """
637 self.client_id = client_id
638 self.client_secret = client_secret
639 self.scope = scope
640 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500641 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500642 self.token_uri = token_uri
643 self.params = kwargs
644 self.redirect_uri = None
645
646 def step1_get_authorize_url(self, redirect_uri='oob'):
647 """Returns a URI to redirect to the provider.
648
649 Args:
650 redirect_uri: string, Either the string 'oob' for a non-web-based
651 application, or a URI that handles the callback from
652 the authorization server.
653
654 If redirect_uri is 'oob' then pass in the
655 generated verification code to step2_exchange,
656 otherwise pass in the query parameters received
657 at the callback uri to step2_exchange.
658 """
659
660 self.redirect_uri = redirect_uri
661 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400662 'response_type': 'code',
663 'client_id': self.client_id,
664 'redirect_uri': redirect_uri,
665 'scope': self.scope,
666 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500667 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500668 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500669 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
670 parts[4] = urllib.urlencode(query)
671 return urlparse.urlunparse(parts)
672
Joe Gregorioccc79542011-02-19 00:05:26 -0500673 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500674 """Exhanges a code for OAuth2Credentials.
675
676 Args:
677 code: string or dict, either the code as a string, or a dictionary
678 of the query parameters to the redirect_uri, which contains
679 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500680 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500681 """
682
683 if not (isinstance(code, str) or isinstance(code, unicode)):
684 code = code['code']
685
686 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400687 'grant_type': 'authorization_code',
688 'client_id': self.client_id,
689 'client_secret': self.client_secret,
690 'code': code,
691 'redirect_uri': self.redirect_uri,
692 'scope': self.scope,
693 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500694 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400695 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500696 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400697
698 if self.user_agent is not None:
699 headers['user-agent'] = self.user_agent
700
Joe Gregorioccc79542011-02-19 00:05:26 -0500701 if http is None:
702 http = httplib2.Http()
JacobMoshenko8e905102011-06-20 09:53:10 -0400703 resp, content = http.request(self.token_uri, method='POST', body=body,
704 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500705 if resp.status == 200:
706 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
707 d = simplejson.loads(content)
708 access_token = d['access_token']
709 refresh_token = d.get('refresh_token', None)
710 token_expiry = None
711 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400712 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400713 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500714
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400715 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400716 return OAuth2Credentials(access_token, self.client_id,
717 self.client_secret, refresh_token, token_expiry,
718 self.token_uri, self.user_agent)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500719 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400720 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500721 error_msg = 'Invalid response %s.' % resp['status']
722 try:
723 d = simplejson.loads(content)
724 if 'error' in d:
725 error_msg = d['error']
726 except:
727 pass
728
729 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400730
731def flow_from_clientsecrets(filename, scope, message=None):
732 """Create a Flow from a clientsecrets file.
733
734 Will create the right kind of Flow based on the contents of the clientsecrets
735 file or will raise InvalidClientSecretsError for unknown types of Flows.
736
737 Args:
738 filename: string, File name of client secrets.
739 scope: string, Space separated list of scopes.
740 message: string, A friendly string to display to the user if the
741 clientsecrets file is missing or invalid. If message is provided then
742 sys.exit will be called in the case of an error. If message in not
743 provided then clientsecrets.InvalidClientSecretsError will be raised.
744
745 Returns:
746 A Flow object.
747
748 Raises:
749 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
750 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
751 invalid.
752 """
753 client_type, client_info = clientsecrets.loadfile(filename)
754 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
755 return OAuth2WebServerFlow(
756 client_info['client_id'],
757 client_info['client_secret'],
758 scope,
759 None, # user_agent
760 client_info['auth_uri'],
761 client_info['token_uri'])
762 else:
763 raise UnknownClientSecretsFlowError(
764 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)
765
766
767class OAuth2WebServerFlowFromClientSecrets(Flow):
768 """Does the Web Server Flow for OAuth 2.0.
769
770 """
771
772 def __init__(self, client_secrets, scope, user_agent,
773 auth_uri='https://accounts.google.com/o/oauth2/auth',
774 token_uri='https://accounts.google.com/o/oauth2/token',
775 **kwargs):
776 """Constructor for OAuth2WebServerFlow
777
778 Args:
779 client_id: string, client identifier.
780 client_secret: string client secret.
781 scope: string, scope of the credentials being requested.
782 user_agent: string, HTTP User-Agent to provide for this application.
783 auth_uri: string, URI for authorization endpoint. For convenience
784 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
785 token_uri: string, URI for token endpoint. For convenience
786 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
787 **kwargs: dict, The keyword arguments are all optional and required
788 parameters for the OAuth calls.
789 """
790 self.client_id = client_id
791 self.client_secret = client_secret
792 self.scope = scope
793 self.user_agent = user_agent
794 self.auth_uri = auth_uri
795 self.token_uri = token_uri
796 self.params = kwargs
797 self.redirect_uri = None