blob: e8e900102782d84ce2a1dc2b5d3ba4b4c838873b [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 Gregorio8b4c1732011-12-06 11:28:29 -050022import base64
Joe Gregoriof08a4982011-10-07 13:11:16 -040023import clientsecrets
Joe Gregorio695fdc12011-01-16 16:46:55 -050024import copy
25import datetime
26import httplib2
27import logging
Joe Gregorio8b4c1732011-12-06 11:28:29 -050028import os
Joe Gregoriof08a4982011-10-07 13:11:16 -040029import sys
Joe Gregorio8b4c1732011-12-06 11:28:29 -050030import time
Joe Gregorio695fdc12011-01-16 16:46:55 -050031import urllib
32import urlparse
33
Joe Gregorio549230c2012-01-11 10:38:05 -050034from anyjson import simplejson
Joe Gregorio8b4c1732011-12-06 11:28:29 -050035
36HAS_OPENSSL = False
37try:
38 from oauth2client.crypt import Signer
39 from oauth2client.crypt import make_signed_jwt
40 from oauth2client.crypt import verify_signed_jwt_with_certs
41 HAS_OPENSSL = True
42except ImportError:
43 pass
44
Joe Gregorio695fdc12011-01-16 16:46:55 -050045try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040046 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050047except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040048 from cgi import parse_qsl
49
50logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050051
Joe Gregorio562b7312011-09-15 09:06:38 -040052# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050053EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
54
55# Which certs to use to validate id_tokens received.
56ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040057
Joe Gregoriof2326c02012-02-09 12:18:44 -050058# Constant to use for the out of band OAuth 2.0 flow.
59OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
60
Joe Gregorio695fdc12011-01-16 16:46:55 -050061
62class Error(Exception):
63 """Base error for this module."""
64 pass
65
66
Joe Gregorioccc79542011-02-19 00:05:26 -050067class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050068 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050069 pass
70
71
72class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050073 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050074 pass
75
Joe Gregoriof08a4982011-10-07 13:11:16 -040076class UnknownClientSecretsFlowError(Error):
77 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
78 pass
79
Joe Gregorio695fdc12011-01-16 16:46:55 -050080
Joe Gregorio3b79fa82011-02-17 11:47:17 -050081class AccessTokenCredentialsError(Error):
82 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050083 pass
84
85
Joe Gregorio8b4c1732011-12-06 11:28:29 -050086class VerifyJwtTokenError(Error):
87 """Could on retrieve certificates for validation."""
88 pass
89
90
Joe Gregorio695fdc12011-01-16 16:46:55 -050091def _abstract():
92 raise NotImplementedError('You need to override this function')
93
94
Joe Gregorio9f2f38f2012-02-06 12:53:00 -050095class MemoryCache(object):
96 """httplib2 Cache implementation which only caches locally."""
97
98 def __init__(self):
99 self.cache = {}
100
101 def get(self, key):
102 return self.cache.get(key)
103
104 def set(self, key, value):
105 self.cache[key] = value
106
107 def delete(self, key):
108 self.cache.pop(key, None)
109
110
Joe Gregorio695fdc12011-01-16 16:46:55 -0500111class Credentials(object):
112 """Base class for all Credentials objects.
113
Joe Gregorio562b7312011-09-15 09:06:38 -0400114 Subclasses must define an authorize() method that applies the credentials to
115 an HTTP transport.
116
117 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500118 string as input and returns an instaniated Credentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500119 """
120
Joe Gregorio562b7312011-09-15 09:06:38 -0400121 NON_SERIALIZED_MEMBERS = ['store']
122
Joe Gregorio695fdc12011-01-16 16:46:55 -0500123 def authorize(self, http):
124 """Take an httplib2.Http instance (or equivalent) and
125 authorizes it for the set of credentials, usually by
126 replacing http.request() with a method that adds in
127 the appropriate headers and then delegates to the original
128 Http.request() method.
129 """
130 _abstract()
131
Joe Gregorio654f4a22012-02-09 14:15:44 -0500132 def refresh(self, http):
133 """Forces a refresh of the access_token.
134
135 Args:
136 http: httplib2.Http, an http object to be used to make the refresh
137 request.
138 """
139 _abstract()
140
141 def apply(self, headers):
142 """Add the authorization to the headers.
143
144 Args:
145 headers: dict, the headers to add the Authorization header to.
146 """
147 _abstract()
148
Joe Gregorio562b7312011-09-15 09:06:38 -0400149 def _to_json(self, strip):
150 """Utility function for creating a JSON representation of an instance of Credentials.
151
152 Args:
153 strip: array, An array of names of members to not include in the JSON.
154
155 Returns:
156 string, a JSON representation of this instance, suitable to pass to
157 from_json().
158 """
159 t = type(self)
160 d = copy.copy(self.__dict__)
161 for member in strip:
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400162 if member in d:
163 del d[member]
Joe Gregorio562b7312011-09-15 09:06:38 -0400164 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
165 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
166 # Add in information we will need later to reconsistitue this instance.
167 d['_class'] = t.__name__
168 d['_module'] = t.__module__
169 return simplejson.dumps(d)
170
171 def to_json(self):
172 """Creating a JSON representation of an instance of Credentials.
173
174 Returns:
175 string, a JSON representation of this instance, suitable to pass to
176 from_json().
177 """
178 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
179
180 @classmethod
181 def new_from_json(cls, s):
182 """Utility class method to instantiate a Credentials subclass from a JSON
183 representation produced by to_json().
184
185 Args:
186 s: string, JSON from to_json().
187
188 Returns:
189 An instance of the subclass of Credentials that was serialized with
190 to_json().
191 """
192 data = simplejson.loads(s)
193 # Find and call the right classmethod from_json() to restore the object.
194 module = data['_module']
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500195 try:
196 m = __import__(module)
197 except ImportError:
198 # In case there's an object from the old package structure, update it
199 module = module.replace('.apiclient', '')
200 m = __import__(module)
201
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400202 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400203 kls = getattr(m, data['_class'])
204 from_json = getattr(kls, 'from_json')
205 return from_json(s)
206
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400207 @classmethod
208 def from_json(cls, s):
Joe Gregorio401b8422012-05-03 16:35:35 -0400209 """Instantiate a Credentials object from a JSON description of it.
210
211 The JSON should have been produced by calling .to_json() on the object.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400212
213 Args:
214 data: dict, A deserialized JSON object.
215
216 Returns:
217 An instance of a Credentials subclass.
218 """
219 return Credentials()
220
JacobMoshenko8e905102011-06-20 09:53:10 -0400221
Joe Gregorio695fdc12011-01-16 16:46:55 -0500222class Flow(object):
223 """Base class for all Flow objects."""
224 pass
225
226
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500227class Storage(object):
228 """Base class for all Storage objects.
229
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400230 Store and retrieve a single credential. This class supports locking
231 such that multiple processes and threads can operate on a single
232 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500233 """
234
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400235 def acquire_lock(self):
236 """Acquires any lock necessary to access this Storage.
237
Joe Gregorio401b8422012-05-03 16:35:35 -0400238 This lock is not reentrant.
239 """
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400240 pass
241
242 def release_lock(self):
243 """Release the Storage lock.
244
245 Trying to release a lock that isn't held will result in a
246 RuntimeError.
247 """
248 pass
249
250 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500251 """Retrieve credential.
252
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400253 The Storage lock must be held when this is called.
254
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500255 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400256 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500257 """
258 _abstract()
259
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400260 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500261 """Write a credential.
262
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400263 The Storage lock must be held when this is called.
264
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500265 Args:
266 credentials: Credentials, the credentials to store.
267 """
268 _abstract()
269
Joe Gregorioec75dc12012-02-06 13:40:42 -0500270 def locked_delete(self):
271 """Delete a credential.
272
273 The Storage lock must be held when this is called.
274 """
275 _abstract()
276
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400277 def get(self):
278 """Retrieve credential.
279
280 The Storage lock must *not* be held when this is called.
281
282 Returns:
283 oauth2client.client.Credentials
284 """
285 self.acquire_lock()
286 try:
287 return self.locked_get()
288 finally:
289 self.release_lock()
290
291 def put(self, credentials):
292 """Write a credential.
293
294 The Storage lock must be held when this is called.
295
296 Args:
297 credentials: Credentials, the credentials to store.
298 """
299 self.acquire_lock()
300 try:
301 self.locked_put(credentials)
302 finally:
303 self.release_lock()
304
Joe Gregorioec75dc12012-02-06 13:40:42 -0500305 def delete(self):
306 """Delete credential.
307
308 Frees any resources associated with storing the credential.
309 The Storage lock must *not* be held when this is called.
310
311 Returns:
312 None
313 """
314 self.acquire_lock()
315 try:
316 return self.locked_delete()
317 finally:
318 self.release_lock()
319
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500320
Joe Gregorio695fdc12011-01-16 16:46:55 -0500321class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400322 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500323
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500324 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500325 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500326
327 OAuth2Credentials objects may be safely pickled and unpickled.
328 """
329
330 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500331 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400332 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500333
334 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500335 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500336
337 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400338 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500339 client_id: string, client identifier.
340 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500341 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400342 token_expiry: datetime, when the access_token expires.
343 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500344 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500345 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500346
Joe Gregorio695fdc12011-01-16 16:46:55 -0500347 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500348 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500349 will store the credential back to where it came from.
350 This is needed to store the latest access_token if it
351 has expired and been refreshed.
352 """
353 self.access_token = access_token
354 self.client_id = client_id
355 self.client_secret = client_secret
356 self.refresh_token = refresh_token
357 self.store = None
358 self.token_expiry = token_expiry
359 self.token_uri = token_uri
360 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500361 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500362
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500363 # True if the credentials have been revoked or expired and can't be
364 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400365 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500366
Joe Gregorio654f4a22012-02-09 14:15:44 -0500367 def authorize(self, http):
368 """Authorize an httplib2.Http instance with these credentials.
369
370 The modified http.request method will add authentication headers to each
371 request and will refresh access_tokens when a 401 is received on a
372 request. In addition the http.request method has a credentials property,
373 http.request.credentials, which is the Credentials object that authorized
374 it.
375
376 Args:
377 http: An instance of httplib2.Http
378 or something that acts like it.
379
380 Returns:
381 A modified instance of http that was passed in.
382
383 Example:
384
385 h = httplib2.Http()
386 h = credentials.authorize(h)
387
388 You can't create a new OAuth subclass of httplib2.Authenication
389 because it never gets passed the absolute URI, which is needed for
390 signing. So instead we have to overload 'request' with a closure
391 that adds in the Authorization header and then calls the original
392 version of 'request()'.
393 """
394 request_orig = http.request
395
396 # The closure that will replace 'httplib2.Http.request'.
397 def new_request(uri, method='GET', body=None, headers=None,
398 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
399 connection_type=None):
400 if not self.access_token:
401 logger.info('Attempting refresh to obtain initial access_token')
402 self._refresh(request_orig)
403
404 # Modify the request headers to add the appropriate
405 # Authorization header.
406 if headers is None:
407 headers = {}
408 self.apply(headers)
409
410 if self.user_agent is not None:
411 if 'user-agent' in headers:
412 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
413 else:
414 headers['user-agent'] = self.user_agent
415
416 resp, content = request_orig(uri, method, body, headers,
417 redirections, connection_type)
418
419 if resp.status == 401:
420 logger.info('Refreshing due to a 401')
421 self._refresh(request_orig)
422 self.apply(headers)
423 return request_orig(uri, method, body, headers,
424 redirections, connection_type)
425 else:
426 return (resp, content)
427
428 # Replace the request method with our own closure.
429 http.request = new_request
430
431 # Set credentials as a property of the request method.
432 setattr(http.request, 'credentials', self)
433
434 return http
435
436 def refresh(self, http):
437 """Forces a refresh of the access_token.
438
439 Args:
440 http: httplib2.Http, an http object to be used to make the refresh
441 request.
442 """
443 self._refresh(http.request)
444
445 def apply(self, headers):
446 """Add the authorization to the headers.
447
448 Args:
449 headers: dict, the headers to add the Authorization header to.
450 """
451 headers['Authorization'] = 'Bearer ' + self.access_token
452
Joe Gregorio562b7312011-09-15 09:06:38 -0400453 def to_json(self):
454 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
455
456 @classmethod
457 def from_json(cls, s):
458 """Instantiate a Credentials object from a JSON description of it. The JSON
459 should have been produced by calling .to_json() on the object.
460
461 Args:
462 data: dict, A deserialized JSON object.
463
464 Returns:
465 An instance of a Credentials subclass.
466 """
467 data = simplejson.loads(s)
468 if 'token_expiry' in data and not isinstance(data['token_expiry'],
469 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400470 try:
471 data['token_expiry'] = datetime.datetime.strptime(
472 data['token_expiry'], EXPIRY_FORMAT)
473 except:
474 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400475 retval = OAuth2Credentials(
476 data['access_token'],
477 data['client_id'],
478 data['client_secret'],
479 data['refresh_token'],
480 data['token_expiry'],
481 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500482 data['user_agent'],
483 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400484 retval.invalid = data['invalid']
485 return retval
486
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500487 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400488 def access_token_expired(self):
489 """True if the credential is expired or invalid.
490
491 If the token_expiry isn't set, we assume the token doesn't expire.
492 """
493 if self.invalid:
494 return True
495
496 if not self.token_expiry:
497 return False
498
Joe Gregorio562b7312011-09-15 09:06:38 -0400499 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400500 if now >= self.token_expiry:
501 logger.info('access_token is expired. Now: %s, token_expiry: %s',
502 now, self.token_expiry)
503 return True
504 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500505
Joe Gregorio695fdc12011-01-16 16:46:55 -0500506 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400507 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500508
509 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400510 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500511 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400512 has expired and been refreshed. This implementation uses
513 locking to check for updates before updating the
514 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500515 """
516 self.store = store
517
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400518 def _updateFromCredential(self, other):
519 """Update this Credential from another instance."""
520 self.__dict__.update(other.__getstate__())
521
Joe Gregorio695fdc12011-01-16 16:46:55 -0500522 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400523 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500524 d = copy.copy(self.__dict__)
525 del d['store']
526 return d
527
528 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400529 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500530 self.__dict__.update(state)
531 self.store = None
532
JacobMoshenko8e905102011-06-20 09:53:10 -0400533 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400534 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400535 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400536 'grant_type': 'refresh_token',
537 'client_id': self.client_id,
538 'client_secret': self.client_secret,
539 'refresh_token': self.refresh_token,
540 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400541 return body
542
543 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400544 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400545 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400546 'content-type': 'application/x-www-form-urlencoded',
547 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400548
549 if self.user_agent is not None:
550 headers['user-agent'] = self.user_agent
551
JacobMoshenko8e905102011-06-20 09:53:10 -0400552 return headers
553
Joe Gregorio695fdc12011-01-16 16:46:55 -0500554 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400555 """Refreshes the access_token.
556
557 This method first checks by reading the Storage object if available.
558 If a refresh is still needed, it holds the Storage lock until the
559 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500560
561 Args:
562 http_request: callable, a callable that matches the method signature of
563 httplib2.Http.request, used to make the refresh request.
564
565 Raises:
566 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400567 """
568 if not self.store:
569 self._do_refresh_request(http_request)
570 else:
571 self.store.acquire_lock()
572 try:
573 new_cred = self.store.locked_get()
574 if (new_cred and not new_cred.invalid and
575 new_cred.access_token != self.access_token):
576 logger.info('Updated access_token read from Storage')
577 self._updateFromCredential(new_cred)
578 else:
579 self._do_refresh_request(http_request)
580 finally:
581 self.store.release_lock()
582
583 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500584 """Refresh the access_token using the refresh_token.
585
586 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500587 http_request: callable, a callable that matches the method signature of
588 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400589
590 Raises:
591 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500592 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400593 body = self._generate_refresh_request_body()
594 headers = self._generate_refresh_request_headers()
595
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400596 logger.info('Refreshing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500597 resp, content = http_request(
598 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500599 if resp.status == 200:
600 # TODO(jcgregorio) Raise an error if loads fails?
601 d = simplejson.loads(content)
602 self.access_token = d['access_token']
603 self.refresh_token = d.get('refresh_token', self.refresh_token)
604 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500605 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400606 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500607 else:
608 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400609 if self.store:
610 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500611 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400612 # An {'error':...} response body means the token is expired or revoked,
613 # so we flag the credentials as such.
Joe Gregorioe78621a2012-03-09 15:47:23 -0500614 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500615 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500616 try:
617 d = simplejson.loads(content)
618 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500619 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400620 self.invalid = True
621 if self.store:
622 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500623 except:
624 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500625 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500626
Joe Gregorio695fdc12011-01-16 16:46:55 -0500627
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500628class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400629 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500630
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400631 Credentials can be applied to an httplib2.Http object using the
632 authorize() method, which then signs each request from that object
633 with the OAuth 2.0 access token. This set of credentials is for the
634 use case where you have acquired an OAuth 2.0 access_token from
635 another place such as a JavaScript client or another web
636 application, and wish to use it from Python. Because only the
637 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500638 expire.
639
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500640 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500641
642 Usage:
643 credentials = AccessTokenCredentials('<an access token>',
644 'my-user-agent/1.0')
645 http = httplib2.Http()
646 http = credentials.authorize(http)
647
648 Exceptions:
649 AccessTokenCredentialsExpired: raised when the access_token expires or is
650 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500651 """
652
653 def __init__(self, access_token, user_agent):
654 """Create an instance of OAuth2Credentials
655
656 This is one of the few types if Credentials that you should contrust,
657 Credentials objects are usually instantiated by a Flow.
658
659 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000660 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500661 user_agent: string, The HTTP User-Agent to provide for this application.
662
663 Notes:
664 store: callable, a callable that when passed a Credential
665 will store the credential back to where it came from.
666 """
667 super(AccessTokenCredentials, self).__init__(
668 access_token,
669 None,
670 None,
671 None,
672 None,
673 None,
674 user_agent)
675
Joe Gregorio562b7312011-09-15 09:06:38 -0400676
677 @classmethod
678 def from_json(cls, s):
679 data = simplejson.loads(s)
680 retval = AccessTokenCredentials(
681 data['access_token'],
682 data['user_agent'])
683 return retval
684
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500685 def _refresh(self, http_request):
686 raise AccessTokenCredentialsError(
687 "The access_token is expired or invalid and can't be refreshed.")
688
JacobMoshenko8e905102011-06-20 09:53:10 -0400689
690class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400691 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400692
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400693 This credential does not require a flow to instantiate because it
694 represents a two legged flow, and therefore has all of the required
695 information to generate and refresh its own access tokens. It must
696 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400697
698 AssertionCredentials objects may be safely pickled and unpickled.
699 """
700
701 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400702 token_uri='https://accounts.google.com/o/oauth2/token',
703 **unused_kwargs):
704 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400705
706 Args:
707 assertion_type: string, assertion type that will be declared to the auth
708 server
709 user_agent: string, The HTTP User-Agent to provide for this application.
710 token_uri: string, URI for token endpoint. For convenience
711 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
712 """
713 super(AssertionCredentials, self).__init__(
714 None,
715 None,
716 None,
717 None,
718 None,
719 token_uri,
720 user_agent)
721 self.assertion_type = assertion_type
722
723 def _generate_refresh_request_body(self):
724 assertion = self._generate_assertion()
725
726 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400727 'assertion_type': self.assertion_type,
728 'assertion': assertion,
729 'grant_type': 'assertion',
730 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400731
732 return body
733
734 def _generate_assertion(self):
735 """Generate the assertion string that will be used in the access token
736 request.
737 """
738 _abstract()
739
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500740if HAS_OPENSSL:
741 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
742 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
743 # method.
744
745 class SignedJwtAssertionCredentials(AssertionCredentials):
746 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
747
Joe Gregorio672051e2012-07-10 09:11:45 -0400748 This credential does not require a flow to instantiate because it represents
749 a two legged flow, and therefore has all of the required information to
750 generate and refresh its own access tokens.
751
752 SignedJwtAssertionCredentials requires PyOpenSSL and because of that it does
753 not work on App Engine. For App Engine you may consider using
754 AppAssertionCredentials.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500755 """
756
757 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
758
759 def __init__(self,
760 service_account_name,
761 private_key,
762 scope,
763 private_key_password='notasecret',
764 user_agent=None,
765 token_uri='https://accounts.google.com/o/oauth2/token',
766 **kwargs):
767 """Constructor for SignedJwtAssertionCredentials.
768
769 Args:
770 service_account_name: string, id for account, usually an email address.
771 private_key: string, private key in P12 format.
772 scope: string or list of strings, scope(s) of the credentials being
773 requested.
774 private_key_password: string, password for private_key.
775 user_agent: string, HTTP User-Agent to provide for this application.
776 token_uri: string, URI for token endpoint. For convenience
777 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
778 kwargs: kwargs, Additional parameters to add to the JWT token, for
779 example prn=joe@xample.org."""
780
781 super(SignedJwtAssertionCredentials, self).__init__(
782 'http://oauth.net/grant_type/jwt/1.0/bearer',
783 user_agent,
784 token_uri=token_uri,
785 )
786
787 if type(scope) is list:
788 scope = ' '.join(scope)
789 self.scope = scope
790
791 self.private_key = private_key
792 self.private_key_password = private_key_password
793 self.service_account_name = service_account_name
794 self.kwargs = kwargs
795
796 @classmethod
797 def from_json(cls, s):
798 data = simplejson.loads(s)
799 retval = SignedJwtAssertionCredentials(
800 data['service_account_name'],
801 data['private_key'],
802 data['private_key_password'],
803 data['scope'],
804 data['user_agent'],
805 data['token_uri'],
806 data['kwargs']
807 )
808 retval.invalid = data['invalid']
809 return retval
810
811 def _generate_assertion(self):
812 """Generate the assertion that will be used in the request."""
813 now = long(time.time())
814 payload = {
815 'aud': self.token_uri,
816 'scope': self.scope,
817 'iat': now,
818 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
819 'iss': self.service_account_name
820 }
821 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500822 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500823
824 return make_signed_jwt(
825 Signer.from_string(self.private_key, self.private_key_password),
826 payload)
827
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500828 # Only used in verify_id_token(), which is always calling to the same URI
829 # for the certs.
830 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500831
832 def verify_id_token(id_token, audience, http=None,
833 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
834 """Verifies a signed JWT id_token.
835
Joe Gregorio672051e2012-07-10 09:11:45 -0400836 This function requires PyOpenSSL and because of that it does not work on
837 App Engine. For App Engine you may consider using AppAssertionCredentials.
838
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500839 Args:
840 id_token: string, A Signed JWT.
841 audience: string, The audience 'aud' that the token should be for.
842 http: httplib2.Http, instance to use to make the HTTP request. Callers
843 should supply an instance that has caching enabled.
844 cert_uri: string, URI of the certificates in JSON format to
845 verify the JWT against.
846
847 Returns:
848 The deserialized JSON in the JWT.
849
850 Raises:
851 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
852 """
853 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500854 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500855
856 resp, content = http.request(cert_uri)
857
858 if resp.status == 200:
859 certs = simplejson.loads(content)
860 return verify_signed_jwt_with_certs(id_token, certs, audience)
861 else:
862 raise VerifyJwtTokenError('Status code: %d' % resp.status)
863
864
865def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500866 # Guard against unicode strings, which base64 can't handle.
867 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500868 padded = b64string + '=' * (4 - len(b64string) % 4)
869 return base64.urlsafe_b64decode(padded)
870
871
872def _extract_id_token(id_token):
873 """Extract the JSON payload from a JWT.
874
875 Does the extraction w/o checking the signature.
876
877 Args:
878 id_token: string, OAuth 2.0 id_token.
879
880 Returns:
881 object, The deserialized JSON payload.
882 """
883 segments = id_token.split('.')
884
885 if (len(segments) != 3):
886 raise VerifyJwtTokenError(
887 'Wrong number of segments in token: %s' % id_token)
888
889 return simplejson.loads(_urlsafe_b64decode(segments[1]))
890
Joe Gregorio32d852d2012-06-14 09:08:18 -0400891def credentials_from_code(client_id, client_secret, scope, code,
892 redirect_uri = 'postmessage',
893 http=None, user_agent=None,
894 token_uri='https://accounts.google.com/o/oauth2/token'):
895 """Exchanges an authorization code for an OAuth2Credentials object.
896
897 Args:
898 client_id: string, client identifier.
899 client_secret: string, client secret.
900 scope: string or list of strings, scope(s) to request.
901 code: string, An authroization code, most likely passed down from
902 the client
903 redirect_uri: string, this is generally set to 'postmessage' to match the
904 redirect_uri that the client specified
905 http: httplib2.Http, optional http instance to use to do the fetch
906 token_uri: string, URI for token endpoint. For convenience
907 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
908 Returns:
909 An OAuth2Credentials object.
910
911 Raises:
912 FlowExchangeError if the authorization code cannot be exchanged for an
913 access token
914 """
915 flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
916 'https://accounts.google.com/o/oauth2/auth',
917 token_uri)
918
919 # We primarily make this call to set up the redirect_uri in the flow object
920 uriThatWeDontReallyUse = flow.step1_get_authorize_url(redirect_uri)
921 credentials = flow.step2_exchange(code, http)
922 return credentials
923
924
925def credentials_from_clientsecrets_and_code(filename, scope, code,
926 message = None,
927 redirect_uri = 'postmessage',
928 http=None):
929 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
930
931 Will create the right kind of Flow based on the contents of the clientsecrets
932 file or will raise InvalidClientSecretsError for unknown types of Flows.
933
934 Args:
935 filename: string, File name of clientsecrets.
936 scope: string or list of strings, scope(s) to request.
937 code: string, An authroization code, most likely passed down from
938 the client
939 message: string, A friendly string to display to the user if the
940 clientsecrets file is missing or invalid. If message is provided then
941 sys.exit will be called in the case of an error. If message in not
942 provided then clientsecrets.InvalidClientSecretsError will be raised.
943 redirect_uri: string, this is generally set to 'postmessage' to match the
944 redirect_uri that the client specified
945 http: httplib2.Http, optional http instance to use to do the fetch
946
947 Returns:
948 An OAuth2Credentials object.
949
950 Raises:
951 FlowExchangeError if the authorization code cannot be exchanged for an
952 access token
953 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
954 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
955 invalid.
956 """
957 flow = flow_from_clientsecrets(filename, scope, message)
958 # We primarily make this call to set up the redirect_uri in the flow object
959 uriThatWeDontReallyUse = flow.step1_get_authorize_url(redirect_uri)
960 credentials = flow.step2_exchange(code, http)
961 return credentials
962
JacobMoshenko8e905102011-06-20 09:53:10 -0400963
Joe Gregorio695fdc12011-01-16 16:46:55 -0500964class OAuth2WebServerFlow(Flow):
965 """Does the Web Server Flow for OAuth 2.0.
966
967 OAuth2Credentials objects may be safely pickled and unpickled.
968 """
969
Joe Gregoriof08a4982011-10-07 13:11:16 -0400970 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400971 auth_uri='https://accounts.google.com/o/oauth2/auth',
972 token_uri='https://accounts.google.com/o/oauth2/token',
973 **kwargs):
974 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500975
976 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500977 client_id: string, client identifier.
978 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400979 scope: string or list of strings, scope(s) of the credentials being
980 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500981 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500982 auth_uri: string, URI for authorization endpoint. For convenience
983 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
984 token_uri: string, URI for token endpoint. For convenience
985 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500986 **kwargs: dict, The keyword arguments are all optional and required
987 parameters for the OAuth calls.
988 """
989 self.client_id = client_id
990 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400991 if type(scope) is list:
992 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500993 self.scope = scope
994 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500995 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500996 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400997 self.params = {
998 'access_type': 'offline',
999 }
1000 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001001 self.redirect_uri = None
1002
Joe Gregoriof2326c02012-02-09 12:18:44 -05001003 def step1_get_authorize_url(self, redirect_uri=OOB_CALLBACK_URN):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001004 """Returns a URI to redirect to the provider.
1005
1006 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -05001007 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1008 a non-web-based application, or a URI that handles the callback from
1009 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001010
Joe Gregoriof2326c02012-02-09 12:18:44 -05001011 If redirect_uri is 'urn:ietf:wg:oauth:2.0:oob' then pass in the
Joe Gregorio695fdc12011-01-16 16:46:55 -05001012 generated verification code to step2_exchange,
1013 otherwise pass in the query parameters received
1014 at the callback uri to step2_exchange.
1015 """
1016
1017 self.redirect_uri = redirect_uri
1018 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001019 'response_type': 'code',
1020 'client_id': self.client_id,
1021 'redirect_uri': redirect_uri,
1022 'scope': self.scope,
1023 }
Joe Gregorio695fdc12011-01-16 16:46:55 -05001024 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001025 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001026 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
1027 parts[4] = urllib.urlencode(query)
1028 return urlparse.urlunparse(parts)
1029
Joe Gregorioccc79542011-02-19 00:05:26 -05001030 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001031 """Exhanges a code for OAuth2Credentials.
1032
1033 Args:
1034 code: string or dict, either the code as a string, or a dictionary
1035 of the query parameters to the redirect_uri, which contains
1036 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -05001037 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001038
1039 Returns:
1040 An OAuth2Credentials object that can be used to authorize requests.
1041
1042 Raises:
1043 FlowExchangeError if a problem occured exchanging the code for a
1044 refresh_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001045 """
1046
1047 if not (isinstance(code, str) or isinstance(code, unicode)):
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001048 if 'code' not in code:
1049 if 'error' in code:
1050 error_msg = code['error']
1051 else:
1052 error_msg = 'No code was supplied in the query parameters.'
1053 raise FlowExchangeError(error_msg)
1054 else:
1055 code = code['code']
Joe Gregorio695fdc12011-01-16 16:46:55 -05001056
1057 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001058 'grant_type': 'authorization_code',
1059 'client_id': self.client_id,
1060 'client_secret': self.client_secret,
1061 'code': code,
1062 'redirect_uri': self.redirect_uri,
1063 'scope': self.scope,
1064 })
Joe Gregorio695fdc12011-01-16 16:46:55 -05001065 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001066 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -05001067 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -04001068
1069 if self.user_agent is not None:
1070 headers['user-agent'] = self.user_agent
1071
Joe Gregorioccc79542011-02-19 00:05:26 -05001072 if http is None:
1073 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001074
JacobMoshenko8e905102011-06-20 09:53:10 -04001075 resp, content = http.request(self.token_uri, method='POST', body=body,
1076 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001077 if resp.status == 200:
1078 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
1079 d = simplejson.loads(content)
1080 access_token = d['access_token']
1081 refresh_token = d.get('refresh_token', None)
1082 token_expiry = None
1083 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -04001084 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -04001085 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001086
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001087 if 'id_token' in d:
1088 d['id_token'] = _extract_id_token(d['id_token'])
1089
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001090 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -04001091 return OAuth2Credentials(access_token, self.client_id,
1092 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001093 self.token_uri, self.user_agent,
1094 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001095 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -05001096 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -05001097 error_msg = 'Invalid response %s.' % resp['status']
1098 try:
1099 d = simplejson.loads(content)
1100 if 'error' in d:
1101 error_msg = d['error']
1102 except:
1103 pass
1104
1105 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -04001106
1107def flow_from_clientsecrets(filename, scope, message=None):
1108 """Create a Flow from a clientsecrets file.
1109
1110 Will create the right kind of Flow based on the contents of the clientsecrets
1111 file or will raise InvalidClientSecretsError for unknown types of Flows.
1112
1113 Args:
1114 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001115 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001116 message: string, A friendly string to display to the user if the
1117 clientsecrets file is missing or invalid. If message is provided then
1118 sys.exit will be called in the case of an error. If message in not
1119 provided then clientsecrets.InvalidClientSecretsError will be raised.
1120
1121 Returns:
1122 A Flow object.
1123
1124 Raises:
1125 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1126 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1127 invalid.
1128 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001129 try:
1130 client_type, client_info = clientsecrets.loadfile(filename)
1131 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
1132 return OAuth2WebServerFlow(
1133 client_info['client_id'],
1134 client_info['client_secret'],
1135 scope,
1136 None, # user_agent
1137 client_info['auth_uri'],
1138 client_info['token_uri'])
1139 except clientsecrets.InvalidClientSecretsError:
1140 if message:
1141 sys.exit(message)
1142 else:
1143 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001144 else:
1145 raise UnknownClientSecretsFlowError(
1146 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)