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

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

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

Powered by Google App Engine
This is Rietveld 408576698