blob: d77e776bc4c0aee1ccea9da0e56cb3c691203c78 [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']
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400146 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400147 kls = getattr(m, data['_class'])
148 from_json = getattr(kls, 'from_json')
149 return from_json(s)
150
JacobMoshenko8e905102011-06-20 09:53:10 -0400151
Joe Gregorio695fdc12011-01-16 16:46:55 -0500152class Flow(object):
153 """Base class for all Flow objects."""
154 pass
155
156
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500157class Storage(object):
158 """Base class for all Storage objects.
159
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400160 Store and retrieve a single credential. This class supports locking
161 such that multiple processes and threads can operate on a single
162 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500163 """
164
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400165 def acquire_lock(self):
166 """Acquires any lock necessary to access this Storage.
167
168 This lock is not reentrant."""
169 pass
170
171 def release_lock(self):
172 """Release the Storage lock.
173
174 Trying to release a lock that isn't held will result in a
175 RuntimeError.
176 """
177 pass
178
179 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500180 """Retrieve credential.
181
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400182 The Storage lock must be held when this is called.
183
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500184 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400185 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500186 """
187 _abstract()
188
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400189 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500190 """Write a credential.
191
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400192 The Storage lock must be held when this is called.
193
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500194 Args:
195 credentials: Credentials, the credentials to store.
196 """
197 _abstract()
198
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400199 def get(self):
200 """Retrieve credential.
201
202 The Storage lock must *not* be held when this is called.
203
204 Returns:
205 oauth2client.client.Credentials
206 """
207 self.acquire_lock()
208 try:
209 return self.locked_get()
210 finally:
211 self.release_lock()
212
213 def put(self, credentials):
214 """Write a credential.
215
216 The Storage lock must be held when this is called.
217
218 Args:
219 credentials: Credentials, the credentials to store.
220 """
221 self.acquire_lock()
222 try:
223 self.locked_put(credentials)
224 finally:
225 self.release_lock()
226
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500227
Joe Gregorio695fdc12011-01-16 16:46:55 -0500228class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400229 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500230
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500231 Credentials can be applied to an httplib2.Http object using the authorize()
232 method, which then signs each request from that object with the OAuth 2.0
233 access token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500234
235 OAuth2Credentials objects may be safely pickled and unpickled.
236 """
237
238 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400239 token_expiry, token_uri, user_agent):
240 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500241
242 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500243 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500244
245 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400246 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500247 client_id: string, client identifier.
248 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500249 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400250 token_expiry: datetime, when the access_token expires.
251 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500252 user_agent: string, The HTTP User-Agent to provide for this application.
253
Joe Gregorio695fdc12011-01-16 16:46:55 -0500254 Notes:
255 store: callable, a callable that when passed a Credential
256 will store the credential back to where it came from.
257 This is needed to store the latest access_token if it
258 has expired and been refreshed.
259 """
260 self.access_token = access_token
261 self.client_id = client_id
262 self.client_secret = client_secret
263 self.refresh_token = refresh_token
264 self.store = None
265 self.token_expiry = token_expiry
266 self.token_uri = token_uri
267 self.user_agent = user_agent
268
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500269 # True if the credentials have been revoked or expired and can't be
270 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400271 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500272
Joe Gregorio562b7312011-09-15 09:06:38 -0400273 def to_json(self):
274 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
275
276 @classmethod
277 def from_json(cls, s):
278 """Instantiate a Credentials object from a JSON description of it. The JSON
279 should have been produced by calling .to_json() on the object.
280
281 Args:
282 data: dict, A deserialized JSON object.
283
284 Returns:
285 An instance of a Credentials subclass.
286 """
287 data = simplejson.loads(s)
288 if 'token_expiry' in data and not isinstance(data['token_expiry'],
289 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400290 try:
291 data['token_expiry'] = datetime.datetime.strptime(
292 data['token_expiry'], EXPIRY_FORMAT)
293 except:
294 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400295 retval = OAuth2Credentials(
296 data['access_token'],
297 data['client_id'],
298 data['client_secret'],
299 data['refresh_token'],
300 data['token_expiry'],
301 data['token_uri'],
302 data['user_agent'])
303 retval.invalid = data['invalid']
304 return retval
305
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500306 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400307 def access_token_expired(self):
308 """True if the credential is expired or invalid.
309
310 If the token_expiry isn't set, we assume the token doesn't expire.
311 """
312 if self.invalid:
313 return True
314
315 if not self.token_expiry:
316 return False
317
Joe Gregorio562b7312011-09-15 09:06:38 -0400318 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400319 if now >= self.token_expiry:
320 logger.info('access_token is expired. Now: %s, token_expiry: %s',
321 now, self.token_expiry)
322 return True
323 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500324
Joe Gregorio695fdc12011-01-16 16:46:55 -0500325 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400326 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500327
328 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400329 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500330 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400331 has expired and been refreshed. This implementation uses
332 locking to check for updates before updating the
333 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500334 """
335 self.store = store
336
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400337 def _updateFromCredential(self, other):
338 """Update this Credential from another instance."""
339 self.__dict__.update(other.__getstate__())
340
Joe Gregorio695fdc12011-01-16 16:46:55 -0500341 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400342 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500343 d = copy.copy(self.__dict__)
344 del d['store']
345 return d
346
347 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400348 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500349 self.__dict__.update(state)
350 self.store = None
351
JacobMoshenko8e905102011-06-20 09:53:10 -0400352 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400353 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400354 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400355 'grant_type': 'refresh_token',
356 'client_id': self.client_id,
357 'client_secret': self.client_secret,
358 'refresh_token': self.refresh_token,
359 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400360 return body
361
362 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400363 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400364 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400365 'content-type': 'application/x-www-form-urlencoded',
366 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400367
368 if self.user_agent is not None:
369 headers['user-agent'] = self.user_agent
370
JacobMoshenko8e905102011-06-20 09:53:10 -0400371 return headers
372
Joe Gregorio695fdc12011-01-16 16:46:55 -0500373 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400374 """Refreshes the access_token.
375
376 This method first checks by reading the Storage object if available.
377 If a refresh is still needed, it holds the Storage lock until the
378 refresh is completed.
379 """
380 if not self.store:
381 self._do_refresh_request(http_request)
382 else:
383 self.store.acquire_lock()
384 try:
385 new_cred = self.store.locked_get()
386 if (new_cred and not new_cred.invalid and
387 new_cred.access_token != self.access_token):
388 logger.info('Updated access_token read from Storage')
389 self._updateFromCredential(new_cred)
390 else:
391 self._do_refresh_request(http_request)
392 finally:
393 self.store.release_lock()
394
395 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500396 """Refresh the access_token using the refresh_token.
397
398 Args:
399 http: An instance of httplib2.Http.request
400 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400401
402 Raises:
403 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500404 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400405 body = self._generate_refresh_request_body()
406 headers = self._generate_refresh_request_headers()
407
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400408 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500409 resp, content = http_request(
410 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500411 if resp.status == 200:
412 # TODO(jcgregorio) Raise an error if loads fails?
413 d = simplejson.loads(content)
414 self.access_token = d['access_token']
415 self.refresh_token = d.get('refresh_token', self.refresh_token)
416 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500417 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400418 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500419 else:
420 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400421 if self.store:
422 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500423 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400424 # An {'error':...} response body means the token is expired or revoked,
425 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400426 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500427 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500428 try:
429 d = simplejson.loads(content)
430 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500431 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400432 self.invalid = True
433 if self.store:
434 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500435 except:
436 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500437 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500438
439 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500440 """Authorize an httplib2.Http instance with these credentials.
441
Joe Gregorio695fdc12011-01-16 16:46:55 -0500442 Args:
443 http: An instance of httplib2.Http
444 or something that acts like it.
445
446 Returns:
447 A modified instance of http that was passed in.
448
449 Example:
450
451 h = httplib2.Http()
452 h = credentials.authorize(h)
453
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400454 You can't create a new OAuth subclass of httplib2.Authenication
455 because it never gets passed the absolute URI, which is needed for
456 signing. So instead we have to overload 'request' with a closure
457 that adds in the Authorization header and then calls the original
458 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500459 """
460 request_orig = http.request
461
462 # The closure that will replace 'httplib2.Http.request'.
463 def new_request(uri, method='GET', body=None, headers=None,
464 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
465 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400466 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400467 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400468 self._refresh(request_orig)
469
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400470 # Modify the request headers to add the appropriate
471 # Authorization header.
472 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500473 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500474 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400475
476 if self.user_agent is not None:
477 if 'user-agent' in headers:
478 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
479 else:
480 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400481
Joe Gregorio695fdc12011-01-16 16:46:55 -0500482 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500483 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400484
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500485 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400486 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500487 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500488 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500489 return request_orig(uri, method, body, headers,
490 redirections, connection_type)
491 else:
492 return (resp, content)
493
494 http.request = new_request
495 return http
496
497
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500498class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400499 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500500
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400501 Credentials can be applied to an httplib2.Http object using the
502 authorize() method, which then signs each request from that object
503 with the OAuth 2.0 access token. This set of credentials is for the
504 use case where you have acquired an OAuth 2.0 access_token from
505 another place such as a JavaScript client or another web
506 application, and wish to use it from Python. Because only the
507 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500508 expire.
509
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500510 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500511
512 Usage:
513 credentials = AccessTokenCredentials('<an access token>',
514 'my-user-agent/1.0')
515 http = httplib2.Http()
516 http = credentials.authorize(http)
517
518 Exceptions:
519 AccessTokenCredentialsExpired: raised when the access_token expires or is
520 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500521 """
522
523 def __init__(self, access_token, user_agent):
524 """Create an instance of OAuth2Credentials
525
526 This is one of the few types if Credentials that you should contrust,
527 Credentials objects are usually instantiated by a Flow.
528
529 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000530 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500531 user_agent: string, The HTTP User-Agent to provide for this application.
532
533 Notes:
534 store: callable, a callable that when passed a Credential
535 will store the credential back to where it came from.
536 """
537 super(AccessTokenCredentials, self).__init__(
538 access_token,
539 None,
540 None,
541 None,
542 None,
543 None,
544 user_agent)
545
Joe Gregorio562b7312011-09-15 09:06:38 -0400546
547 @classmethod
548 def from_json(cls, s):
549 data = simplejson.loads(s)
550 retval = AccessTokenCredentials(
551 data['access_token'],
552 data['user_agent'])
553 return retval
554
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500555 def _refresh(self, http_request):
556 raise AccessTokenCredentialsError(
557 "The access_token is expired or invalid and can't be refreshed.")
558
JacobMoshenko8e905102011-06-20 09:53:10 -0400559
560class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400561 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400562
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400563 This credential does not require a flow to instantiate because it
564 represents a two legged flow, and therefore has all of the required
565 information to generate and refresh its own access tokens. It must
566 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400567
568 AssertionCredentials objects may be safely pickled and unpickled.
569 """
570
571 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400572 token_uri='https://accounts.google.com/o/oauth2/token',
573 **unused_kwargs):
574 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400575
576 Args:
577 assertion_type: string, assertion type that will be declared to the auth
578 server
579 user_agent: string, The HTTP User-Agent to provide for this application.
580 token_uri: string, URI for token endpoint. For convenience
581 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
582 """
583 super(AssertionCredentials, self).__init__(
584 None,
585 None,
586 None,
587 None,
588 None,
589 token_uri,
590 user_agent)
591 self.assertion_type = assertion_type
592
593 def _generate_refresh_request_body(self):
594 assertion = self._generate_assertion()
595
596 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400597 'assertion_type': self.assertion_type,
598 'assertion': assertion,
599 'grant_type': 'assertion',
600 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400601
602 return body
603
604 def _generate_assertion(self):
605 """Generate the assertion string that will be used in the access token
606 request.
607 """
608 _abstract()
609
610
Joe Gregorio695fdc12011-01-16 16:46:55 -0500611class OAuth2WebServerFlow(Flow):
612 """Does the Web Server Flow for OAuth 2.0.
613
614 OAuth2Credentials objects may be safely pickled and unpickled.
615 """
616
Joe Gregoriof08a4982011-10-07 13:11:16 -0400617 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400618 auth_uri='https://accounts.google.com/o/oauth2/auth',
619 token_uri='https://accounts.google.com/o/oauth2/token',
620 **kwargs):
621 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500622
623 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500624 client_id: string, client identifier.
625 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400626 scope: string or list of strings, scope(s) of the credentials being
627 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500628 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500629 auth_uri: string, URI for authorization endpoint. For convenience
630 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
631 token_uri: string, URI for token endpoint. For convenience
632 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500633 **kwargs: dict, The keyword arguments are all optional and required
634 parameters for the OAuth calls.
635 """
636 self.client_id = client_id
637 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400638 if type(scope) is list:
639 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500640 self.scope = scope
641 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500642 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500643 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400644 self.params = {
645 'access_type': 'offline',
646 }
647 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500648 self.redirect_uri = None
649
650 def step1_get_authorize_url(self, redirect_uri='oob'):
651 """Returns a URI to redirect to the provider.
652
653 Args:
654 redirect_uri: string, Either the string 'oob' for a non-web-based
655 application, or a URI that handles the callback from
656 the authorization server.
657
658 If redirect_uri is 'oob' then pass in the
659 generated verification code to step2_exchange,
660 otherwise pass in the query parameters received
661 at the callback uri to step2_exchange.
662 """
663
664 self.redirect_uri = redirect_uri
665 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400666 'response_type': 'code',
667 'client_id': self.client_id,
668 'redirect_uri': redirect_uri,
669 'scope': self.scope,
670 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500671 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500672 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500673 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
674 parts[4] = urllib.urlencode(query)
675 return urlparse.urlunparse(parts)
676
Joe Gregorioccc79542011-02-19 00:05:26 -0500677 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500678 """Exhanges a code for OAuth2Credentials.
679
680 Args:
681 code: string or dict, either the code as a string, or a dictionary
682 of the query parameters to the redirect_uri, which contains
683 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500684 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500685 """
686
687 if not (isinstance(code, str) or isinstance(code, unicode)):
688 code = code['code']
689
690 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400691 'grant_type': 'authorization_code',
692 'client_id': self.client_id,
693 'client_secret': self.client_secret,
694 'code': code,
695 'redirect_uri': self.redirect_uri,
696 'scope': self.scope,
697 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500698 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400699 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500700 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400701
702 if self.user_agent is not None:
703 headers['user-agent'] = self.user_agent
704
Joe Gregorioccc79542011-02-19 00:05:26 -0500705 if http is None:
706 http = httplib2.Http()
JacobMoshenko8e905102011-06-20 09:53:10 -0400707 resp, content = http.request(self.token_uri, method='POST', body=body,
708 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500709 if resp.status == 200:
710 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
711 d = simplejson.loads(content)
712 access_token = d['access_token']
713 refresh_token = d.get('refresh_token', None)
714 token_expiry = None
715 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400716 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400717 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500718
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400719 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400720 return OAuth2Credentials(access_token, self.client_id,
721 self.client_secret, refresh_token, token_expiry,
722 self.token_uri, self.user_agent)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500723 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400724 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500725 error_msg = 'Invalid response %s.' % resp['status']
726 try:
727 d = simplejson.loads(content)
728 if 'error' in d:
729 error_msg = d['error']
730 except:
731 pass
732
733 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400734
735def flow_from_clientsecrets(filename, scope, message=None):
736 """Create a Flow from a clientsecrets file.
737
738 Will create the right kind of Flow based on the contents of the clientsecrets
739 file or will raise InvalidClientSecretsError for unknown types of Flows.
740
741 Args:
742 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400743 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400744 message: string, A friendly string to display to the user if the
745 clientsecrets file is missing or invalid. If message is provided then
746 sys.exit will be called in the case of an error. If message in not
747 provided then clientsecrets.InvalidClientSecretsError will be raised.
748
749 Returns:
750 A Flow object.
751
752 Raises:
753 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
754 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
755 invalid.
756 """
Joe Gregorio0984ef22011-10-14 13:17:43 -0400757 try:
758 client_type, client_info = clientsecrets.loadfile(filename)
759 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
760 return OAuth2WebServerFlow(
761 client_info['client_id'],
762 client_info['client_secret'],
763 scope,
764 None, # user_agent
765 client_info['auth_uri'],
766 client_info['token_uri'])
767 except clientsecrets.InvalidClientSecretsError:
768 if message:
769 sys.exit(message)
770 else:
771 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -0400772 else:
773 raise UnknownClientSecretsFlowError(
774 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)