Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(436)

Side by Side Diff: appengine/chromium_build_logs/third_party/oauth2client/client.py

Issue 1260293009: make version of ts_mon compatible with appengine (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: clean up code Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright (C) 2010 Google Inc. 1 # Copyright 2014 Google Inc. All rights reserved.
2 # 2 #
3 # Licensed under the Apache License, Version 2.0 (the "License"); 3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with 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 5 # You may obtain a copy of the License at
6 # 6 #
7 # http://www.apache.org/licenses/LICENSE-2.0 7 # http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 # Unless required by applicable law or agreed to in writing, software 9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, 10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and 12 # See the License for the specific language governing permissions and
13 # limitations under the License. 13 # limitations under the License.
14 14
15 """An OAuth 2.0 client. 15 """An OAuth 2.0 client.
16 16
17 Tools for interacting with OAuth 2.0 protected resources. 17 Tools for interacting with OAuth 2.0 protected resources.
18 """ 18 """
19 19
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)' 20 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 21
22 import base64 22 import base64
23 import clientsecrets 23 import collections
24 import copy 24 import copy
25 import datetime 25 import datetime
26 import httplib2 26 import json
27 import logging 27 import logging
28 import os 28 import os
29 import socket
29 import sys 30 import sys
31 import tempfile
30 import time 32 import time
31 import urllib 33 import shutil
32 import urlparse 34 import six
35 from six.moves import urllib
33 36
34 from anyjson import simplejson 37 import httplib2
38 from oauth2client import GOOGLE_AUTH_URI
39 from oauth2client import GOOGLE_DEVICE_URI
40 from oauth2client import GOOGLE_REVOKE_URI
41 from oauth2client import GOOGLE_TOKEN_URI
42 from oauth2client._helpers import _urlsafe_b64decode
43 from oauth2client import clientsecrets
44 from oauth2client import util
35 45
36 HAS_OPENSSL = False 46 HAS_OPENSSL = False
47 HAS_CRYPTO = False
37 try: 48 try:
38 from oauth2client.crypt import Signer 49 from oauth2client import crypt
39 from oauth2client.crypt import make_signed_jwt 50 HAS_CRYPTO = True
40 from oauth2client.crypt import verify_signed_jwt_with_certs 51 if crypt.OpenSSLVerifier is not None:
41 HAS_OPENSSL = True 52 HAS_OPENSSL = True
42 except ImportError: 53 except ImportError:
43 pass 54 pass
44 55
45 try:
46 from urlparse import parse_qsl
47 except ImportError:
48 from cgi import parse_qsl
49
50 logger = logging.getLogger(__name__) 56 logger = logging.getLogger(__name__)
51 57
52 # Expiry is stored in RFC3339 UTC format 58 # Expiry is stored in RFC3339 UTC format
53 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' 59 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
54 60
55 # Which certs to use to validate id_tokens received. 61 # Which certs to use to validate id_tokens received.
56 ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' 62 ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
63 # This symbol previously had a typo in the name; we keep the old name
64 # around for now, but will remove it in the future.
65 ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
66
67 # Constant to use for the out of band OAuth 2.0 flow.
68 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
69
70 # Google Data client libraries may need to set this to [401, 403].
71 REFRESH_STATUS_CODES = [401]
72
73 # The value representing user credentials.
74 AUTHORIZED_USER = 'authorized_user'
75
76 # The value representing service account credentials.
77 SERVICE_ACCOUNT = 'service_account'
78
79 # The environment variable pointing the file with local
80 # Application Default Credentials.
81 GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS'
82 # The ~/.config subdirectory containing gcloud credentials. Intended
83 # to be swapped out in tests.
84 _CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'
85 # The environment variable name which can replace ~/.config if set.
86 _CLOUDSDK_CONFIG_ENV_VAR = 'CLOUDSDK_CONFIG'
87
88 # The error message we show users when we can't find the Application
89 # Default Credentials.
90 ADC_HELP_MSG = (
91 'The Application Default Credentials are not available. They are available '
92 'if running in Google Compute Engine. Otherwise, the environment variable '
93 + GOOGLE_APPLICATION_CREDENTIALS +
94 ' must be defined pointing to a file defining the credentials. See '
95 'https://developers.google.com/accounts/docs/application-default-credentials ' # pylint:disable=line-too-long
96 ' for more information.')
97
98 # The access token along with the seconds in which it expires.
99 AccessTokenInfo = collections.namedtuple(
100 'AccessTokenInfo', ['access_token', 'expires_in'])
101
102 DEFAULT_ENV_NAME = 'UNKNOWN'
103
104 # If set to True _get_environment avoid GCE check (_detect_gce_environment)
105 NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False')
106
107 class SETTINGS(object):
108 """Settings namespace for globally defined values."""
109 env_name = None
57 110
58 111
59 class Error(Exception): 112 class Error(Exception):
60 """Base error for this module.""" 113 """Base error for this module."""
61 pass
62 114
63 115
64 class FlowExchangeError(Error): 116 class FlowExchangeError(Error):
65 """Error trying to exchange an authorization grant for an access token.""" 117 """Error trying to exchange an authorization grant for an access token."""
66 pass
67 118
68 119
69 class AccessTokenRefreshError(Error): 120 class AccessTokenRefreshError(Error):
70 """Error trying to refresh an expired access token.""" 121 """Error trying to refresh an expired access token."""
71 pass 122
123
124 class TokenRevokeError(Error):
125 """Error trying to revoke a token."""
126
72 127
73 class UnknownClientSecretsFlowError(Error): 128 class UnknownClientSecretsFlowError(Error):
74 """The client secrets file called for an unknown type of OAuth 2.0 flow. """ 129 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
75 pass
76 130
77 131
78 class AccessTokenCredentialsError(Error): 132 class AccessTokenCredentialsError(Error):
79 """Having only the access_token means no refresh is possible.""" 133 """Having only the access_token means no refresh is possible."""
80 pass
81 134
82 135
83 class VerifyJwtTokenError(Error): 136 class VerifyJwtTokenError(Error):
84 """Could on retrieve certificates for validation.""" 137 """Could not retrieve certificates for validation."""
85 pass 138
139
140 class NonAsciiHeaderError(Error):
141 """Header names and values must be ASCII strings."""
142
143
144 class ApplicationDefaultCredentialsError(Error):
145 """Error retrieving the Application Default Credentials."""
146
147
148 class OAuth2DeviceCodeError(Error):
149 """Error trying to retrieve a device code."""
150
151
152 class CryptoUnavailableError(Error, NotImplementedError):
153 """Raised when a crypto library is required, but none is available."""
86 154
87 155
88 def _abstract(): 156 def _abstract():
89 raise NotImplementedError('You need to override this function') 157 raise NotImplementedError('You need to override this function')
90 158
91 159
160 class MemoryCache(object):
161 """httplib2 Cache implementation which only caches locally."""
162
163 def __init__(self):
164 self.cache = {}
165
166 def get(self, key):
167 return self.cache.get(key)
168
169 def set(self, key, value):
170 self.cache[key] = value
171
172 def delete(self, key):
173 self.cache.pop(key, None)
174
175
92 class Credentials(object): 176 class Credentials(object):
93 """Base class for all Credentials objects. 177 """Base class for all Credentials objects.
94 178
95 Subclasses must define an authorize() method that applies the credentials to 179 Subclasses must define an authorize() method that applies the credentials to
96 an HTTP transport. 180 an HTTP transport.
97 181
98 Subclasses must also specify a classmethod named 'from_json' that takes a JSON 182 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
99 string as input and returns an instaniated Crentials object. 183 string as input and returns an instantiated Credentials object.
100 """ 184 """
101 185
102 NON_SERIALIZED_MEMBERS = ['store'] 186 NON_SERIALIZED_MEMBERS = ['store']
103 187
188
104 def authorize(self, http): 189 def authorize(self, http):
105 """Take an httplib2.Http instance (or equivalent) and 190 """Take an httplib2.Http instance (or equivalent) and authorizes it.
106 authorizes it for the set of credentials, usually by 191
107 replacing http.request() with a method that adds in 192 Authorizes it for the set of credentials, usually by replacing
108 the appropriate headers and then delegates to the original 193 http.request() with a method that adds in the appropriate headers and then
109 Http.request() method. 194 delegates to the original Http.request() method.
195
196 Args:
197 http: httplib2.Http, an http object to be used to make the refresh
198 request.
199 """
200 _abstract()
201
202
203 def refresh(self, http):
204 """Forces a refresh of the access_token.
205
206 Args:
207 http: httplib2.Http, an http object to be used to make the refresh
208 request.
209 """
210 _abstract()
211
212
213 def revoke(self, http):
214 """Revokes a refresh_token and makes the credentials void.
215
216 Args:
217 http: httplib2.Http, an http object to be used to make the revoke
218 request.
219 """
220 _abstract()
221
222
223 def apply(self, headers):
224 """Add the authorization to the headers.
225
226 Args:
227 headers: dict, the headers to add the Authorization header to.
110 """ 228 """
111 _abstract() 229 _abstract()
112 230
113 def _to_json(self, strip): 231 def _to_json(self, strip):
114 """Utility function for creating a JSON representation of an instance of Cre dentials. 232 """Utility function that creates JSON repr. of a Credentials object.
115 233
116 Args: 234 Args:
117 strip: array, An array of names of members to not include in the JSON. 235 strip: array, An array of names of members to not include in the JSON.
118 236
119 Returns: 237 Returns:
120 string, a JSON representation of this instance, suitable to pass to 238 string, a JSON representation of this instance, suitable to pass to
121 from_json(). 239 from_json().
122 """ 240 """
123 t = type(self) 241 t = type(self)
124 d = copy.copy(self.__dict__) 242 d = copy.copy(self.__dict__)
125 for member in strip: 243 for member in strip:
126 del d[member] 244 if member in d:
127 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): 245 del d[member]
246 if (d.get('token_expiry') and
247 isinstance(d['token_expiry'], datetime.datetime)):
128 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) 248 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
129 # Add in information we will need later to reconsistitue this instance. 249 # Add in information we will need later to reconsistitue this instance.
130 d['_class'] = t.__name__ 250 d['_class'] = t.__name__
131 d['_module'] = t.__module__ 251 d['_module'] = t.__module__
132 return simplejson.dumps(d) 252 for key, val in d.items():
253 if isinstance(val, bytes):
254 d[key] = val.decode('utf-8')
255 return json.dumps(d)
133 256
134 def to_json(self): 257 def to_json(self):
135 """Creating a JSON representation of an instance of Credentials. 258 """Creating a JSON representation of an instance of Credentials.
136 259
137 Returns: 260 Returns:
138 string, a JSON representation of this instance, suitable to pass to 261 string, a JSON representation of this instance, suitable to pass to
139 from_json(). 262 from_json().
140 """ 263 """
141 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) 264 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
142 265
143 @classmethod 266 @classmethod
144 def new_from_json(cls, s): 267 def new_from_json(cls, s):
145 """Utility class method to instantiate a Credentials subclass from a JSON 268 """Utility class method to instantiate a Credentials subclass from a JSON
146 representation produced by to_json(). 269 representation produced by to_json().
147 270
148 Args: 271 Args:
149 s: string, JSON from to_json(). 272 s: string, JSON from to_json().
150 273
151 Returns: 274 Returns:
152 An instance of the subclass of Credentials that was serialized with 275 An instance of the subclass of Credentials that was serialized with
153 to_json(). 276 to_json().
154 """ 277 """
155 data = simplejson.loads(s) 278 if six.PY3 and isinstance(s, bytes):
279 s = s.decode('utf-8')
280 data = json.loads(s)
156 # Find and call the right classmethod from_json() to restore the object. 281 # Find and call the right classmethod from_json() to restore the object.
157 module = data['_module'] 282 module = data['_module']
283 try:
284 m = __import__(module)
285 except ImportError:
286 # In case there's an object from the old package structure, update it
287 module = module.replace('.googleapiclient', '')
288 m = __import__(module)
289
158 m = __import__(module, fromlist=module.split('.')[:-1]) 290 m = __import__(module, fromlist=module.split('.')[:-1])
159 kls = getattr(m, data['_class']) 291 kls = getattr(m, data['_class'])
160 from_json = getattr(kls, 'from_json') 292 from_json = getattr(kls, 'from_json')
161 return from_json(s) 293 return from_json(s)
162 294
295 @classmethod
296 def from_json(cls, unused_data):
297 """Instantiate a Credentials object from a JSON description of it.
298
299 The JSON should have been produced by calling .to_json() on the object.
300
301 Args:
302 unused_data: dict, A deserialized JSON object.
303
304 Returns:
305 An instance of a Credentials subclass.
306 """
307 return Credentials()
308
163 309
164 class Flow(object): 310 class Flow(object):
165 """Base class for all Flow objects.""" 311 """Base class for all Flow objects."""
166 pass 312 pass
167 313
168 314
169 class Storage(object): 315 class Storage(object):
170 """Base class for all Storage objects. 316 """Base class for all Storage objects.
171 317
172 Store and retrieve a single credential. This class supports locking 318 Store and retrieve a single credential. This class supports locking
173 such that multiple processes and threads can operate on a single 319 such that multiple processes and threads can operate on a single
174 store. 320 store.
175 """ 321 """
176 322
177 def acquire_lock(self): 323 def acquire_lock(self):
178 """Acquires any lock necessary to access this Storage. 324 """Acquires any lock necessary to access this Storage.
179 325
180 This lock is not reentrant.""" 326 This lock is not reentrant.
327 """
181 pass 328 pass
182 329
183 def release_lock(self): 330 def release_lock(self):
184 """Release the Storage lock. 331 """Release the Storage lock.
185 332
186 Trying to release a lock that isn't held will result in a 333 Trying to release a lock that isn't held will result in a
187 RuntimeError. 334 RuntimeError.
188 """ 335 """
189 pass 336 pass
190 337
(...skipping 10 matching lines...) Expand all
201 def locked_put(self, credentials): 348 def locked_put(self, credentials):
202 """Write a credential. 349 """Write a credential.
203 350
204 The Storage lock must be held when this is called. 351 The Storage lock must be held when this is called.
205 352
206 Args: 353 Args:
207 credentials: Credentials, the credentials to store. 354 credentials: Credentials, the credentials to store.
208 """ 355 """
209 _abstract() 356 _abstract()
210 357
358 def locked_delete(self):
359 """Delete a credential.
360
361 The Storage lock must be held when this is called.
362 """
363 _abstract()
364
211 def get(self): 365 def get(self):
212 """Retrieve credential. 366 """Retrieve credential.
213 367
214 The Storage lock must *not* be held when this is called. 368 The Storage lock must *not* be held when this is called.
215 369
216 Returns: 370 Returns:
217 oauth2client.client.Credentials 371 oauth2client.client.Credentials
218 """ 372 """
219 self.acquire_lock() 373 self.acquire_lock()
220 try: 374 try:
221 return self.locked_get() 375 return self.locked_get()
222 finally: 376 finally:
223 self.release_lock() 377 self.release_lock()
224 378
225 def put(self, credentials): 379 def put(self, credentials):
226 """Write a credential. 380 """Write a credential.
227 381
228 The Storage lock must be held when this is called. 382 The Storage lock must be held when this is called.
229 383
230 Args: 384 Args:
231 credentials: Credentials, the credentials to store. 385 credentials: Credentials, the credentials to store.
232 """ 386 """
233 self.acquire_lock() 387 self.acquire_lock()
234 try: 388 try:
235 self.locked_put(credentials) 389 self.locked_put(credentials)
236 finally: 390 finally:
237 self.release_lock() 391 self.release_lock()
238 392
393 def delete(self):
394 """Delete credential.
395
396 Frees any resources associated with storing the credential.
397 The Storage lock must *not* be held when this is called.
398
399 Returns:
400 None
401 """
402 self.acquire_lock()
403 try:
404 return self.locked_delete()
405 finally:
406 self.release_lock()
407
239 408
409 def clean_headers(headers):
410 """Forces header keys and values to be strings, i.e not unicode.
411
412 The httplib module just concats the header keys and values in a way that may
413 make the message header a unicode string, which, if it then tries to
414 contatenate to a binary request body may result in a unicode decode error.
415
416 Args:
417 headers: dict, A dictionary of headers.
418
419 Returns:
420 The same dictionary but with all the keys converted to strings.
421 """
422 clean = {}
423 try:
424 for k, v in six.iteritems(headers):
425 clean_k = k if isinstance(k, bytes) else str(k).encode('ascii')
426 clean_v = v if isinstance(v, bytes) else str(v).encode('ascii')
427 clean[clean_k] = clean_v
428 except UnicodeEncodeError:
429 raise NonAsciiHeaderError(k + ': ' + v)
430 return clean
431
432
433 def _update_query_params(uri, params):
434 """Updates a URI with new query parameters.
435
436 Args:
437 uri: string, A valid URI, with potential existing query parameters.
438 params: dict, A dictionary of query parameters.
439
440 Returns:
441 The same URI but with the new query parameters added.
442 """
443 parts = urllib.parse.urlparse(uri)
444 query_params = dict(urllib.parse.parse_qsl(parts.query))
445 query_params.update(params)
446 new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
447 return urllib.parse.urlunparse(new_parts)
448
449
240 class OAuth2Credentials(Credentials): 450 class OAuth2Credentials(Credentials):
241 """Credentials object for OAuth 2.0. 451 """Credentials object for OAuth 2.0.
242 452
243 Credentials can be applied to an httplib2.Http object using the authorize() 453 Credentials can be applied to an httplib2.Http object using the authorize()
244 method, which then adds the OAuth 2.0 access token to each request. 454 method, which then adds the OAuth 2.0 access token to each request.
245 455
246 OAuth2Credentials objects may be safely pickled and unpickled. 456 OAuth2Credentials objects may be safely pickled and unpickled.
247 """ 457 """
248 458
459 @util.positional(8)
249 def __init__(self, access_token, client_id, client_secret, refresh_token, 460 def __init__(self, access_token, client_id, client_secret, refresh_token,
250 token_expiry, token_uri, user_agent, id_token=None): 461 token_expiry, token_uri, user_agent, revoke_uri=None,
462 id_token=None, token_response=None):
251 """Create an instance of OAuth2Credentials. 463 """Create an instance of OAuth2Credentials.
252 464
253 This constructor is not usually called by the user, instead 465 This constructor is not usually called by the user, instead
254 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. 466 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
255 467
256 Args: 468 Args:
257 access_token: string, access token. 469 access_token: string, access token.
258 client_id: string, client identifier. 470 client_id: string, client identifier.
259 client_secret: string, client secret. 471 client_secret: string, client secret.
260 refresh_token: string, refresh token. 472 refresh_token: string, refresh token.
261 token_expiry: datetime, when the access_token expires. 473 token_expiry: datetime, when the access_token expires.
262 token_uri: string, URI of token endpoint. 474 token_uri: string, URI of token endpoint.
263 user_agent: string, The HTTP User-Agent to provide for this application. 475 user_agent: string, The HTTP User-Agent to provide for this application.
476 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
477 can't be revoked if this is None.
264 id_token: object, The identity of the resource owner. 478 id_token: object, The identity of the resource owner.
479 token_response: dict, the decoded response to the token request. None
480 if a token hasn't been requested yet. Stored because some providers
481 (e.g. wordpress.com) include extra fields that clients may want.
265 482
266 Notes: 483 Notes:
267 store: callable, A callable that when passed a Credential 484 store: callable, A callable that when passed a Credential
268 will store the credential back to where it came from. 485 will store the credential back to where it came from.
269 This is needed to store the latest access_token if it 486 This is needed to store the latest access_token if it
270 has expired and been refreshed. 487 has expired and been refreshed.
271 """ 488 """
272 self.access_token = access_token 489 self.access_token = access_token
273 self.client_id = client_id 490 self.client_id = client_id
274 self.client_secret = client_secret 491 self.client_secret = client_secret
275 self.refresh_token = refresh_token 492 self.refresh_token = refresh_token
276 self.store = None 493 self.store = None
277 self.token_expiry = token_expiry 494 self.token_expiry = token_expiry
278 self.token_uri = token_uri 495 self.token_uri = token_uri
279 self.user_agent = user_agent 496 self.user_agent = user_agent
497 self.revoke_uri = revoke_uri
280 self.id_token = id_token 498 self.id_token = id_token
499 self.token_response = token_response
281 500
282 # True if the credentials have been revoked or expired and can't be 501 # True if the credentials have been revoked or expired and can't be
283 # refreshed. 502 # refreshed.
284 self.invalid = False 503 self.invalid = False
285 504
505 def authorize(self, http):
506 """Authorize an httplib2.Http instance with these credentials.
507
508 The modified http.request method will add authentication headers to each
509 request and will refresh access_tokens when a 401 is received on a
510 request. In addition the http.request method has a credentials property,
511 http.request.credentials, which is the Credentials object that authorized
512 it.
513
514 Args:
515 http: An instance of ``httplib2.Http`` or something that acts
516 like it.
517
518 Returns:
519 A modified instance of http that was passed in.
520
521 Example::
522
523 h = httplib2.Http()
524 h = credentials.authorize(h)
525
526 You can't create a new OAuth subclass of httplib2.Authentication
527 because it never gets passed the absolute URI, which is needed for
528 signing. So instead we have to overload 'request' with a closure
529 that adds in the Authorization header and then calls the original
530 version of 'request()'.
531
532 """
533 request_orig = http.request
534
535 # The closure that will replace 'httplib2.Http.request'.
536 def new_request(uri, method='GET', body=None, headers=None,
537 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
538 connection_type=None):
539 if not self.access_token:
540 logger.info('Attempting refresh to obtain initial access_token')
541 self._refresh(request_orig)
542
543 # Clone and modify the request headers to add the appropriate
544 # Authorization header.
545 if headers is None:
546 headers = {}
547 else:
548 headers = dict(headers)
549 self.apply(headers)
550
551 if self.user_agent is not None:
552 if 'user-agent' in headers:
553 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
554 else:
555 headers['user-agent'] = self.user_agent
556
557 body_stream_position = None
558 if all(getattr(body, stream_prop, None) for stream_prop in
559 ('read', 'seek', 'tell')):
560 body_stream_position = body.tell()
561
562 resp, content = request_orig(uri, method, body, clean_headers(headers),
563 redirections, connection_type)
564
565 # A stored token may expire between the time it is retrieved and the time
566 # the request is made, so we may need to try twice.
567 max_refresh_attempts = 2
568 for refresh_attempt in range(max_refresh_attempts):
569 if resp.status not in REFRESH_STATUS_CODES:
570 break
571 logger.info('Refreshing due to a %s (attempt %s/%s)', resp.status,
572 refresh_attempt + 1, max_refresh_attempts)
573 self._refresh(request_orig)
574 self.apply(headers)
575 if body_stream_position is not None:
576 body.seek(body_stream_position)
577
578 resp, content = request_orig(uri, method, body, clean_headers(headers),
579 redirections, connection_type)
580
581 return (resp, content)
582
583 # Replace the request method with our own closure.
584 http.request = new_request
585
586 # Set credentials as a property of the request method.
587 setattr(http.request, 'credentials', self)
588
589 return http
590
591 def refresh(self, http):
592 """Forces a refresh of the access_token.
593
594 Args:
595 http: httplib2.Http, an http object to be used to make the refresh
596 request.
597 """
598 self._refresh(http.request)
599
600 def revoke(self, http):
601 """Revokes a refresh_token and makes the credentials void.
602
603 Args:
604 http: httplib2.Http, an http object to be used to make the revoke
605 request.
606 """
607 self._revoke(http.request)
608
609 def apply(self, headers):
610 """Add the authorization to the headers.
611
612 Args:
613 headers: dict, the headers to add the Authorization header to.
614 """
615 headers['Authorization'] = 'Bearer ' + self.access_token
616
286 def to_json(self): 617 def to_json(self):
287 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) 618 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
288 619
289 @classmethod 620 @classmethod
290 def from_json(cls, s): 621 def from_json(cls, s):
291 """Instantiate a Credentials object from a JSON description of it. The JSON 622 """Instantiate a Credentials object from a JSON description of it. The JSON
292 should have been produced by calling .to_json() on the object. 623 should have been produced by calling .to_json() on the object.
293 624
294 Args: 625 Args:
295 data: dict, A deserialized JSON object. 626 data: dict, A deserialized JSON object.
296 627
297 Returns: 628 Returns:
298 An instance of a Credentials subclass. 629 An instance of a Credentials subclass.
299 """ 630 """
300 data = simplejson.loads(s) 631 if six.PY3 and isinstance(s, bytes):
301 if 'token_expiry' in data and not isinstance(data['token_expiry'], 632 s = s.decode('utf-8')
302 datetime.datetime): 633 data = json.loads(s)
634 if (data.get('token_expiry') and
635 not isinstance(data['token_expiry'], datetime.datetime)):
303 try: 636 try:
304 data['token_expiry'] = datetime.datetime.strptime( 637 data['token_expiry'] = datetime.datetime.strptime(
305 data['token_expiry'], EXPIRY_FORMAT) 638 data['token_expiry'], EXPIRY_FORMAT)
306 except: 639 except ValueError:
307 data['token_expiry'] = None 640 data['token_expiry'] = None
308 retval = OAuth2Credentials( 641 retval = cls(
309 data['access_token'], 642 data['access_token'],
310 data['client_id'], 643 data['client_id'],
311 data['client_secret'], 644 data['client_secret'],
312 data['refresh_token'], 645 data['refresh_token'],
313 data['token_expiry'], 646 data['token_expiry'],
314 data['token_uri'], 647 data['token_uri'],
315 data['user_agent'], 648 data['user_agent'],
316 data.get('id_token', None)) 649 revoke_uri=data.get('revoke_uri', None),
650 id_token=data.get('id_token', None),
651 token_response=data.get('token_response', None))
317 retval.invalid = data['invalid'] 652 retval.invalid = data['invalid']
318 return retval 653 return retval
319 654
320 @property 655 @property
321 def access_token_expired(self): 656 def access_token_expired(self):
322 """True if the credential is expired or invalid. 657 """True if the credential is expired or invalid.
323 658
324 If the token_expiry isn't set, we assume the token doesn't expire. 659 If the token_expiry isn't set, we assume the token doesn't expire.
325 """ 660 """
326 if self.invalid: 661 if self.invalid:
327 return True 662 return True
328 663
329 if not self.token_expiry: 664 if not self.token_expiry:
330 return False 665 return False
331 666
332 now = datetime.datetime.utcnow() 667 now = datetime.datetime.utcnow()
333 if now >= self.token_expiry: 668 if now >= self.token_expiry:
334 logger.info('access_token is expired. Now: %s, token_expiry: %s', 669 logger.info('access_token is expired. Now: %s, token_expiry: %s',
335 now, self.token_expiry) 670 now, self.token_expiry)
336 return True 671 return True
337 return False 672 return False
338 673
674 def get_access_token(self, http=None):
675 """Return the access token and its expiration information.
676
677 If the token does not exist, get one.
678 If the token expired, refresh it.
679 """
680 if not self.access_token or self.access_token_expired:
681 if not http:
682 http = httplib2.Http()
683 self.refresh(http)
684 return AccessTokenInfo(access_token=self.access_token,
685 expires_in=self._expires_in())
686
339 def set_store(self, store): 687 def set_store(self, store):
340 """Set the Storage for the credential. 688 """Set the Storage for the credential.
341 689
342 Args: 690 Args:
343 store: Storage, an implementation of Stroage object. 691 store: Storage, an implementation of Storage object.
344 This is needed to store the latest access_token if it 692 This is needed to store the latest access_token if it
345 has expired and been refreshed. This implementation uses 693 has expired and been refreshed. This implementation uses
346 locking to check for updates before updating the 694 locking to check for updates before updating the
347 access_token. 695 access_token.
348 """ 696 """
349 self.store = store 697 self.store = store
350 698
699 def _expires_in(self):
700 """Return the number of seconds until this token expires.
701
702 If token_expiry is in the past, this method will return 0, meaning the
703 token has already expired.
704 If token_expiry is None, this method will return None. Note that returning
705 0 in such a case would not be fair: the token may still be valid;
706 we just don't know anything about it.
707 """
708 if self.token_expiry:
709 now = datetime.datetime.utcnow()
710 if self.token_expiry > now:
711 time_delta = self.token_expiry - now
712 # TODO(orestica): return time_delta.total_seconds()
713 # once dropping support for Python 2.6
714 return time_delta.days * 86400 + time_delta.seconds
715 else:
716 return 0
717
351 def _updateFromCredential(self, other): 718 def _updateFromCredential(self, other):
352 """Update this Credential from another instance.""" 719 """Update this Credential from another instance."""
353 self.__dict__.update(other.__getstate__()) 720 self.__dict__.update(other.__getstate__())
354 721
355 def __getstate__(self): 722 def __getstate__(self):
356 """Trim the state down to something that can be pickled.""" 723 """Trim the state down to something that can be pickled."""
357 d = copy.copy(self.__dict__) 724 d = copy.copy(self.__dict__)
358 del d['store'] 725 del d['store']
359 return d 726 return d
360 727
361 def __setstate__(self, state): 728 def __setstate__(self, state):
362 """Reconstitute the state of the object from being pickled.""" 729 """Reconstitute the state of the object from being pickled."""
363 self.__dict__.update(state) 730 self.__dict__.update(state)
364 self.store = None 731 self.store = None
365 732
366 def _generate_refresh_request_body(self): 733 def _generate_refresh_request_body(self):
367 """Generate the body that will be used in the refresh request.""" 734 """Generate the body that will be used in the refresh request."""
368 body = urllib.urlencode({ 735 body = urllib.parse.urlencode({
369 'grant_type': 'refresh_token', 736 'grant_type': 'refresh_token',
370 'client_id': self.client_id, 737 'client_id': self.client_id,
371 'client_secret': self.client_secret, 738 'client_secret': self.client_secret,
372 'refresh_token': self.refresh_token, 739 'refresh_token': self.refresh_token,
373 }) 740 })
374 return body 741 return body
375 742
376 def _generate_refresh_request_headers(self): 743 def _generate_refresh_request_headers(self):
377 """Generate the headers that will be used in the refresh request.""" 744 """Generate the headers that will be used in the refresh request."""
378 headers = { 745 headers = {
379 'content-type': 'application/x-www-form-urlencoded', 746 'content-type': 'application/x-www-form-urlencoded',
380 } 747 }
381 748
382 if self.user_agent is not None: 749 if self.user_agent is not None:
383 headers['user-agent'] = self.user_agent 750 headers['user-agent'] = self.user_agent
384 751
385 return headers 752 return headers
386 753
387 def _refresh(self, http_request): 754 def _refresh(self, http_request):
388 """Refreshes the access_token. 755 """Refreshes the access_token.
389 756
390 This method first checks by reading the Storage object if available. 757 This method first checks by reading the Storage object if available.
391 If a refresh is still needed, it holds the Storage lock until the 758 If a refresh is still needed, it holds the Storage lock until the
392 refresh is completed. 759 refresh is completed.
760
761 Args:
762 http_request: callable, a callable that matches the method signature of
763 httplib2.Http.request, used to make the refresh request.
764
765 Raises:
766 AccessTokenRefreshError: When the refresh fails.
393 """ 767 """
394 if not self.store: 768 if not self.store:
395 self._do_refresh_request(http_request) 769 self._do_refresh_request(http_request)
396 else: 770 else:
397 self.store.acquire_lock() 771 self.store.acquire_lock()
398 try: 772 try:
399 new_cred = self.store.locked_get() 773 new_cred = self.store.locked_get()
774
400 if (new_cred and not new_cred.invalid and 775 if (new_cred and not new_cred.invalid and
401 new_cred.access_token != self.access_token): 776 new_cred.access_token != self.access_token and
777 not new_cred.access_token_expired):
402 logger.info('Updated access_token read from Storage') 778 logger.info('Updated access_token read from Storage')
403 self._updateFromCredential(new_cred) 779 self._updateFromCredential(new_cred)
404 else: 780 else:
405 self._do_refresh_request(http_request) 781 self._do_refresh_request(http_request)
406 finally: 782 finally:
407 self.store.release_lock() 783 self.store.release_lock()
408 784
409 def _do_refresh_request(self, http_request): 785 def _do_refresh_request(self, http_request):
410 """Refresh the access_token using the refresh_token. 786 """Refresh the access_token using the refresh_token.
411 787
412 Args: 788 Args:
413 http: An instance of httplib2.Http.request 789 http_request: callable, a callable that matches the method signature of
414 or something that acts like it. 790 httplib2.Http.request, used to make the refresh request.
415 791
416 Raises: 792 Raises:
417 AccessTokenRefreshError: When the refresh fails. 793 AccessTokenRefreshError: When the refresh fails.
418 """ 794 """
419 body = self._generate_refresh_request_body() 795 body = self._generate_refresh_request_body()
420 headers = self._generate_refresh_request_headers() 796 headers = self._generate_refresh_request_headers()
421 797
422 logger.info('Refresing access_token') 798 logger.info('Refreshing access_token')
423 resp, content = http_request( 799 resp, content = http_request(
424 self.token_uri, method='POST', body=body, headers=headers) 800 self.token_uri, method='POST', body=body, headers=headers)
801 if six.PY3 and isinstance(content, bytes):
802 content = content.decode('utf-8')
425 if resp.status == 200: 803 if resp.status == 200:
426 # TODO(jcgregorio) Raise an error if loads fails? 804 d = json.loads(content)
427 d = simplejson.loads(content) 805 self.token_response = d
428 self.access_token = d['access_token'] 806 self.access_token = d['access_token']
429 self.refresh_token = d.get('refresh_token', self.refresh_token) 807 self.refresh_token = d.get('refresh_token', self.refresh_token)
430 if 'expires_in' in d: 808 if 'expires_in' in d:
431 self.token_expiry = datetime.timedelta( 809 self.token_expiry = datetime.timedelta(
432 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() 810 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
433 else: 811 else:
434 self.token_expiry = None 812 self.token_expiry = None
813 # On temporary refresh errors, the user does not actually have to
814 # re-authorize, so we unflag here.
815 self.invalid = False
435 if self.store: 816 if self.store:
436 self.store.locked_put(self) 817 self.store.locked_put(self)
437 else: 818 else:
438 # An {'error':...} response body means the token is expired or revoked, 819 # An {'error':...} response body means the token is expired or revoked,
439 # so we flag the credentials as such. 820 # so we flag the credentials as such.
440 logger.error('Failed to retrieve access token: %s' % content) 821 logger.info('Failed to retrieve access token: %s', content)
441 error_msg = 'Invalid response %s.' % resp['status'] 822 error_msg = 'Invalid response %s.' % resp['status']
442 try: 823 try:
443 d = simplejson.loads(content) 824 d = json.loads(content)
444 if 'error' in d: 825 if 'error' in d:
445 error_msg = d['error'] 826 error_msg = d['error']
827 if 'error_description' in d:
828 error_msg += ': ' + d['error_description']
446 self.invalid = True 829 self.invalid = True
447 if self.store: 830 if self.store:
448 self.store.locked_put(self) 831 self.store.locked_put(self)
449 except: 832 except (TypeError, ValueError):
450 pass 833 pass
451 raise AccessTokenRefreshError(error_msg) 834 raise AccessTokenRefreshError(error_msg)
452 835
453 def authorize(self, http): 836 def _revoke(self, http_request):
454 """Authorize an httplib2.Http instance with these credentials. 837 """Revokes this credential and deletes the stored copy (if it exists).
455 838
456 Args: 839 Args:
457 http: An instance of httplib2.Http 840 http_request: callable, a callable that matches the method signature of
458 or something that acts like it. 841 httplib2.Http.request, used to make the revoke request.
842 """
843 self._do_revoke(http_request, self.refresh_token or self.access_token)
459 844
460 Returns: 845 def _do_revoke(self, http_request, token):
461 A modified instance of http that was passed in. 846 """Revokes this credential and deletes the stored copy (if it exists).
462 847
463 Example: 848 Args:
849 http_request: callable, a callable that matches the method signature of
850 httplib2.Http.request, used to make the refresh request.
851 token: A string used as the token to be revoked. Can be either an
852 access_token or refresh_token.
464 853
465 h = httplib2.Http() 854 Raises:
466 h = credentials.authorize(h) 855 TokenRevokeError: If the revoke request does not return with a 200 OK.
856 """
857 logger.info('Revoking token')
858 query_params = {'token': token}
859 token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
860 resp, content = http_request(token_revoke_uri)
861 if resp.status == 200:
862 self.invalid = True
863 else:
864 error_msg = 'Invalid response %s.' % resp.status
865 try:
866 d = json.loads(content)
867 if 'error' in d:
868 error_msg = d['error']
869 except (TypeError, ValueError):
870 pass
871 raise TokenRevokeError(error_msg)
467 872
468 You can't create a new OAuth subclass of httplib2.Authenication 873 if self.store:
469 because it never gets passed the absolute URI, which is needed for 874 self.store.delete()
470 signing. So instead we have to overload 'request' with a closure
471 that adds in the Authorization header and then calls the original
472 version of 'request()'.
473 """
474 request_orig = http.request
475
476 # The closure that will replace 'httplib2.Http.request'.
477 def new_request(uri, method='GET', body=None, headers=None,
478 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
479 connection_type=None):
480 if not self.access_token:
481 logger.info('Attempting refresh to obtain initial access_token')
482 self._refresh(request_orig)
483
484 # Modify the request headers to add the appropriate
485 # Authorization header.
486 if headers is None:
487 headers = {}
488 headers['authorization'] = 'OAuth ' + self.access_token
489
490 if self.user_agent is not None:
491 if 'user-agent' in headers:
492 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
493 else:
494 headers['user-agent'] = self.user_agent
495
496 resp, content = request_orig(uri, method, body, headers,
497 redirections, connection_type)
498
499 if resp.status == 401:
500 logger.info('Refreshing due to a 401')
501 self._refresh(request_orig)
502 headers['authorization'] = 'OAuth ' + self.access_token
503 return request_orig(uri, method, body, headers,
504 redirections, connection_type)
505 else:
506 return (resp, content)
507
508 http.request = new_request
509 return http
510 875
511 876
512 class AccessTokenCredentials(OAuth2Credentials): 877 class AccessTokenCredentials(OAuth2Credentials):
513 """Credentials object for OAuth 2.0. 878 """Credentials object for OAuth 2.0.
514 879
515 Credentials can be applied to an httplib2.Http object using the 880 Credentials can be applied to an httplib2.Http object using the
516 authorize() method, which then signs each request from that object 881 authorize() method, which then signs each request from that object
517 with the OAuth 2.0 access token. This set of credentials is for the 882 with the OAuth 2.0 access token. This set of credentials is for the
518 use case where you have acquired an OAuth 2.0 access_token from 883 use case where you have acquired an OAuth 2.0 access_token from
519 another place such as a JavaScript client or another web 884 another place such as a JavaScript client or another web
520 application, and wish to use it from Python. Because only the 885 application, and wish to use it from Python. Because only the
521 access_token is present it can not be refreshed and will in time 886 access_token is present it can not be refreshed and will in time
522 expire. 887 expire.
523 888
524 AccessTokenCredentials objects may be safely pickled and unpickled. 889 AccessTokenCredentials objects may be safely pickled and unpickled.
525 890
526 Usage: 891 Usage::
892
527 credentials = AccessTokenCredentials('<an access token>', 893 credentials = AccessTokenCredentials('<an access token>',
528 'my-user-agent/1.0') 894 'my-user-agent/1.0')
529 http = httplib2.Http() 895 http = httplib2.Http()
530 http = credentials.authorize(http) 896 http = credentials.authorize(http)
531 897
532 Exceptions: 898 Exceptions:
533 AccessTokenCredentialsExpired: raised when the access_token expires or is 899 AccessTokenCredentialsExpired: raised when the access_token expires or is
534 revoked. 900 revoked.
535 """ 901 """
536 902
537 def __init__(self, access_token, user_agent): 903 def __init__(self, access_token, user_agent, revoke_uri=None):
538 """Create an instance of OAuth2Credentials 904 """Create an instance of OAuth2Credentials
539 905
540 This is one of the few types if Credentials that you should contrust, 906 This is one of the few types if Credentials that you should contrust,
541 Credentials objects are usually instantiated by a Flow. 907 Credentials objects are usually instantiated by a Flow.
542 908
543 Args: 909 Args:
544 access_token: string, access token. 910 access_token: string, access token.
545 user_agent: string, The HTTP User-Agent to provide for this application. 911 user_agent: string, The HTTP User-Agent to provide for this application.
546 912 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
547 Notes: 913 can't be revoked if this is None.
548 store: callable, a callable that when passed a Credential
549 will store the credential back to where it came from.
550 """ 914 """
551 super(AccessTokenCredentials, self).__init__( 915 super(AccessTokenCredentials, self).__init__(
552 access_token, 916 access_token,
553 None, 917 None,
554 None, 918 None,
555 None, 919 None,
556 None, 920 None,
557 None, 921 None,
558 user_agent) 922 user_agent,
923 revoke_uri=revoke_uri)
559 924
560 925
561 @classmethod 926 @classmethod
562 def from_json(cls, s): 927 def from_json(cls, s):
563 data = simplejson.loads(s) 928 if six.PY3 and isinstance(s, bytes):
929 s = s.decode('utf-8')
930 data = json.loads(s)
564 retval = AccessTokenCredentials( 931 retval = AccessTokenCredentials(
565 data['access_token'], 932 data['access_token'],
566 data['user_agent']) 933 data['user_agent'])
567 return retval 934 return retval
568 935
569 def _refresh(self, http_request): 936 def _refresh(self, http_request):
570 raise AccessTokenCredentialsError( 937 raise AccessTokenCredentialsError(
571 "The access_token is expired or invalid and can't be refreshed.") 938 'The access_token is expired or invalid and can\'t be refreshed.')
572 939
573 940 def _revoke(self, http_request):
574 class AssertionCredentials(OAuth2Credentials): 941 """Revokes the access_token and deletes the store if available.
942
943 Args:
944 http_request: callable, a callable that matches the method signature of
945 httplib2.Http.request, used to make the revoke request.
946 """
947 self._do_revoke(http_request, self.access_token)
948
949
950 def _detect_gce_environment(urlopen=None):
951 """Determine if the current environment is Compute Engine.
952
953 Args:
954 urlopen: Optional argument. Function used to open a connection to a URL.
955
956 Returns:
957 Boolean indicating whether or not the current environment is Google
958 Compute Engine.
959 """
960 urlopen = urlopen or urllib.request.urlopen
961 # Note: the explicit `timeout` below is a workaround. The underlying
962 # issue is that resolving an unknown host on some networks will take
963 # 20-30 seconds; making this timeout short fixes the issue, but
964 # could lead to false negatives in the event that we are on GCE, but
965 # the metadata resolution was particularly slow. The latter case is
966 # "unlikely".
967 try:
968 response = urlopen('http://169.254.169.254/', timeout=1)
969 return response.info().get('Metadata-Flavor', '') == 'Google'
970 except socket.timeout:
971 logger.info('Timeout attempting to reach GCE metadata service.')
972 return False
973 except urllib.error.URLError as e:
974 if isinstance(getattr(e, 'reason', None), socket.timeout):
975 logger.info('Timeout attempting to reach GCE metadata service.')
976 return False
977
978
979 def _in_gae_environment():
980 """Detects if the code is running in the App Engine environment.
981
982 Returns:
983 True if running in the GAE environment, False otherwise.
984 """
985 if SETTINGS.env_name is not None:
986 return SETTINGS.env_name in ('GAE_PRODUCTION', 'GAE_LOCAL')
987
988 try:
989 import google.appengine
990 server_software = os.environ.get('SERVER_SOFTWARE', '')
991 if server_software.startswith('Google App Engine/'):
992 SETTINGS.env_name = 'GAE_PRODUCTION'
993 return True
994 elif server_software.startswith('Development/'):
995 SETTINGS.env_name = 'GAE_LOCAL'
996 return True
997 except ImportError:
998 pass
999
1000 return False
1001
1002
1003 def _in_gce_environment(urlopen=None):
1004 """Detect if the code is running in the Compute Engine environment.
1005
1006 Args:
1007 urlopen: Optional argument. Function used to open a connection to a URL.
1008
1009 Returns:
1010 True if running in the GCE environment, False otherwise.
1011 """
1012 if SETTINGS.env_name is not None:
1013 return SETTINGS.env_name == 'GCE_PRODUCTION'
1014
1015 if NO_GCE_CHECK != 'True' and _detect_gce_environment(urlopen=urlopen):
1016 SETTINGS.env_name = 'GCE_PRODUCTION'
1017 return True
1018 return False
1019
1020
1021 class GoogleCredentials(OAuth2Credentials):
1022 """Application Default Credentials for use in calling Google APIs.
1023
1024 The Application Default Credentials are being constructed as a function of
1025 the environment where the code is being run.
1026 More details can be found on this page:
1027 https://developers.google.com/accounts/docs/application-default-credentials
1028
1029 Here is an example of how to use the Application Default Credentials for a
1030 service that requires authentication:
1031
1032 from googleapiclient.discovery import build
1033 from oauth2client.client import GoogleCredentials
1034
1035 credentials = GoogleCredentials.get_application_default()
1036 service = build('compute', 'v1', credentials=credentials)
1037
1038 PROJECT = 'bamboo-machine-422'
1039 ZONE = 'us-central1-a'
1040 request = service.instances().list(project=PROJECT, zone=ZONE)
1041 response = request.execute()
1042
1043 print(response)
1044 """
1045
1046 def __init__(self, access_token, client_id, client_secret, refresh_token,
1047 token_expiry, token_uri, user_agent,
1048 revoke_uri=GOOGLE_REVOKE_URI):
1049 """Create an instance of GoogleCredentials.
1050
1051 This constructor is not usually called by the user, instead
1052 GoogleCredentials objects are instantiated by
1053 GoogleCredentials.from_stream() or
1054 GoogleCredentials.get_application_default().
1055
1056 Args:
1057 access_token: string, access token.
1058 client_id: string, client identifier.
1059 client_secret: string, client secret.
1060 refresh_token: string, refresh token.
1061 token_expiry: datetime, when the access_token expires.
1062 token_uri: string, URI of token endpoint.
1063 user_agent: string, The HTTP User-Agent to provide for this application.
1064 revoke_uri: string, URI for revoke endpoint.
1065 Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None.
1066 """
1067 super(GoogleCredentials, self).__init__(
1068 access_token, client_id, client_secret, refresh_token, token_expiry,
1069 token_uri, user_agent, revoke_uri=revoke_uri)
1070
1071 def create_scoped_required(self):
1072 """Whether this Credentials object is scopeless.
1073
1074 create_scoped(scopes) method needs to be called in order to create
1075 a Credentials object for API calls.
1076 """
1077 return False
1078
1079 def create_scoped(self, scopes):
1080 """Create a Credentials object for the given scopes.
1081
1082 The Credentials type is preserved.
1083 """
1084 return self
1085
1086 @property
1087 def serialization_data(self):
1088 """Get the fields and their values identifying the current credentials."""
1089 return {
1090 'type': 'authorized_user',
1091 'client_id': self.client_id,
1092 'client_secret': self.client_secret,
1093 'refresh_token': self.refresh_token
1094 }
1095
1096 @staticmethod
1097 def _implicit_credentials_from_gae():
1098 """Attempts to get implicit credentials in Google App Engine env.
1099
1100 If the current environment is not detected as App Engine, returns None,
1101 indicating no Google App Engine credentials can be detected from the
1102 current environment.
1103
1104 Returns:
1105 None, if not in GAE, else an appengine.AppAssertionCredentials object.
1106 """
1107 if not _in_gae_environment():
1108 return None
1109
1110 return _get_application_default_credential_GAE()
1111
1112 @staticmethod
1113 def _implicit_credentials_from_gce():
1114 """Attempts to get implicit credentials in Google Compute Engine env.
1115
1116 If the current environment is not detected as Compute Engine, returns None,
1117 indicating no Google Compute Engine credentials can be detected from the
1118 current environment.
1119
1120 Returns:
1121 None, if not in GCE, else a gce.AppAssertionCredentials object.
1122 """
1123 if not _in_gce_environment():
1124 return None
1125
1126 return _get_application_default_credential_GCE()
1127
1128 @staticmethod
1129 def _implicit_credentials_from_files():
1130 """Attempts to get implicit credentials from local credential files.
1131
1132 First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS
1133 is set with a filename and then falls back to a configuration file (the
1134 "well known" file) associated with the 'gcloud' command line tool.
1135
1136 Returns:
1137 Credentials object associated with the GOOGLE_APPLICATION_CREDENTIALS
1138 file or the "well known" file if either exist. If neither file is
1139 define, returns None, indicating no credentials from a file can
1140 detected from the current environment.
1141 """
1142 credentials_filename = _get_environment_variable_file()
1143 if not credentials_filename:
1144 credentials_filename = _get_well_known_file()
1145 if os.path.isfile(credentials_filename):
1146 extra_help = (' (produced automatically when running'
1147 ' "gcloud auth login" command)')
1148 else:
1149 credentials_filename = None
1150 else:
1151 extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS +
1152 ' environment variable)')
1153
1154 if not credentials_filename:
1155 return
1156
1157 # If we can read the credentials from a file, we don't need to know what
1158 # environment we are in.
1159 SETTINGS.env_name = DEFAULT_ENV_NAME
1160
1161 try:
1162 return _get_application_default_credential_from_file(credentials_filename)
1163 except (ApplicationDefaultCredentialsError, ValueError) as error:
1164 _raise_exception_for_reading_json(credentials_filename, extra_help, error)
1165
1166 @classmethod
1167 def _get_implicit_credentials(cls):
1168 """Gets credentials implicitly from the environment.
1169
1170 Checks environment in order of precedence:
1171 - Google App Engine (production and testing)
1172 - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to
1173 a file with stored credentials information.
1174 - Stored "well known" file associated with `gcloud` command line tool.
1175 - Google Compute Engine production environment.
1176
1177 Exceptions:
1178 ApplicationDefaultCredentialsError: raised when the credentials fail
1179 to be retrieved.
1180 """
1181
1182 # Environ checks (in order).
1183 environ_checkers = [
1184 cls._implicit_credentials_from_gae,
1185 cls._implicit_credentials_from_files,
1186 cls._implicit_credentials_from_gce,
1187 ]
1188
1189 for checker in environ_checkers:
1190 credentials = checker()
1191 if credentials is not None:
1192 return credentials
1193
1194 # If no credentials, fail.
1195 raise ApplicationDefaultCredentialsError(ADC_HELP_MSG)
1196
1197 @staticmethod
1198 def get_application_default():
1199 """Get the Application Default Credentials for the current environment.
1200
1201 Exceptions:
1202 ApplicationDefaultCredentialsError: raised when the credentials fail
1203 to be retrieved.
1204 """
1205 return GoogleCredentials._get_implicit_credentials()
1206
1207 @staticmethod
1208 def from_stream(credential_filename):
1209 """Create a Credentials object by reading the information from a given file.
1210
1211 It returns an object of type GoogleCredentials.
1212
1213 Args:
1214 credential_filename: the path to the file from where the credentials
1215 are to be read
1216
1217 Exceptions:
1218 ApplicationDefaultCredentialsError: raised when the credentials fail
1219 to be retrieved.
1220 """
1221
1222 if credential_filename and os.path.isfile(credential_filename):
1223 try:
1224 return _get_application_default_credential_from_file(
1225 credential_filename)
1226 except (ApplicationDefaultCredentialsError, ValueError) as error:
1227 extra_help = ' (provided as parameter to the from_stream() method)'
1228 _raise_exception_for_reading_json(credential_filename,
1229 extra_help,
1230 error)
1231 else:
1232 raise ApplicationDefaultCredentialsError(
1233 'The parameter passed to the from_stream() '
1234 'method should point to a file.')
1235
1236
1237 def _save_private_file(filename, json_contents):
1238 """Saves a file with read-write permissions on for the owner.
1239
1240 Args:
1241 filename: String. Absolute path to file.
1242 json_contents: JSON serializable object to be saved.
1243 """
1244 temp_filename = tempfile.mktemp()
1245 file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600)
1246 with os.fdopen(file_desc, 'w') as file_handle:
1247 json.dump(json_contents, file_handle, sort_keys=True,
1248 indent=2, separators=(',', ': '))
1249 shutil.move(temp_filename, filename)
1250
1251
1252 def save_to_well_known_file(credentials, well_known_file=None):
1253 """Save the provided GoogleCredentials to the well known file.
1254
1255 Args:
1256 credentials:
1257 the credentials to be saved to the well known file;
1258 it should be an instance of GoogleCredentials
1259 well_known_file:
1260 the name of the file where the credentials are to be saved;
1261 this parameter is supposed to be used for testing only
1262 """
1263 # TODO(orestica): move this method to tools.py
1264 # once the argparse import gets fixed (it is not present in Python 2.6)
1265
1266 if well_known_file is None:
1267 well_known_file = _get_well_known_file()
1268
1269 config_dir = os.path.dirname(well_known_file)
1270 if not os.path.isdir(config_dir):
1271 raise OSError('Config directory does not exist: %s' % config_dir)
1272
1273 credentials_data = credentials.serialization_data
1274 _save_private_file(well_known_file, credentials_data)
1275
1276
1277 def _get_environment_variable_file():
1278 application_default_credential_filename = (
1279 os.environ.get(GOOGLE_APPLICATION_CREDENTIALS,
1280 None))
1281
1282 if application_default_credential_filename:
1283 if os.path.isfile(application_default_credential_filename):
1284 return application_default_credential_filename
1285 else:
1286 raise ApplicationDefaultCredentialsError(
1287 'File ' + application_default_credential_filename + ' (pointed by ' +
1288 GOOGLE_APPLICATION_CREDENTIALS +
1289 ' environment variable) does not exist!')
1290
1291
1292 def _get_well_known_file():
1293 """Get the well known file produced by command 'gcloud auth login'."""
1294 # TODO(orestica): Revisit this method once gcloud provides a better way
1295 # of pinpointing the exact location of the file.
1296
1297 WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
1298
1299 default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR)
1300 if default_config_dir is None:
1301 if os.name == 'nt':
1302 try:
1303 default_config_dir = os.path.join(os.environ['APPDATA'],
1304 _CLOUDSDK_CONFIG_DIRECTORY)
1305 except KeyError:
1306 # This should never happen unless someone is really messing with things.
1307 drive = os.environ.get('SystemDrive', 'C:')
1308 default_config_dir = os.path.join(drive, '\\',
1309 _CLOUDSDK_CONFIG_DIRECTORY)
1310 else:
1311 default_config_dir = os.path.join(os.path.expanduser('~'),
1312 '.config',
1313 _CLOUDSDK_CONFIG_DIRECTORY)
1314
1315 return os.path.join(default_config_dir, WELL_KNOWN_CREDENTIALS_FILE)
1316
1317
1318 def _get_application_default_credential_from_file(filename):
1319 """Build the Application Default Credentials from file."""
1320
1321 from oauth2client import service_account
1322
1323 # read the credentials from the file
1324 with open(filename) as file_obj:
1325 client_credentials = json.load(file_obj)
1326
1327 credentials_type = client_credentials.get('type')
1328 if credentials_type == AUTHORIZED_USER:
1329 required_fields = set(['client_id', 'client_secret', 'refresh_token'])
1330 elif credentials_type == SERVICE_ACCOUNT:
1331 required_fields = set(['client_id', 'client_email', 'private_key_id',
1332 'private_key'])
1333 else:
1334 raise ApplicationDefaultCredentialsError(
1335 "'type' field should be defined (and have one of the '" +
1336 AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)")
1337
1338 missing_fields = required_fields.difference(client_credentials.keys())
1339
1340 if missing_fields:
1341 _raise_exception_for_missing_fields(missing_fields)
1342
1343 if client_credentials['type'] == AUTHORIZED_USER:
1344 return GoogleCredentials(
1345 access_token=None,
1346 client_id=client_credentials['client_id'],
1347 client_secret=client_credentials['client_secret'],
1348 refresh_token=client_credentials['refresh_token'],
1349 token_expiry=None,
1350 token_uri=GOOGLE_TOKEN_URI,
1351 user_agent='Python client library')
1352 else: # client_credentials['type'] == SERVICE_ACCOUNT
1353 return service_account._ServiceAccountCredentials(
1354 service_account_id=client_credentials['client_id'],
1355 service_account_email=client_credentials['client_email'],
1356 private_key_id=client_credentials['private_key_id'],
1357 private_key_pkcs8_text=client_credentials['private_key'],
1358 scopes=[])
1359
1360
1361 def _raise_exception_for_missing_fields(missing_fields):
1362 raise ApplicationDefaultCredentialsError(
1363 'The following field(s) must be defined: ' + ', '.join(missing_fields))
1364
1365
1366 def _raise_exception_for_reading_json(credential_file,
1367 extra_help,
1368 error):
1369 raise ApplicationDefaultCredentialsError(
1370 'An error was encountered while reading json file: '+
1371 credential_file + extra_help + ': ' + str(error))
1372
1373
1374 def _get_application_default_credential_GAE():
1375 from oauth2client.appengine import AppAssertionCredentials
1376
1377 return AppAssertionCredentials([])
1378
1379
1380 def _get_application_default_credential_GCE():
1381 from oauth2client.gce import AppAssertionCredentials
1382
1383 return AppAssertionCredentials([])
1384
1385
1386 class AssertionCredentials(GoogleCredentials):
575 """Abstract Credentials object used for OAuth 2.0 assertion grants. 1387 """Abstract Credentials object used for OAuth 2.0 assertion grants.
576 1388
577 This credential does not require a flow to instantiate because it 1389 This credential does not require a flow to instantiate because it
578 represents a two legged flow, and therefore has all of the required 1390 represents a two legged flow, and therefore has all of the required
579 information to generate and refresh its own access tokens. It must 1391 information to generate and refresh its own access tokens. It must
580 be subclassed to generate the appropriate assertion string. 1392 be subclassed to generate the appropriate assertion string.
581 1393
582 AssertionCredentials objects may be safely pickled and unpickled. 1394 AssertionCredentials objects may be safely pickled and unpickled.
583 """ 1395 """
584 1396
585 def __init__(self, assertion_type, user_agent, 1397 @util.positional(2)
586 token_uri='https://accounts.google.com/o/oauth2/token', 1398 def __init__(self, assertion_type, user_agent=None,
1399 token_uri=GOOGLE_TOKEN_URI,
1400 revoke_uri=GOOGLE_REVOKE_URI,
587 **unused_kwargs): 1401 **unused_kwargs):
588 """Constructor for AssertionFlowCredentials. 1402 """Constructor for AssertionFlowCredentials.
589 1403
590 Args: 1404 Args:
591 assertion_type: string, assertion type that will be declared to the auth 1405 assertion_type: string, assertion type that will be declared to the auth
592 server 1406 server
593 user_agent: string, The HTTP User-Agent to provide for this application. 1407 user_agent: string, The HTTP User-Agent to provide for this application.
594 token_uri: string, URI for token endpoint. For convenience 1408 token_uri: string, URI for token endpoint. For convenience
595 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1409 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1410 revoke_uri: string, URI for revoke endpoint.
596 """ 1411 """
597 super(AssertionCredentials, self).__init__( 1412 super(AssertionCredentials, self).__init__(
598 None, 1413 None,
599 None, 1414 None,
600 None, 1415 None,
601 None, 1416 None,
602 None, 1417 None,
603 token_uri, 1418 token_uri,
604 user_agent) 1419 user_agent,
1420 revoke_uri=revoke_uri)
605 self.assertion_type = assertion_type 1421 self.assertion_type = assertion_type
606 1422
607 def _generate_refresh_request_body(self): 1423 def _generate_refresh_request_body(self):
608 assertion = self._generate_assertion() 1424 assertion = self._generate_assertion()
609 1425
610 body = urllib.urlencode({ 1426 body = urllib.parse.urlencode({
611 'assertion_type': self.assertion_type,
612 'assertion': assertion, 1427 'assertion': assertion,
613 'grant_type': 'assertion', 1428 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
614 }) 1429 })
615 1430
616 return body 1431 return body
617 1432
618 def _generate_assertion(self): 1433 def _generate_assertion(self):
619 """Generate the assertion string that will be used in the access token 1434 """Generate the assertion string that will be used in the access token
620 request. 1435 request.
621 """ 1436 """
622 _abstract() 1437 _abstract()
623 1438
624 if HAS_OPENSSL: 1439 def _revoke(self, http_request):
625 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then 1440 """Revokes the access_token and deletes the store if available.
626 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
627 # method.
628 1441
629 class SignedJwtAssertionCredentials(AssertionCredentials): 1442 Args:
630 """Credentials object used for OAuth 2.0 Signed JWT assertion grants. 1443 http_request: callable, a callable that matches the method signature of
631 1444 httplib2.Http.request, used to make the revoke request.
632 This credential does not require a flow to instantiate because it
633 represents a two legged flow, and therefore has all of the required
634 information to generate and refresh its own access tokens.
635 """ 1445 """
636 1446 self._do_revoke(http_request, self.access_token)
637 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
638
639 def __init__(self,
640 service_account_name,
641 private_key,
642 scope,
643 private_key_password='notasecret',
644 user_agent=None,
645 token_uri='https://accounts.google.com/o/oauth2/token',
646 **kwargs):
647 """Constructor for SignedJwtAssertionCredentials.
648
649 Args:
650 service_account_name: string, id for account, usually an email address.
651 private_key: string, private key in P12 format.
652 scope: string or list of strings, scope(s) of the credentials being
653 requested.
654 private_key_password: string, password for private_key.
655 user_agent: string, HTTP User-Agent to provide for this application.
656 token_uri: string, URI for token endpoint. For convenience
657 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
658 kwargs: kwargs, Additional parameters to add to the JWT token, for
659 example prn=joe@xample.org."""
660
661 super(SignedJwtAssertionCredentials, self).__init__(
662 'http://oauth.net/grant_type/jwt/1.0/bearer',
663 user_agent,
664 token_uri=token_uri,
665 )
666
667 if type(scope) is list:
668 scope = ' '.join(scope)
669 self.scope = scope
670
671 self.private_key = private_key
672 self.private_key_password = private_key_password
673 self.service_account_name = service_account_name
674 self.kwargs = kwargs
675
676 @classmethod
677 def from_json(cls, s):
678 data = simplejson.loads(s)
679 retval = SignedJwtAssertionCredentials(
680 data['service_account_name'],
681 data['private_key'],
682 data['private_key_password'],
683 data['scope'],
684 data['user_agent'],
685 data['token_uri'],
686 data['kwargs']
687 )
688 retval.invalid = data['invalid']
689 return retval
690
691 def _generate_assertion(self):
692 """Generate the assertion that will be used in the request."""
693 now = long(time.time())
694 payload = {
695 'aud': self.token_uri,
696 'scope': self.scope,
697 'iat': now,
698 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
699 'iss': self.service_account_name
700 }
701 payload.update(self.kwargs)
702 logging.debug(str(payload))
703
704 return make_signed_jwt(
705 Signer.from_string(self.private_key, self.private_key_password),
706 payload)
707 1447
708 1448
709 def verify_id_token(id_token, audience, http=None, 1449 def _RequireCryptoOrDie():
710 cert_uri=ID_TOKEN_VERIFICATON_CERTS): 1450 """Ensure we have a crypto library, or throw CryptoUnavailableError.
711 """Verifies a signed JWT id_token. 1451
1452 The oauth2client.crypt module requires either PyCrypto or PyOpenSSL
1453 to be available in order to function, but these are optional
1454 dependencies.
1455 """
1456 if not HAS_CRYPTO:
1457 raise CryptoUnavailableError('No crypto library available')
1458
1459
1460 class SignedJwtAssertionCredentials(AssertionCredentials):
1461 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
1462
1463 This credential does not require a flow to instantiate because it
1464 represents a two legged flow, and therefore has all of the required
1465 information to generate and refresh its own access tokens.
1466
1467 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto
1468 2.6 or later. For App Engine you may also consider using
1469 AppAssertionCredentials.
1470 """
1471
1472 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
1473
1474 @util.positional(4)
1475 def __init__(self,
1476 service_account_name,
1477 private_key,
1478 scope,
1479 private_key_password='notasecret',
1480 user_agent=None,
1481 token_uri=GOOGLE_TOKEN_URI,
1482 revoke_uri=GOOGLE_REVOKE_URI,
1483 **kwargs):
1484 """Constructor for SignedJwtAssertionCredentials.
712 1485
713 Args: 1486 Args:
714 id_token: string, A Signed JWT. 1487 service_account_name: string, id for account, usually an email address.
715 audience: string, The audience 'aud' that the token should be for. 1488 private_key: string, private key in PKCS12 or PEM format.
716 http: httplib2.Http, instance to use to make the HTTP request. Callers 1489 scope: string or iterable of strings, scope(s) of the credentials being
717 should supply an instance that has caching enabled. 1490 requested.
718 cert_uri: string, URI of the certificates in JSON format to 1491 private_key_password: string, password for private_key, unused if
719 verify the JWT against. 1492 private_key is in PEM format.
720 1493 user_agent: string, HTTP User-Agent to provide for this application.
721 Returns: 1494 token_uri: string, URI for token endpoint. For convenience
722 The deserialized JSON in the JWT. 1495 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1496 revoke_uri: string, URI for revoke endpoint.
1497 kwargs: kwargs, Additional parameters to add to the JWT token, for
1498 example sub=joe@xample.org.
723 1499
724 Raises: 1500 Raises:
725 oauth2client.crypt.AppIdentityError if the JWT fails to verify. 1501 CryptoUnavailableError if no crypto library is available.
726 """ 1502 """
727 if http is None: 1503 _RequireCryptoOrDie()
728 http = httplib2.Http() 1504 super(SignedJwtAssertionCredentials, self).__init__(
1505 None,
1506 user_agent=user_agent,
1507 token_uri=token_uri,
1508 revoke_uri=revoke_uri,
1509 )
729 1510
730 resp, content = http.request(cert_uri) 1511 self.scope = util.scopes_to_string(scope)
731 1512
732 if resp.status == 200: 1513 # Keep base64 encoded so it can be stored in JSON.
733 certs = simplejson.loads(content) 1514 self.private_key = base64.b64encode(private_key)
734 return verify_signed_jwt_with_certs(id_token, certs, audience) 1515 if isinstance(self.private_key, six.text_type):
735 else: 1516 self.private_key = self.private_key.encode('utf-8')
736 raise VerifyJwtTokenError('Status code: %d' % resp.status)
737 1517
1518 self.private_key_password = private_key_password
1519 self.service_account_name = service_account_name
1520 self.kwargs = kwargs
738 1521
739 def _urlsafe_b64decode(b64string): 1522 @classmethod
740 # Guard against unicode strings, which base64 can't handle. 1523 def from_json(cls, s):
741 b64string = b64string.encode('ascii') 1524 data = json.loads(s)
742 padded = b64string + '=' * (4 - len(b64string) % 4) 1525 retval = SignedJwtAssertionCredentials(
743 return base64.urlsafe_b64decode(padded) 1526 data['service_account_name'],
1527 base64.b64decode(data['private_key']),
1528 data['scope'],
1529 private_key_password=data['private_key_password'],
1530 user_agent=data['user_agent'],
1531 token_uri=data['token_uri'],
1532 **data['kwargs']
1533 )
1534 retval.invalid = data['invalid']
1535 retval.access_token = data['access_token']
1536 return retval
1537
1538 def _generate_assertion(self):
1539 """Generate the assertion that will be used in the request."""
1540 now = int(time.time())
1541 payload = {
1542 'aud': self.token_uri,
1543 'scope': self.scope,
1544 'iat': now,
1545 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
1546 'iss': self.service_account_name
1547 }
1548 payload.update(self.kwargs)
1549 logger.debug(str(payload))
1550
1551 private_key = base64.b64decode(self.private_key)
1552 return crypt.make_signed_jwt(crypt.Signer.from_string(
1553 private_key, self.private_key_password), payload)
1554
1555 # Only used in verify_id_token(), which is always calling to the same URI
1556 # for the certs.
1557 _cached_http = httplib2.Http(MemoryCache())
1558
1559 @util.positional(2)
1560 def verify_id_token(id_token, audience, http=None,
1561 cert_uri=ID_TOKEN_VERIFICATION_CERTS):
1562 """Verifies a signed JWT id_token.
1563
1564 This function requires PyOpenSSL and because of that it does not work on
1565 App Engine.
1566
1567 Args:
1568 id_token: string, A Signed JWT.
1569 audience: string, The audience 'aud' that the token should be for.
1570 http: httplib2.Http, instance to use to make the HTTP request. Callers
1571 should supply an instance that has caching enabled.
1572 cert_uri: string, URI of the certificates in JSON format to
1573 verify the JWT against.
1574
1575 Returns:
1576 The deserialized JSON in the JWT.
1577
1578 Raises:
1579 oauth2client.crypt.AppIdentityError: if the JWT fails to verify.
1580 CryptoUnavailableError: if no crypto library is available.
1581 """
1582 _RequireCryptoOrDie()
1583 if http is None:
1584 http = _cached_http
1585
1586 resp, content = http.request(cert_uri)
1587
1588 if resp.status == 200:
1589 certs = json.loads(content.decode('utf-8'))
1590 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
1591 else:
1592 raise VerifyJwtTokenError('Status code: %d' % resp.status)
744 1593
745 1594
746 def _extract_id_token(id_token): 1595 def _extract_id_token(id_token):
747 """Extract the JSON payload from a JWT. 1596 """Extract the JSON payload from a JWT.
748 1597
749 Does the extraction w/o checking the signature. 1598 Does the extraction w/o checking the signature.
750 1599
751 Args: 1600 Args:
752 id_token: string, OAuth 2.0 id_token. 1601 id_token: string or bytestring, OAuth 2.0 id_token.
753 1602
754 Returns: 1603 Returns:
755 object, The deserialized JSON payload. 1604 object, The deserialized JSON payload.
756 """ 1605 """
757 segments = id_token.split('.') 1606 if type(id_token) == bytes:
758 1607 segments = id_token.split(b'.')
759 if (len(segments) != 3): 1608 else:
1609 segments = id_token.split(u'.')
1610
1611 if len(segments) != 3:
760 raise VerifyJwtTokenError( 1612 raise VerifyJwtTokenError(
761 'Wrong number of segments in token: %s' % id_token) 1613 'Wrong number of segments in token: %s' % id_token)
762 1614
763 return simplejson.loads(_urlsafe_b64decode(segments[1])) 1615 return json.loads(_urlsafe_b64decode(segments[1]).decode('utf-8'))
764 1616
1617
1618 def _parse_exchange_token_response(content):
1619 """Parses response of an exchange token request.
1620
1621 Most providers return JSON but some (e.g. Facebook) return a
1622 url-encoded string.
1623
1624 Args:
1625 content: The body of a response
1626
1627 Returns:
1628 Content as a dictionary object. Note that the dict could be empty,
1629 i.e. {}. That basically indicates a failure.
1630 """
1631 resp = {}
1632 try:
1633 resp = json.loads(content.decode('utf-8'))
1634 except Exception:
1635 # different JSON libs raise different exceptions,
1636 # so we just do a catch-all here
1637 content = content.decode('utf-8')
1638 resp = dict(urllib.parse.parse_qsl(content))
1639
1640 # some providers respond with 'expires', others with 'expires_in'
1641 if resp and 'expires' in resp:
1642 resp['expires_in'] = resp.pop('expires')
1643
1644 return resp
1645
1646
1647 @util.positional(4)
1648 def credentials_from_code(client_id, client_secret, scope, code,
1649 redirect_uri='postmessage', http=None,
1650 user_agent=None, token_uri=GOOGLE_TOKEN_URI,
1651 auth_uri=GOOGLE_AUTH_URI,
1652 revoke_uri=GOOGLE_REVOKE_URI,
1653 device_uri=GOOGLE_DEVICE_URI):
1654 """Exchanges an authorization code for an OAuth2Credentials object.
1655
1656 Args:
1657 client_id: string, client identifier.
1658 client_secret: string, client secret.
1659 scope: string or iterable of strings, scope(s) to request.
1660 code: string, An authorization code, most likely passed down from
1661 the client
1662 redirect_uri: string, this is generally set to 'postmessage' to match the
1663 redirect_uri that the client specified
1664 http: httplib2.Http, optional http instance to use to do the fetch
1665 token_uri: string, URI for token endpoint. For convenience
1666 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1667 auth_uri: string, URI for authorization endpoint. For convenience
1668 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1669 revoke_uri: string, URI for revoke endpoint. For convenience
1670 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1671 device_uri: string, URI for device authorization endpoint. For convenience
1672 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1673
1674 Returns:
1675 An OAuth2Credentials object.
1676
1677 Raises:
1678 FlowExchangeError if the authorization code cannot be exchanged for an
1679 access token
1680 """
1681 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
1682 redirect_uri=redirect_uri, user_agent=user_agent,
1683 auth_uri=auth_uri, token_uri=token_uri,
1684 revoke_uri=revoke_uri, device_uri=device_uri)
1685
1686 credentials = flow.step2_exchange(code, http=http)
1687 return credentials
1688
1689
1690 @util.positional(3)
1691 def credentials_from_clientsecrets_and_code(filename, scope, code,
1692 message = None,
1693 redirect_uri='postmessage',
1694 http=None,
1695 cache=None,
1696 device_uri=None):
1697 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
1698
1699 Will create the right kind of Flow based on the contents of the clientsecrets
1700 file or will raise InvalidClientSecretsError for unknown types of Flows.
1701
1702 Args:
1703 filename: string, File name of clientsecrets.
1704 scope: string or iterable of strings, scope(s) to request.
1705 code: string, An authorization code, most likely passed down from
1706 the client
1707 message: string, A friendly string to display to the user if the
1708 clientsecrets file is missing or invalid. If message is provided then
1709 sys.exit will be called in the case of an error. If message in not
1710 provided then clientsecrets.InvalidClientSecretsError will be raised.
1711 redirect_uri: string, this is generally set to 'postmessage' to match the
1712 redirect_uri that the client specified
1713 http: httplib2.Http, optional http instance to use to do the fetch
1714 cache: An optional cache service client that implements get() and set()
1715 methods. See clientsecrets.loadfile() for details.
1716 device_uri: string, OAuth 2.0 device authorization endpoint
1717
1718 Returns:
1719 An OAuth2Credentials object.
1720
1721 Raises:
1722 FlowExchangeError if the authorization code cannot be exchanged for an
1723 access token
1724 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1725 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1726 invalid.
1727 """
1728 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1729 redirect_uri=redirect_uri,
1730 device_uri=device_uri)
1731 credentials = flow.step2_exchange(code, http=http)
1732 return credentials
1733
1734
1735 class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
1736 'device_code', 'user_code', 'interval', 'verification_url',
1737 'user_code_expiry'))):
1738 """Intermediate information the OAuth2 for devices flow."""
1739
1740 @classmethod
1741 def FromResponse(cls, response):
1742 """Create a DeviceFlowInfo from a server response.
1743
1744 The response should be a dict containing entries as described here:
1745
1746 http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1
1747 """
1748 # device_code, user_code, and verification_url are required.
1749 kwargs = {
1750 'device_code': response['device_code'],
1751 'user_code': response['user_code'],
1752 }
1753 # The response may list the verification address as either
1754 # verification_url or verification_uri, so we check for both.
1755 verification_url = response.get(
1756 'verification_url', response.get('verification_uri'))
1757 if verification_url is None:
1758 raise OAuth2DeviceCodeError(
1759 'No verification_url provided in server response')
1760 kwargs['verification_url'] = verification_url
1761 # expires_in and interval are optional.
1762 kwargs.update({
1763 'interval': response.get('interval'),
1764 'user_code_expiry': None,
1765 })
1766 if 'expires_in' in response:
1767 kwargs['user_code_expiry'] = datetime.datetime.now() + datetime.timedelta(
1768 seconds=int(response['expires_in']))
1769
1770 return cls(**kwargs)
765 1771
766 class OAuth2WebServerFlow(Flow): 1772 class OAuth2WebServerFlow(Flow):
767 """Does the Web Server Flow for OAuth 2.0. 1773 """Does the Web Server Flow for OAuth 2.0.
768 1774
769 OAuth2Credentials objects may be safely pickled and unpickled. 1775 OAuth2WebServerFlow objects may be safely pickled and unpickled.
770 """ 1776 """
771 1777
772 def __init__(self, client_id, client_secret, scope, user_agent=None, 1778 @util.positional(4)
773 auth_uri='https://accounts.google.com/o/oauth2/auth', 1779 def __init__(self, client_id,
774 token_uri='https://accounts.google.com/o/oauth2/token', 1780 client_secret=None,
1781 scope=None,
1782 redirect_uri=None,
1783 user_agent=None,
1784 auth_uri=GOOGLE_AUTH_URI,
1785 token_uri=GOOGLE_TOKEN_URI,
1786 revoke_uri=GOOGLE_REVOKE_URI,
1787 login_hint=None,
1788 device_uri=GOOGLE_DEVICE_URI,
1789 authorization_header=None,
775 **kwargs): 1790 **kwargs):
776 """Constructor for OAuth2WebServerFlow. 1791 """Constructor for OAuth2WebServerFlow.
777 1792
1793 The kwargs argument is used to set extra query parameters on the
1794 auth_uri. For example, the access_type and approval_prompt
1795 query parameters can be set via kwargs.
1796
778 Args: 1797 Args:
779 client_id: string, client identifier. 1798 client_id: string, client identifier.
780 client_secret: string client secret. 1799 client_secret: string client secret.
781 scope: string or list of strings, scope(s) of the credentials being 1800 scope: string or iterable of strings, scope(s) of the credentials being
782 requested. 1801 requested.
1802 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1803 a non-web-based application, or a URI that handles the callback from
1804 the authorization server.
783 user_agent: string, HTTP User-Agent to provide for this application. 1805 user_agent: string, HTTP User-Agent to provide for this application.
784 auth_uri: string, URI for authorization endpoint. For convenience 1806 auth_uri: string, URI for authorization endpoint. For convenience
785 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1807 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
786 token_uri: string, URI for token endpoint. For convenience 1808 token_uri: string, URI for token endpoint. For convenience
787 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1809 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1810 revoke_uri: string, URI for revoke endpoint. For convenience
1811 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1812 login_hint: string, Either an email address or domain. Passing this hint
1813 will either pre-fill the email box on the sign-in form or select the
1814 proper multi-login session, thereby simplifying the login flow.
1815 device_uri: string, URI for device authorization endpoint. For convenience
1816 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1817 authorization_header: string, For use with OAuth 2.0 providers that
1818 require a client to authenticate using a header value instead of passing
1819 client_secret in the POST body.
788 **kwargs: dict, The keyword arguments are all optional and required 1820 **kwargs: dict, The keyword arguments are all optional and required
789 parameters for the OAuth calls. 1821 parameters for the OAuth calls.
790 """ 1822 """
1823 # scope is a required argument, but to preserve backwards-compatibility
1824 # we don't want to rearrange the positional arguments
1825 if scope is None:
1826 raise TypeError("The value of scope must not be None")
791 self.client_id = client_id 1827 self.client_id = client_id
792 self.client_secret = client_secret 1828 self.client_secret = client_secret
793 if type(scope) is list: 1829 self.scope = util.scopes_to_string(scope)
794 scope = ' '.join(scope) 1830 self.redirect_uri = redirect_uri
795 self.scope = scope 1831 self.login_hint = login_hint
796 self.user_agent = user_agent 1832 self.user_agent = user_agent
797 self.auth_uri = auth_uri 1833 self.auth_uri = auth_uri
798 self.token_uri = token_uri 1834 self.token_uri = token_uri
1835 self.revoke_uri = revoke_uri
1836 self.device_uri = device_uri
1837 self.authorization_header = authorization_header
799 self.params = { 1838 self.params = {
800 'access_type': 'offline', 1839 'access_type': 'offline',
801 } 1840 'response_type': 'code',
1841 }
802 self.params.update(kwargs) 1842 self.params.update(kwargs)
803 self.redirect_uri = None 1843
804 1844 @util.positional(1)
805 def step1_get_authorize_url(self, redirect_uri='oob'): 1845 def step1_get_authorize_url(self, redirect_uri=None, state=None):
806 """Returns a URI to redirect to the provider. 1846 """Returns a URI to redirect to the provider.
807 1847
808 Args: 1848 Args:
809 redirect_uri: string, Either the string 'oob' for a non-web-based 1849 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
810 application, or a URI that handles the callback from 1850 a non-web-based application, or a URI that handles the callback from
811 the authorization server. 1851 the authorization server. This parameter is deprecated, please move to
812 1852 passing the redirect_uri in via the constructor.
813 If redirect_uri is 'oob' then pass in the 1853 state: string, Opaque state string which is passed through the OAuth2 flow
814 generated verification code to step2_exchange, 1854 and returned to the client as a query parameter in the callback.
815 otherwise pass in the query parameters received 1855
816 at the callback uri to step2_exchange. 1856 Returns:
817 """ 1857 A URI as a string to redirect the user to begin the authorization flow.
818 1858 """
819 self.redirect_uri = redirect_uri 1859 if redirect_uri is not None:
820 query = { 1860 logger.warning((
821 'response_type': 'code', 1861 'The redirect_uri parameter for '
1862 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please '
1863 'move to passing the redirect_uri in via the constructor.'))
1864 self.redirect_uri = redirect_uri
1865
1866 if self.redirect_uri is None:
1867 raise ValueError('The value of redirect_uri must not be None.')
1868
1869 query_params = {
822 'client_id': self.client_id, 1870 'client_id': self.client_id,
823 'redirect_uri': redirect_uri,
824 'scope': self.scope,
825 }
826 query.update(self.params)
827 parts = list(urlparse.urlparse(self.auth_uri))
828 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
829 parts[4] = urllib.urlencode(query)
830 return urlparse.urlunparse(parts)
831
832 def step2_exchange(self, code, http=None):
833 """Exhanges a code for OAuth2Credentials.
834
835 Args:
836 code: string or dict, either the code as a string, or a dictionary
837 of the query parameters to the redirect_uri, which contains
838 the code.
839 http: httplib2.Http, optional http instance to use to do the fetch
840 """
841
842 if not (isinstance(code, str) or isinstance(code, unicode)):
843 code = code['code']
844
845 body = urllib.urlencode({
846 'grant_type': 'authorization_code',
847 'client_id': self.client_id,
848 'client_secret': self.client_secret,
849 'code': code,
850 'redirect_uri': self.redirect_uri, 1871 'redirect_uri': self.redirect_uri,
851 'scope': self.scope, 1872 'scope': self.scope,
852 }) 1873 }
1874 if state is not None:
1875 query_params['state'] = state
1876 if self.login_hint is not None:
1877 query_params['login_hint'] = self.login_hint
1878 query_params.update(self.params)
1879 return _update_query_params(self.auth_uri, query_params)
1880
1881 @util.positional(1)
1882 def step1_get_device_and_user_codes(self, http=None):
1883 """Returns a user code and the verification URL where to enter it
1884
1885 Returns:
1886 A user code as a string for the user to authorize the application
1887 An URL as a string where the user has to enter the code
1888 """
1889 if self.device_uri is None:
1890 raise ValueError('The value of device_uri must not be None.')
1891
1892 body = urllib.parse.urlencode({
1893 'client_id': self.client_id,
1894 'scope': self.scope,
1895 })
853 headers = { 1896 headers = {
854 'content-type': 'application/x-www-form-urlencoded', 1897 'content-type': 'application/x-www-form-urlencoded',
855 } 1898 }
856 1899
857 if self.user_agent is not None: 1900 if self.user_agent is not None:
858 headers['user-agent'] = self.user_agent 1901 headers['user-agent'] = self.user_agent
859 1902
1903 if http is None:
1904 http = httplib2.Http()
1905
1906 resp, content = http.request(self.device_uri, method='POST', body=body,
1907 headers=headers)
1908 if resp.status == 200:
1909 try:
1910 flow_info = json.loads(content)
1911 except ValueError as e:
1912 raise OAuth2DeviceCodeError(
1913 'Could not parse server response as JSON: "%s", error: "%s"' % (
1914 content, e))
1915 return DeviceFlowInfo.FromResponse(flow_info)
1916 else:
1917 error_msg = 'Invalid response %s.' % resp.status
1918 try:
1919 d = json.loads(content)
1920 if 'error' in d:
1921 error_msg += ' Error: %s' % d['error']
1922 except ValueError:
1923 # Couldn't decode a JSON response, stick with the default message.
1924 pass
1925 raise OAuth2DeviceCodeError(error_msg)
1926
1927 @util.positional(2)
1928 def step2_exchange(self, code=None, http=None, device_flow_info=None):
1929 """Exchanges a code for OAuth2Credentials.
1930
1931 Args:
1932
1933 code: string, a dict-like object, or None. For a non-device
1934 flow, this is either the response code as a string, or a
1935 dictionary of query parameters to the redirect_uri. For a
1936 device flow, this should be None.
1937 http: httplib2.Http, optional http instance to use when fetching
1938 credentials.
1939 device_flow_info: DeviceFlowInfo, return value from step1 in the
1940 case of a device flow.
1941
1942 Returns:
1943 An OAuth2Credentials object that can be used to authorize requests.
1944
1945 Raises:
1946 FlowExchangeError: if a problem occurred exchanging the code for a
1947 refresh_token.
1948 ValueError: if code and device_flow_info are both provided or both
1949 missing.
1950
1951 """
1952 if code is None and device_flow_info is None:
1953 raise ValueError('No code or device_flow_info provided.')
1954 if code is not None and device_flow_info is not None:
1955 raise ValueError('Cannot provide both code and device_flow_info.')
1956
1957 if code is None:
1958 code = device_flow_info.device_code
1959 elif not isinstance(code, six.string_types):
1960 if 'code' not in code:
1961 raise FlowExchangeError(code.get(
1962 'error', 'No code was supplied in the query parameters.'))
1963 code = code['code']
1964
1965 post_data = {
1966 'client_id': self.client_id,
1967 'code': code,
1968 'scope': self.scope,
1969 }
1970 if self.client_secret is not None:
1971 post_data['client_secret'] = self.client_secret
1972 if device_flow_info is not None:
1973 post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
1974 else:
1975 post_data['grant_type'] = 'authorization_code'
1976 post_data['redirect_uri'] = self.redirect_uri
1977 body = urllib.parse.urlencode(post_data)
1978 headers = {
1979 'content-type': 'application/x-www-form-urlencoded',
1980 }
1981 if self.authorization_header is not None:
1982 headers['Authorization'] = self.authorization_header
1983 if self.user_agent is not None:
1984 headers['user-agent'] = self.user_agent
1985
860 if http is None: 1986 if http is None:
861 http = httplib2.Http() 1987 http = httplib2.Http()
862 1988
863 resp, content = http.request(self.token_uri, method='POST', body=body, 1989 resp, content = http.request(self.token_uri, method='POST', body=body,
864 headers=headers) 1990 headers=headers)
865 if resp.status == 200: 1991 d = _parse_exchange_token_response(content)
866 # TODO(jcgregorio) Raise an error if simplejson.loads fails? 1992 if resp.status == 200 and 'access_token' in d:
867 d = simplejson.loads(content)
868 access_token = d['access_token'] 1993 access_token = d['access_token']
869 refresh_token = d.get('refresh_token', None) 1994 refresh_token = d.get('refresh_token', None)
1995 if not refresh_token:
1996 logger.info(
1997 'Received token response with no refresh_token. Consider '
1998 "reauthenticating with approval_prompt='force'.")
870 token_expiry = None 1999 token_expiry = None
871 if 'expires_in' in d: 2000 if 'expires_in' in d:
872 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( 2001 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
873 seconds=int(d['expires_in'])) 2002 seconds=int(d['expires_in']))
874 2003
2004 extracted_id_token = None
875 if 'id_token' in d: 2005 if 'id_token' in d:
876 d['id_token'] = _extract_id_token(d['id_token']) 2006 extracted_id_token = _extract_id_token(d['id_token'])
877 2007
878 logger.info('Successfully retrieved access token: %s' % content) 2008 logger.info('Successfully retrieved access token')
879 return OAuth2Credentials(access_token, self.client_id, 2009 return OAuth2Credentials(access_token, self.client_id,
880 self.client_secret, refresh_token, token_expiry, 2010 self.client_secret, refresh_token, token_expiry,
881 self.token_uri, self.user_agent, 2011 self.token_uri, self.user_agent,
882 id_token=d.get('id_token', None)) 2012 revoke_uri=self.revoke_uri,
2013 id_token=extracted_id_token,
2014 token_response=d)
883 else: 2015 else:
884 logger.error('Failed to retrieve access token: %s' % content) 2016 logger.info('Failed to retrieve access token: %s', content)
885 error_msg = 'Invalid response %s.' % resp['status'] 2017 if 'error' in d:
886 try: 2018 # you never know what those providers got to say
887 d = simplejson.loads(content) 2019 error_msg = str(d['error']) + str(d.get('error_description', ''))
888 if 'error' in d: 2020 else:
889 error_msg = d['error'] 2021 error_msg = 'Invalid response: %s.' % str(resp.status)
890 except:
891 pass
892
893 raise FlowExchangeError(error_msg) 2022 raise FlowExchangeError(error_msg)
894 2023
895 def flow_from_clientsecrets(filename, scope, message=None): 2024
2025 @util.positional(2)
2026 def flow_from_clientsecrets(filename, scope, redirect_uri=None,
2027 message=None, cache=None, login_hint=None,
2028 device_uri=None):
896 """Create a Flow from a clientsecrets file. 2029 """Create a Flow from a clientsecrets file.
897 2030
898 Will create the right kind of Flow based on the contents of the clientsecrets 2031 Will create the right kind of Flow based on the contents of the clientsecrets
899 file or will raise InvalidClientSecretsError for unknown types of Flows. 2032 file or will raise InvalidClientSecretsError for unknown types of Flows.
900 2033
901 Args: 2034 Args:
902 filename: string, File name of client secrets. 2035 filename: string, File name of client secrets.
903 scope: string or list of strings, scope(s) to request. 2036 scope: string or iterable of strings, scope(s) to request.
2037 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
2038 a non-web-based application, or a URI that handles the callback from
2039 the authorization server.
904 message: string, A friendly string to display to the user if the 2040 message: string, A friendly string to display to the user if the
905 clientsecrets file is missing or invalid. If message is provided then 2041 clientsecrets file is missing or invalid. If message is provided then
906 sys.exit will be called in the case of an error. If message in not 2042 sys.exit will be called in the case of an error. If message in not
907 provided then clientsecrets.InvalidClientSecretsError will be raised. 2043 provided then clientsecrets.InvalidClientSecretsError will be raised.
2044 cache: An optional cache service client that implements get() and set()
2045 methods. See clientsecrets.loadfile() for details.
2046 login_hint: string, Either an email address or domain. Passing this hint
2047 will either pre-fill the email box on the sign-in form or select the
2048 proper multi-login session, thereby simplifying the login flow.
2049 device_uri: string, URI for device authorization endpoint. For convenience
2050 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
908 2051
909 Returns: 2052 Returns:
910 A Flow object. 2053 A Flow object.
911 2054
912 Raises: 2055 Raises:
913 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 2056 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
914 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 2057 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
915 invalid. 2058 invalid.
916 """ 2059 """
917 try: 2060 try:
918 client_type, client_info = clientsecrets.loadfile(filename) 2061 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
919 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 2062 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED):
920 return OAuth2WebServerFlow( 2063 constructor_kwargs = {
921 client_info['client_id'], 2064 'redirect_uri': redirect_uri,
922 client_info['client_secret'], 2065 'auth_uri': client_info['auth_uri'],
923 scope, 2066 'token_uri': client_info['token_uri'],
924 None, # user_agent 2067 'login_hint': login_hint,
925 client_info['auth_uri'], 2068 }
926 client_info['token_uri']) 2069 revoke_uri = client_info.get('revoke_uri')
2070 if revoke_uri is not None:
2071 constructor_kwargs['revoke_uri'] = revoke_uri
2072 if device_uri is not None:
2073 constructor_kwargs['device_uri'] = device_uri
2074 return OAuth2WebServerFlow(
2075 client_info['client_id'], client_info['client_secret'],
2076 scope, **constructor_kwargs)
2077
927 except clientsecrets.InvalidClientSecretsError: 2078 except clientsecrets.InvalidClientSecretsError:
928 if message: 2079 if message:
929 sys.exit(message) 2080 sys.exit(message)
930 else: 2081 else:
931 raise 2082 raise
932 else: 2083 else:
933 raise UnknownClientSecretsFlowError( 2084 raise UnknownClientSecretsFlowError(
934 'This OAuth 2.0 flow is unsupported: "%s"' * client_type) 2085 'This OAuth 2.0 flow is unsupported: %r' % client_type)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698