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

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

Issue 1085893002: Upgrade 3rd packages (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: rebase 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 (C) 2010 Google Inc. 1 # Copyright 2014 Google Inc. All rights reserved.
2 # 2 #
3 # Licensed under the Apache License, Version 2.0 (the "License"); 3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License. 4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at 5 # You may obtain a copy of the License at
6 # 6 #
7 # http://www.apache.org/licenses/LICENSE-2.0 7 # http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 # Unless required by applicable law or agreed to in writing, software 9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, 10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and 12 # See the License for the specific language governing permissions and
13 # limitations under the License. 13 # limitations under the License.
14 14
15 """An OAuth 2.0 client. 15 """An OAuth 2.0 client.
16 16
17 Tools for interacting with OAuth 2.0 protected resources. 17 Tools for interacting with OAuth 2.0 protected resources.
18 """ 18 """
19 19
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)' 20 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 21
22 import base64 22 import base64
23 import clientsecrets 23 import collections
24 import copy 24 import copy
25 import datetime 25 import datetime
26 import json
27 import logging
28 import os
29 import socket
30 import sys
31 import tempfile
32 import time
33 import shutil
34
26 from .. import httplib2 35 from .. import httplib2
27 import logging 36 from . import clientsecrets
28 import sys
29 import time
30 import urllib
31 import urlparse
32
33 from . import GOOGLE_AUTH_URI 37 from . import GOOGLE_AUTH_URI
38 from . import GOOGLE_DEVICE_URI
34 from . import GOOGLE_REVOKE_URI 39 from . import GOOGLE_REVOKE_URI
35 from . import GOOGLE_TOKEN_URI 40 from . import GOOGLE_TOKEN_URI
36 from . import util 41 from . import util
37 from .anyjson import simplejson 42 from third_party import six
43 from third_party.six.moves import urllib
38 44
39 HAS_OPENSSL = False 45 HAS_OPENSSL = False
40 HAS_CRYPTO = False 46 HAS_CRYPTO = False
41 try: 47 try:
42 from . import crypt 48 from oauth2client import crypt
Adrian Kuegel 2015/04/16 11:36:28 From the error message I gather that this was the
43 HAS_CRYPTO = True 49 HAS_CRYPTO = True
44 if crypt.OpenSSLVerifier is not None: 50 if crypt.OpenSSLVerifier is not None:
45 HAS_OPENSSL = True 51 HAS_OPENSSL = True
46 except ImportError: 52 except ImportError:
47 pass 53 pass
48 54
49 try:
50 from urlparse import parse_qsl
51 except ImportError:
52 from cgi import parse_qsl
53
54 logger = logging.getLogger(__name__) 55 logger = logging.getLogger(__name__)
55 56
56 # Expiry is stored in RFC3339 UTC format 57 # Expiry is stored in RFC3339 UTC format
57 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' 58 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
58 59
59 # Which certs to use to validate id_tokens received. 60 # Which certs to use to validate id_tokens received.
60 ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' 61 ID_TOKEN_VERIFICATION_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
61 65
62 # Constant to use for the out of band OAuth 2.0 flow. 66 # Constant to use for the out of band OAuth 2.0 flow.
63 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' 67 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
64 68
65 # Google Data client libraries may need to set this to [401, 403]. 69 # Google Data client libraries may need to set this to [401, 403].
66 REFRESH_STATUS_CODES = [401] 70 REFRESH_STATUS_CODES = [401]
67 71
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
68 108
69 class Error(Exception): 109 class Error(Exception):
70 """Base error for this module.""" 110 """Base error for this module."""
71 111
72 112
73 class FlowExchangeError(Error): 113 class FlowExchangeError(Error):
74 """Error trying to exchange an authorization grant for an access token.""" 114 """Error trying to exchange an authorization grant for an access token."""
75 115
76 116
77 class AccessTokenRefreshError(Error): 117 class AccessTokenRefreshError(Error):
78 """Error trying to refresh an expired access token.""" 118 """Error trying to refresh an expired access token."""
79 119
80 120
81 class TokenRevokeError(Error): 121 class TokenRevokeError(Error):
82 """Error trying to revoke a token.""" 122 """Error trying to revoke a token."""
83 123
84 124
85 class UnknownClientSecretsFlowError(Error): 125 class UnknownClientSecretsFlowError(Error):
86 """The client secrets file called for an unknown type of OAuth 2.0 flow. """ 126 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
87 127
88 128
89 class AccessTokenCredentialsError(Error): 129 class AccessTokenCredentialsError(Error):
90 """Having only the access_token means no refresh is possible.""" 130 """Having only the access_token means no refresh is possible."""
91 131
92 132
93 class VerifyJwtTokenError(Error): 133 class VerifyJwtTokenError(Error):
94 """Could on retrieve certificates for validation.""" 134 """Could not retrieve certificates for validation."""
95 135
96 136
97 class NonAsciiHeaderError(Error): 137 class NonAsciiHeaderError(Error):
98 """Header names and values must be ASCII strings.""" 138 """Header names and values must be ASCII strings."""
99 139
100 140
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
101 def _abstract(): 153 def _abstract():
102 raise NotImplementedError('You need to override this function') 154 raise NotImplementedError('You need to override this function')
103 155
104 156
105 class MemoryCache(object): 157 class MemoryCache(object):
106 """httplib2 Cache implementation which only caches locally.""" 158 """httplib2 Cache implementation which only caches locally."""
107 159
108 def __init__(self): 160 def __init__(self):
109 self.cache = {} 161 self.cache = {}
110 162
111 def get(self, key): 163 def get(self, key):
112 return self.cache.get(key) 164 return self.cache.get(key)
113 165
114 def set(self, key, value): 166 def set(self, key, value):
115 self.cache[key] = value 167 self.cache[key] = value
116 168
117 def delete(self, key): 169 def delete(self, key):
118 self.cache.pop(key, None) 170 self.cache.pop(key, None)
119 171
120 172
121 class Credentials(object): 173 class Credentials(object):
122 """Base class for all Credentials objects. 174 """Base class for all Credentials objects.
123 175
124 Subclasses must define an authorize() method that applies the credentials to 176 Subclasses must define an authorize() method that applies the credentials to
125 an HTTP transport. 177 an HTTP transport.
126 178
127 Subclasses must also specify a classmethod named 'from_json' that takes a JSON 179 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
128 string as input and returns an instaniated Credentials object. 180 string as input and returns an instantiated Credentials object.
129 """ 181 """
130 182
131 NON_SERIALIZED_MEMBERS = ['store'] 183 NON_SERIALIZED_MEMBERS = ['store']
132 184
185
133 def authorize(self, http): 186 def authorize(self, http):
134 """Take an httplib2.Http instance (or equivalent) and authorizes it. 187 """Take an httplib2.Http instance (or equivalent) and authorizes it.
135 188
136 Authorizes it for the set of credentials, usually by replacing 189 Authorizes it for the set of credentials, usually by replacing
137 http.request() with a method that adds in the appropriate headers and then 190 http.request() with a method that adds in the appropriate headers and then
138 delegates to the original Http.request() method. 191 delegates to the original Http.request() method.
139 192
140 Args: 193 Args:
141 http: httplib2.Http, an http object to be used to make the refresh 194 http: httplib2.Http, an http object to be used to make the refresh
142 request. 195 request.
143 """ 196 """
144 _abstract() 197 _abstract()
145 198
199
146 def refresh(self, http): 200 def refresh(self, http):
147 """Forces a refresh of the access_token. 201 """Forces a refresh of the access_token.
148 202
149 Args: 203 Args:
150 http: httplib2.Http, an http object to be used to make the refresh 204 http: httplib2.Http, an http object to be used to make the refresh
151 request. 205 request.
152 """ 206 """
153 _abstract() 207 _abstract()
154 208
209
155 def revoke(self, http): 210 def revoke(self, http):
156 """Revokes a refresh_token and makes the credentials void. 211 """Revokes a refresh_token and makes the credentials void.
157 212
158 Args: 213 Args:
159 http: httplib2.Http, an http object to be used to make the revoke 214 http: httplib2.Http, an http object to be used to make the revoke
160 request. 215 request.
161 """ 216 """
162 _abstract() 217 _abstract()
163 218
219
164 def apply(self, headers): 220 def apply(self, headers):
165 """Add the authorization to the headers. 221 """Add the authorization to the headers.
166 222
167 Args: 223 Args:
168 headers: dict, the headers to add the Authorization header to. 224 headers: dict, the headers to add the Authorization header to.
169 """ 225 """
170 _abstract() 226 _abstract()
171 227
172 def _to_json(self, strip): 228 def _to_json(self, strip):
173 """Utility function that creates JSON repr. of a Credentials object. 229 """Utility function that creates JSON repr. of a Credentials object.
174 230
175 Args: 231 Args:
176 strip: array, An array of names of members to not include in the JSON. 232 strip: array, An array of names of members to not include in the JSON.
177 233
178 Returns: 234 Returns:
179 string, a JSON representation of this instance, suitable to pass to 235 string, a JSON representation of this instance, suitable to pass to
180 from_json(). 236 from_json().
181 """ 237 """
182 t = type(self) 238 t = type(self)
183 d = copy.copy(self.__dict__) 239 d = copy.copy(self.__dict__)
184 for member in strip: 240 for member in strip:
185 if member in d: 241 if member in d:
186 del d[member] 242 del d[member]
187 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): 243 if (d.get('token_expiry') and
244 isinstance(d['token_expiry'], datetime.datetime)):
188 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) 245 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
189 # Add in information we will need later to reconsistitue this instance. 246 # Add in information we will need later to reconsistitue this instance.
190 d['_class'] = t.__name__ 247 d['_class'] = t.__name__
191 d['_module'] = t.__module__ 248 d['_module'] = t.__module__
192 return simplejson.dumps(d) 249 for key, val in d.items():
250 if isinstance(val, bytes):
251 d[key] = val.decode('utf-8')
252 return json.dumps(d)
193 253
194 def to_json(self): 254 def to_json(self):
195 """Creating a JSON representation of an instance of Credentials. 255 """Creating a JSON representation of an instance of Credentials.
196 256
197 Returns: 257 Returns:
198 string, a JSON representation of this instance, suitable to pass to 258 string, a JSON representation of this instance, suitable to pass to
199 from_json(). 259 from_json().
200 """ 260 """
201 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) 261 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
202 262
203 @classmethod 263 @classmethod
204 def new_from_json(cls, s): 264 def new_from_json(cls, s):
205 """Utility class method to instantiate a Credentials subclass from a JSON 265 """Utility class method to instantiate a Credentials subclass from a JSON
206 representation produced by to_json(). 266 representation produced by to_json().
207 267
208 Args: 268 Args:
209 s: string, JSON from to_json(). 269 s: string, JSON from to_json().
210 270
211 Returns: 271 Returns:
212 An instance of the subclass of Credentials that was serialized with 272 An instance of the subclass of Credentials that was serialized with
213 to_json(). 273 to_json().
214 """ 274 """
215 data = simplejson.loads(s) 275 if six.PY3 and isinstance(s, bytes):
276 s = s.decode('utf-8')
277 data = json.loads(s)
216 # Find and call the right classmethod from_json() to restore the object. 278 # Find and call the right classmethod from_json() to restore the object.
217 module = data['_module'] 279 module = data['_module']
218 try: 280 try:
219 m = __import__(module) 281 m = __import__(module)
220 except ImportError: 282 except ImportError:
221 # In case there's an object from the old package structure, update it 283 # In case there's an object from the old package structure, update it
222 module = module.replace('.apiclient', '') 284 module = module.replace('.googleapiclient', '')
223 m = __import__(module) 285 m = __import__(module)
224 286
225 m = __import__(module, fromlist=module.split('.')[:-1]) 287 m = __import__(module, fromlist=module.split('.')[:-1])
226 kls = getattr(m, data['_class']) 288 kls = getattr(m, data['_class'])
227 from_json = getattr(kls, 'from_json') 289 from_json = getattr(kls, 'from_json')
228 return from_json(s) 290 return from_json(s)
229 291
230 @classmethod 292 @classmethod
231 def from_json(cls, s): 293 def from_json(cls, unused_data):
232 """Instantiate a Credentials object from a JSON description of it. 294 """Instantiate a Credentials object from a JSON description of it.
233 295
234 The JSON should have been produced by calling .to_json() on the object. 296 The JSON should have been produced by calling .to_json() on the object.
235 297
236 Args: 298 Args:
237 data: dict, A deserialized JSON object. 299 unused_data: dict, A deserialized JSON object.
238 300
239 Returns: 301 Returns:
240 An instance of a Credentials subclass. 302 An instance of a Credentials subclass.
241 """ 303 """
242 return Credentials() 304 return Credentials()
243 305
244 306
245 class Flow(object): 307 class Flow(object):
246 """Base class for all Flow objects.""" 308 """Base class for all Flow objects."""
247 pass 309 pass
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after
349 contatenate to a binary request body may result in a unicode decode error. 411 contatenate to a binary request body may result in a unicode decode error.
350 412
351 Args: 413 Args:
352 headers: dict, A dictionary of headers. 414 headers: dict, A dictionary of headers.
353 415
354 Returns: 416 Returns:
355 The same dictionary but with all the keys converted to strings. 417 The same dictionary but with all the keys converted to strings.
356 """ 418 """
357 clean = {} 419 clean = {}
358 try: 420 try:
359 for k, v in headers.iteritems(): 421 for k, v in six.iteritems(headers):
360 clean[str(k)] = str(v) 422 clean_k = k if isinstance(k, bytes) else str(k).encode('ascii')
423 clean_v = v if isinstance(v, bytes) else str(v).encode('ascii')
424 clean[clean_k] = clean_v
361 except UnicodeEncodeError: 425 except UnicodeEncodeError:
362 raise NonAsciiHeaderError(k + ': ' + v) 426 raise NonAsciiHeaderError(k + ': ' + v)
363 return clean 427 return clean
364 428
365 429
366 def _update_query_params(uri, params): 430 def _update_query_params(uri, params):
367 """Updates a URI with new query parameters. 431 """Updates a URI with new query parameters.
368 432
369 Args: 433 Args:
370 uri: string, A valid URI, with potential existing query parameters. 434 uri: string, A valid URI, with potential existing query parameters.
371 params: dict, A dictionary of query parameters. 435 params: dict, A dictionary of query parameters.
372 436
373 Returns: 437 Returns:
374 The same URI but with the new query parameters added. 438 The same URI but with the new query parameters added.
375 """ 439 """
376 parts = list(urlparse.urlparse(uri)) 440 parts = urllib.parse.urlparse(uri)
377 query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part 441 query_params = dict(urllib.parse.parse_qsl(parts.query))
378 query_params.update(params) 442 query_params.update(params)
379 parts[4] = urllib.urlencode(query_params) 443 new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
380 return urlparse.urlunparse(parts) 444 return urllib.parse.urlunparse(new_parts)
381 445
382 446
383 class OAuth2Credentials(Credentials): 447 class OAuth2Credentials(Credentials):
384 """Credentials object for OAuth 2.0. 448 """Credentials object for OAuth 2.0.
385 449
386 Credentials can be applied to an httplib2.Http object using the authorize() 450 Credentials can be applied to an httplib2.Http object using the authorize()
387 method, which then adds the OAuth 2.0 access token to each request. 451 method, which then adds the OAuth 2.0 access token to each request.
388 452
389 OAuth2Credentials objects may be safely pickled and unpickled. 453 OAuth2Credentials objects may be safely pickled and unpickled.
390 """ 454 """
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
438 def authorize(self, http): 502 def authorize(self, http):
439 """Authorize an httplib2.Http instance with these credentials. 503 """Authorize an httplib2.Http instance with these credentials.
440 504
441 The modified http.request method will add authentication headers to each 505 The modified http.request method will add authentication headers to each
442 request and will refresh access_tokens when a 401 is received on a 506 request and will refresh access_tokens when a 401 is received on a
443 request. In addition the http.request method has a credentials property, 507 request. In addition the http.request method has a credentials property,
444 http.request.credentials, which is the Credentials object that authorized 508 http.request.credentials, which is the Credentials object that authorized
445 it. 509 it.
446 510
447 Args: 511 Args:
448 http: An instance of httplib2.Http 512 http: An instance of ``httplib2.Http`` or something that acts
449 or something that acts like it. 513 like it.
450 514
451 Returns: 515 Returns:
452 A modified instance of http that was passed in. 516 A modified instance of http that was passed in.
453 517
454 Example: 518 Example::
455 519
456 h = httplib2.Http() 520 h = httplib2.Http()
457 h = credentials.authorize(h) 521 h = credentials.authorize(h)
458 522
459 You can't create a new OAuth subclass of httplib2.Authenication 523 You can't create a new OAuth subclass of httplib2.Authentication
460 because it never gets passed the absolute URI, which is needed for 524 because it never gets passed the absolute URI, which is needed for
461 signing. So instead we have to overload 'request' with a closure 525 signing. So instead we have to overload 'request' with a closure
462 that adds in the Authorization header and then calls the original 526 that adds in the Authorization header and then calls the original
463 version of 'request()'. 527 version of 'request()'.
528
464 """ 529 """
465 request_orig = http.request 530 request_orig = http.request
466 531
467 # The closure that will replace 'httplib2.Http.request'. 532 # The closure that will replace 'httplib2.Http.request'.
468 @util.positional(1) 533 @util.positional(1)
469 def new_request(uri, method='GET', body=None, headers=None, 534 def new_request(uri, method='GET', body=None, headers=None,
470 redirections=httplib2.DEFAULT_MAX_REDIRECTS, 535 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
471 connection_type=None): 536 connection_type=None):
472 if not self.access_token: 537 if not self.access_token:
473 logger.info('Attempting refresh to obtain initial access_token') 538 logger.info('Attempting refresh to obtain initial access_token')
474 self._refresh(request_orig) 539 self._refresh(request_orig)
475 540
476 # Modify the request headers to add the appropriate 541 # Clone and modify the request headers to add the appropriate
477 # Authorization header. 542 # Authorization header.
478 if headers is None: 543 if headers is None:
479 headers = {} 544 headers = {}
545 else:
546 headers = dict(headers)
480 self.apply(headers) 547 self.apply(headers)
481 548
482 if self.user_agent is not None: 549 if self.user_agent is not None:
483 if 'user-agent' in headers: 550 if 'user-agent' in headers:
484 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] 551 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
485 else: 552 else:
486 headers['user-agent'] = self.user_agent 553 headers['user-agent'] = self.user_agent
487 554
488 resp, content = request_orig(uri, method, body, clean_headers(headers), 555 resp, content = request_orig(uri, method, body, clean_headers(headers),
489 redirections, connection_type) 556 redirections, connection_type)
490 557
491 if resp.status in REFRESH_STATUS_CODES: 558 if resp.status in REFRESH_STATUS_CODES:
492 logger.info('Refreshing due to a %s' % str(resp.status)) 559 logger.info('Refreshing due to a %s', resp.status)
493 self._refresh(request_orig) 560 self._refresh(request_orig)
494 self.apply(headers) 561 self.apply(headers)
495 return request_orig(uri, method, body, clean_headers(headers), 562 return request_orig(uri, method, body, clean_headers(headers),
496 redirections, connection_type) 563 redirections, connection_type)
497 else: 564 else:
498 return (resp, content) 565 return (resp, content)
499 566
500 # Replace the request method with our own closure. 567 # Replace the request method with our own closure.
501 http.request = new_request 568 http.request = new_request
502 569
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
538 def from_json(cls, s): 605 def from_json(cls, s):
539 """Instantiate a Credentials object from a JSON description of it. The JSON 606 """Instantiate a Credentials object from a JSON description of it. The JSON
540 should have been produced by calling .to_json() on the object. 607 should have been produced by calling .to_json() on the object.
541 608
542 Args: 609 Args:
543 data: dict, A deserialized JSON object. 610 data: dict, A deserialized JSON object.
544 611
545 Returns: 612 Returns:
546 An instance of a Credentials subclass. 613 An instance of a Credentials subclass.
547 """ 614 """
548 data = simplejson.loads(s) 615 if six.PY3 and isinstance(s, bytes):
549 if 'token_expiry' in data and not isinstance(data['token_expiry'], 616 s = s.decode('utf-8')
550 datetime.datetime): 617 data = json.loads(s)
618 if (data.get('token_expiry') and
619 not isinstance(data['token_expiry'], datetime.datetime)):
551 try: 620 try:
552 data['token_expiry'] = datetime.datetime.strptime( 621 data['token_expiry'] = datetime.datetime.strptime(
553 data['token_expiry'], EXPIRY_FORMAT) 622 data['token_expiry'], EXPIRY_FORMAT)
554 except: 623 except ValueError:
555 data['token_expiry'] = None 624 data['token_expiry'] = None
556 retval = cls( 625 retval = cls(
557 data['access_token'], 626 data['access_token'],
558 data['client_id'], 627 data['client_id'],
559 data['client_secret'], 628 data['client_secret'],
560 data['refresh_token'], 629 data['refresh_token'],
561 data['token_expiry'], 630 data['token_expiry'],
562 data['token_uri'], 631 data['token_uri'],
563 data['user_agent'], 632 data['user_agent'],
564 revoke_uri=data.get('revoke_uri', None), 633 revoke_uri=data.get('revoke_uri', None),
(...skipping 14 matching lines...) Expand all
579 if not self.token_expiry: 648 if not self.token_expiry:
580 return False 649 return False
581 650
582 now = datetime.datetime.utcnow() 651 now = datetime.datetime.utcnow()
583 if now >= self.token_expiry: 652 if now >= self.token_expiry:
584 logger.info('access_token is expired. Now: %s, token_expiry: %s', 653 logger.info('access_token is expired. Now: %s, token_expiry: %s',
585 now, self.token_expiry) 654 now, self.token_expiry)
586 return True 655 return True
587 return False 656 return False
588 657
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
589 def set_store(self, store): 671 def set_store(self, store):
590 """Set the Storage for the credential. 672 """Set the Storage for the credential.
591 673
592 Args: 674 Args:
593 store: Storage, an implementation of Stroage object. 675 store: Storage, an implementation of Storage object.
594 This is needed to store the latest access_token if it 676 This is needed to store the latest access_token if it
595 has expired and been refreshed. This implementation uses 677 has expired and been refreshed. This implementation uses
596 locking to check for updates before updating the 678 locking to check for updates before updating the
597 access_token. 679 access_token.
598 """ 680 """
599 self.store = store 681 self.store = store
600 682
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
601 def _updateFromCredential(self, other): 702 def _updateFromCredential(self, other):
602 """Update this Credential from another instance.""" 703 """Update this Credential from another instance."""
603 self.__dict__.update(other.__getstate__()) 704 self.__dict__.update(other.__getstate__())
604 705
605 def __getstate__(self): 706 def __getstate__(self):
606 """Trim the state down to something that can be pickled.""" 707 """Trim the state down to something that can be pickled."""
607 d = copy.copy(self.__dict__) 708 d = copy.copy(self.__dict__)
608 del d['store'] 709 del d['store']
609 return d 710 return d
610 711
611 def __setstate__(self, state): 712 def __setstate__(self, state):
612 """Reconstitute the state of the object from being pickled.""" 713 """Reconstitute the state of the object from being pickled."""
613 self.__dict__.update(state) 714 self.__dict__.update(state)
614 self.store = None 715 self.store = None
615 716
616 def _generate_refresh_request_body(self): 717 def _generate_refresh_request_body(self):
617 """Generate the body that will be used in the refresh request.""" 718 """Generate the body that will be used in the refresh request."""
618 body = urllib.urlencode({ 719 body = urllib.parse.urlencode({
619 'grant_type': 'refresh_token', 720 'grant_type': 'refresh_token',
620 'client_id': self.client_id, 721 'client_id': self.client_id,
621 'client_secret': self.client_secret, 722 'client_secret': self.client_secret,
622 'refresh_token': self.refresh_token, 723 'refresh_token': self.refresh_token,
623 }) 724 })
624 return body 725 return body
625 726
626 def _generate_refresh_request_headers(self): 727 def _generate_refresh_request_headers(self):
627 """Generate the headers that will be used in the refresh request.""" 728 """Generate the headers that will be used in the refresh request."""
628 headers = { 729 headers = {
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
672 773
673 Raises: 774 Raises:
674 AccessTokenRefreshError: When the refresh fails. 775 AccessTokenRefreshError: When the refresh fails.
675 """ 776 """
676 body = self._generate_refresh_request_body() 777 body = self._generate_refresh_request_body()
677 headers = self._generate_refresh_request_headers() 778 headers = self._generate_refresh_request_headers()
678 779
679 logger.info('Refreshing access_token') 780 logger.info('Refreshing access_token')
680 resp, content = http_request( 781 resp, content = http_request(
681 self.token_uri, method='POST', body=body, headers=headers) 782 self.token_uri, method='POST', body=body, headers=headers)
783 if six.PY3 and isinstance(content, bytes):
784 content = content.decode('utf-8')
682 if resp.status == 200: 785 if resp.status == 200:
683 # TODO(jcgregorio) Raise an error if loads fails? 786 d = json.loads(content)
684 d = simplejson.loads(content)
685 self.token_response = d 787 self.token_response = d
686 self.access_token = d['access_token'] 788 self.access_token = d['access_token']
687 self.refresh_token = d.get('refresh_token', self.refresh_token) 789 self.refresh_token = d.get('refresh_token', self.refresh_token)
688 if 'expires_in' in d: 790 if 'expires_in' in d:
689 self.token_expiry = datetime.timedelta( 791 self.token_expiry = datetime.timedelta(
690 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() 792 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
691 else: 793 else:
692 self.token_expiry = None 794 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
693 if self.store: 798 if self.store:
694 self.store.locked_put(self) 799 self.store.locked_put(self)
695 else: 800 else:
696 # An {'error':...} response body means the token is expired or revoked, 801 # An {'error':...} response body means the token is expired or revoked,
697 # so we flag the credentials as such. 802 # so we flag the credentials as such.
698 logger.info('Failed to retrieve access token: %s' % content) 803 logger.info('Failed to retrieve access token: %s', content)
699 error_msg = 'Invalid response %s.' % resp['status'] 804 error_msg = 'Invalid response %s.' % resp['status']
700 try: 805 try:
701 d = simplejson.loads(content) 806 d = json.loads(content)
702 if 'error' in d: 807 if 'error' in d:
703 error_msg = d['error'] 808 error_msg = d['error']
809 if 'error_description' in d:
810 error_msg += ': ' + d['error_description']
704 self.invalid = True 811 self.invalid = True
705 if self.store: 812 if self.store:
706 self.store.locked_put(self) 813 self.store.locked_put(self)
707 except StandardError: 814 except (TypeError, ValueError):
708 pass 815 pass
709 raise AccessTokenRefreshError(error_msg) 816 raise AccessTokenRefreshError(error_msg)
710 817
711 def _revoke(self, http_request): 818 def _revoke(self, http_request):
712 """Revokes the refresh_token and deletes the store if available. 819 """Revokes this credential and deletes the stored copy (if it exists).
713 820
714 Args: 821 Args:
715 http_request: callable, a callable that matches the method signature of 822 http_request: callable, a callable that matches the method signature of
716 httplib2.Http.request, used to make the revoke request. 823 httplib2.Http.request, used to make the revoke request.
717 """ 824 """
718 self._do_revoke(http_request, self.refresh_token) 825 self._do_revoke(http_request, self.refresh_token or self.access_token)
719 826
720 def _do_revoke(self, http_request, token): 827 def _do_revoke(self, http_request, token):
721 """Revokes the credentials and deletes the store if available. 828 """Revokes this credential and deletes the stored copy (if it exists).
722 829
723 Args: 830 Args:
724 http_request: callable, a callable that matches the method signature of 831 http_request: callable, a callable that matches the method signature of
725 httplib2.Http.request, used to make the refresh request. 832 httplib2.Http.request, used to make the refresh request.
726 token: A string used as the token to be revoked. Can be either an 833 token: A string used as the token to be revoked. Can be either an
727 access_token or refresh_token. 834 access_token or refresh_token.
728 835
729 Raises: 836 Raises:
730 TokenRevokeError: If the revoke request does not return with a 200 OK. 837 TokenRevokeError: If the revoke request does not return with a 200 OK.
731 """ 838 """
732 logger.info('Revoking token') 839 logger.info('Revoking token')
733 query_params = {'token': token} 840 query_params = {'token': token}
734 token_revoke_uri = _update_query_params(self.revoke_uri, query_params) 841 token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
735 resp, content = http_request(token_revoke_uri) 842 resp, content = http_request(token_revoke_uri)
736 if resp.status == 200: 843 if resp.status == 200:
737 self.invalid = True 844 self.invalid = True
738 else: 845 else:
739 error_msg = 'Invalid response %s.' % resp.status 846 error_msg = 'Invalid response %s.' % resp.status
740 try: 847 try:
741 d = simplejson.loads(content) 848 d = json.loads(content)
742 if 'error' in d: 849 if 'error' in d:
743 error_msg = d['error'] 850 error_msg = d['error']
744 except StandardError: 851 except (TypeError, ValueError):
745 pass 852 pass
746 raise TokenRevokeError(error_msg) 853 raise TokenRevokeError(error_msg)
747 854
748 if self.store: 855 if self.store:
749 self.store.delete() 856 self.store.delete()
750 857
751 858
752 class AccessTokenCredentials(OAuth2Credentials): 859 class AccessTokenCredentials(OAuth2Credentials):
753 """Credentials object for OAuth 2.0. 860 """Credentials object for OAuth 2.0.
754 861
755 Credentials can be applied to an httplib2.Http object using the 862 Credentials can be applied to an httplib2.Http object using the
756 authorize() method, which then signs each request from that object 863 authorize() method, which then signs each request from that object
757 with the OAuth 2.0 access token. This set of credentials is for the 864 with the OAuth 2.0 access token. This set of credentials is for the
758 use case where you have acquired an OAuth 2.0 access_token from 865 use case where you have acquired an OAuth 2.0 access_token from
759 another place such as a JavaScript client or another web 866 another place such as a JavaScript client or another web
760 application, and wish to use it from Python. Because only the 867 application, and wish to use it from Python. Because only the
761 access_token is present it can not be refreshed and will in time 868 access_token is present it can not be refreshed and will in time
762 expire. 869 expire.
763 870
764 AccessTokenCredentials objects may be safely pickled and unpickled. 871 AccessTokenCredentials objects may be safely pickled and unpickled.
765 872
766 Usage: 873 Usage::
874
767 credentials = AccessTokenCredentials('<an access token>', 875 credentials = AccessTokenCredentials('<an access token>',
768 'my-user-agent/1.0') 876 'my-user-agent/1.0')
769 http = httplib2.Http() 877 http = httplib2.Http()
770 http = credentials.authorize(http) 878 http = credentials.authorize(http)
771 879
772 Exceptions: 880 Exceptions:
773 AccessTokenCredentialsExpired: raised when the access_token expires or is 881 AccessTokenCredentialsExpired: raised when the access_token expires or is
774 revoked. 882 revoked.
775 """ 883 """
776 884
(...skipping 15 matching lines...) Expand all
792 None, 900 None,
793 None, 901 None,
794 None, 902 None,
795 None, 903 None,
796 user_agent, 904 user_agent,
797 revoke_uri=revoke_uri) 905 revoke_uri=revoke_uri)
798 906
799 907
800 @classmethod 908 @classmethod
801 def from_json(cls, s): 909 def from_json(cls, s):
802 data = simplejson.loads(s) 910 if six.PY3 and isinstance(s, bytes):
911 s = s.decode('utf-8')
912 data = json.loads(s)
803 retval = AccessTokenCredentials( 913 retval = AccessTokenCredentials(
804 data['access_token'], 914 data['access_token'],
805 data['user_agent']) 915 data['user_agent'])
806 return retval 916 return retval
807 917
808 def _refresh(self, http_request): 918 def _refresh(self, http_request):
809 raise AccessTokenCredentialsError( 919 raise AccessTokenCredentialsError(
810 'The access_token is expired or invalid and can\'t be refreshed.') 920 'The access_token is expired or invalid and can\'t be refreshed.')
811 921
812 def _revoke(self, http_request): 922 def _revoke(self, http_request):
813 """Revokes the access_token and deletes the store if available. 923 """Revokes the access_token and deletes the store if available.
814 924
815 Args: 925 Args:
816 http_request: callable, a callable that matches the method signature of 926 http_request: callable, a callable that matches the method signature of
817 httplib2.Http.request, used to make the revoke request. 927 httplib2.Http.request, used to make the revoke request.
818 """ 928 """
819 self._do_revoke(http_request, self.access_token) 929 self._do_revoke(http_request, self.access_token)
820 930
821 931
822 class AssertionCredentials(OAuth2Credentials): 932 def _detect_gce_environment(urlopen=None):
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):
823 """Abstract Credentials object used for OAuth 2.0 assertion grants. 1360 """Abstract Credentials object used for OAuth 2.0 assertion grants.
824 1361
825 This credential does not require a flow to instantiate because it 1362 This credential does not require a flow to instantiate because it
826 represents a two legged flow, and therefore has all of the required 1363 represents a two legged flow, and therefore has all of the required
827 information to generate and refresh its own access tokens. It must 1364 information to generate and refresh its own access tokens. It must
828 be subclassed to generate the appropriate assertion string. 1365 be subclassed to generate the appropriate assertion string.
829 1366
830 AssertionCredentials objects may be safely pickled and unpickled. 1367 AssertionCredentials objects may be safely pickled and unpickled.
831 """ 1368 """
832 1369
(...skipping 19 matching lines...) Expand all
852 None, 1389 None,
853 None, 1390 None,
854 token_uri, 1391 token_uri,
855 user_agent, 1392 user_agent,
856 revoke_uri=revoke_uri) 1393 revoke_uri=revoke_uri)
857 self.assertion_type = assertion_type 1394 self.assertion_type = assertion_type
858 1395
859 def _generate_refresh_request_body(self): 1396 def _generate_refresh_request_body(self):
860 assertion = self._generate_assertion() 1397 assertion = self._generate_assertion()
861 1398
862 body = urllib.urlencode({ 1399 body = urllib.parse.urlencode({
863 'assertion': assertion, 1400 'assertion': assertion,
864 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', 1401 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
865 }) 1402 })
866 1403
867 return body 1404 return body
868 1405
869 def _generate_assertion(self): 1406 def _generate_assertion(self):
870 """Generate the assertion string that will be used in the access token 1407 """Generate the assertion string that will be used in the access token
871 request. 1408 request.
872 """ 1409 """
873 _abstract() 1410 _abstract()
874 1411
875 def _revoke(self, http_request): 1412 def _revoke(self, http_request):
876 """Revokes the access_token and deletes the store if available. 1413 """Revokes the access_token and deletes the store if available.
877 1414
878 Args: 1415 Args:
879 http_request: callable, a callable that matches the method signature of 1416 http_request: callable, a callable that matches the method signature of
880 httplib2.Http.request, used to make the revoke request. 1417 httplib2.Http.request, used to make the revoke request.
881 """ 1418 """
882 self._do_revoke(http_request, self.access_token) 1419 self._do_revoke(http_request, self.access_token)
883 1420
884 1421
885 if HAS_CRYPTO: 1422 def _RequireCryptoOrDie():
886 # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is 1423 """Ensure we have a crypto library, or throw CryptoUnavailableError.
887 # missing then don't create the SignedJwtAssertionCredentials or the
888 # verify_id_token() method.
889 1424
890 class SignedJwtAssertionCredentials(AssertionCredentials): 1425 The oauth2client.crypt module requires either PyCrypto or PyOpenSSL
891 """Credentials object used for OAuth 2.0 Signed JWT assertion grants. 1426 to be available in order to function, but these are optional
1427 dependencies.
1428 """
1429 if not HAS_CRYPTO:
1430 raise CryptoUnavailableError('No crypto library available')
892 1431
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.
896 1432
897 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or 1433 class SignedJwtAssertionCredentials(AssertionCredentials):
898 later. For App Engine you may also consider using AppAssertionCredentials. 1434 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
899 """
900 1435
901 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 1436 This credential does not require a flow to instantiate because it
1437 represents a two legged flow, and therefore has all of the required
1438 information to generate and refresh its own access tokens.
902 1439
903 @util.positional(4) 1440 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto
904 def __init__(self, 1441 2.6 or later. For App Engine you may also consider using
905 service_account_name, 1442 AppAssertionCredentials.
906 private_key, 1443 """
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.
914 1444
915 Args: 1445 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
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."""
928 1446
929 super(SignedJwtAssertionCredentials, self).__init__( 1447 @util.positional(4)
930 None, 1448 def __init__(self,
931 user_agent=user_agent, 1449 service_account_name,
932 token_uri=token_uri, 1450 private_key,
933 revoke_uri=revoke_uri, 1451 scope,
934 ) 1452 private_key_password='notasecret',
935 1453 user_agent=None,
936 self.scope = util.scopes_to_string(scope) 1454 token_uri=GOOGLE_TOKEN_URI,
937 1455 revoke_uri=GOOGLE_REVOKE_URI,
938 # Keep base64 encoded so it can be stored in JSON. 1456 **kwargs):
939 self.private_key = base64.b64encode(private_key) 1457 """Constructor for SignedJwtAssertionCredentials.
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.
989 1458
990 Args: 1459 Args:
991 id_token: string, A Signed JWT. 1460 service_account_name: string, id for account, usually an email address.
992 audience: string, The audience 'aud' that the token should be for. 1461 private_key: string, private key in PKCS12 or PEM format.
993 http: httplib2.Http, instance to use to make the HTTP request. Callers 1462 scope: string or iterable of strings, scope(s) of the credentials being
994 should supply an instance that has caching enabled. 1463 requested.
995 cert_uri: string, URI of the certificates in JSON format to 1464 private_key_password: string, password for private_key, unused if
996 verify the JWT against. 1465 private_key is in PEM format.
997 1466 user_agent: string, HTTP User-Agent to provide for this application.
998 Returns: 1467 token_uri: string, URI for token endpoint. For convenience
999 The deserialized JSON in the JWT. 1468 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
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.
1000 1472
1001 Raises: 1473 Raises:
1002 oauth2client.crypt.AppIdentityError if the JWT fails to verify. 1474 CryptoUnavailableError if no crypto library is available.
1003 """ 1475 """
1004 if http is None: 1476 _RequireCryptoOrDie()
1005 http = _cached_http 1477 super(SignedJwtAssertionCredentials, self).__init__(
1478 None,
1479 user_agent=user_agent,
1480 token_uri=token_uri,
1481 revoke_uri=revoke_uri,
1482 )
1006 1483
1007 resp, content = http.request(cert_uri) 1484 self.scope = util.scopes_to_string(scope)
1008 1485
1009 if resp.status == 200: 1486 # Keep base64 encoded so it can be stored in JSON.
1010 certs = simplejson.loads(content) 1487 self.private_key = base64.b64encode(private_key)
1011 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) 1488 if isinstance(self.private_key, six.text_type):
1012 else: 1489 self.private_key = self.private_key.encode('utf-8')
1013 raise VerifyJwtTokenError('Status code: %d' % resp.status) 1490
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)
1014 1566
1015 1567
1016 def _urlsafe_b64decode(b64string): 1568 def _urlsafe_b64decode(b64string):
1017 # Guard against unicode strings, which base64 can't handle. 1569 # Guard against unicode strings, which base64 can't handle.
1018 b64string = b64string.encode('ascii') 1570 if isinstance(b64string, six.text_type):
1019 padded = b64string + '=' * (4 - len(b64string) % 4) 1571 b64string = b64string.encode('ascii')
1572 padded = b64string + b'=' * (4 - len(b64string) % 4)
1020 return base64.urlsafe_b64decode(padded) 1573 return base64.urlsafe_b64decode(padded)
1021 1574
1022 1575
1023 def _extract_id_token(id_token): 1576 def _extract_id_token(id_token):
1024 """Extract the JSON payload from a JWT. 1577 """Extract the JSON payload from a JWT.
1025 1578
1026 Does the extraction w/o checking the signature. 1579 Does the extraction w/o checking the signature.
1027 1580
1028 Args: 1581 Args:
1029 id_token: string, OAuth 2.0 id_token. 1582 id_token: string or bytestring, OAuth 2.0 id_token.
1030 1583
1031 Returns: 1584 Returns:
1032 object, The deserialized JSON payload. 1585 object, The deserialized JSON payload.
1033 """ 1586 """
1034 segments = id_token.split('.') 1587 if type(id_token) == bytes:
1588 segments = id_token.split(b'.')
1589 else:
1590 segments = id_token.split(u'.')
1035 1591
1036 if (len(segments) != 3): 1592 if len(segments) != 3:
1037 raise VerifyJwtTokenError( 1593 raise VerifyJwtTokenError(
1038 'Wrong number of segments in token: %s' % id_token) 1594 'Wrong number of segments in token: %s' % id_token)
1039 1595
1040 return simplejson.loads(_urlsafe_b64decode(segments[1])) 1596 return json.loads(_urlsafe_b64decode(segments[1]).decode('utf-8'))
1041 1597
1042 1598
1043 def _parse_exchange_token_response(content): 1599 def _parse_exchange_token_response(content):
1044 """Parses response of an exchange token request. 1600 """Parses response of an exchange token request.
1045 1601
1046 Most providers return JSON but some (e.g. Facebook) return a 1602 Most providers return JSON but some (e.g. Facebook) return a
1047 url-encoded string. 1603 url-encoded string.
1048 1604
1049 Args: 1605 Args:
1050 content: The body of a response 1606 content: The body of a response
1051 1607
1052 Returns: 1608 Returns:
1053 Content as a dictionary object. Note that the dict could be empty, 1609 Content as a dictionary object. Note that the dict could be empty,
1054 i.e. {}. That basically indicates a failure. 1610 i.e. {}. That basically indicates a failure.
1055 """ 1611 """
1056 resp = {} 1612 resp = {}
1057 try: 1613 try:
1058 resp = simplejson.loads(content) 1614 resp = json.loads(content.decode('utf-8'))
1059 except StandardError: 1615 except Exception:
1060 # different JSON libs raise different exceptions, 1616 # different JSON libs raise different exceptions,
1061 # so we just do a catch-all here 1617 # so we just do a catch-all here
1062 resp = dict(parse_qsl(content)) 1618 content = content.decode('utf-8')
1619 resp = dict(urllib.parse.parse_qsl(content))
1063 1620
1064 # some providers respond with 'expires', others with 'expires_in' 1621 # some providers respond with 'expires', others with 'expires_in'
1065 if resp and 'expires' in resp: 1622 if resp and 'expires' in resp:
1066 resp['expires_in'] = resp.pop('expires') 1623 resp['expires_in'] = resp.pop('expires')
1067 1624
1068 return resp 1625 return resp
1069 1626
1070 1627
1071 @util.positional(4) 1628 @util.positional(4)
1072 def credentials_from_code(client_id, client_secret, scope, code, 1629 def credentials_from_code(client_id, client_secret, scope, code,
1073 redirect_uri='postmessage', http=None, 1630 redirect_uri='postmessage', http=None,
1074 user_agent=None, token_uri=GOOGLE_TOKEN_URI, 1631 user_agent=None, token_uri=GOOGLE_TOKEN_URI,
1075 auth_uri=GOOGLE_AUTH_URI, 1632 auth_uri=GOOGLE_AUTH_URI,
1076 revoke_uri=GOOGLE_REVOKE_URI): 1633 revoke_uri=GOOGLE_REVOKE_URI,
1634 device_uri=GOOGLE_DEVICE_URI):
1077 """Exchanges an authorization code for an OAuth2Credentials object. 1635 """Exchanges an authorization code for an OAuth2Credentials object.
1078 1636
1079 Args: 1637 Args:
1080 client_id: string, client identifier. 1638 client_id: string, client identifier.
1081 client_secret: string, client secret. 1639 client_secret: string, client secret.
1082 scope: string or iterable of strings, scope(s) to request. 1640 scope: string or iterable of strings, scope(s) to request.
1083 code: string, An authroization code, most likely passed down from 1641 code: string, An authorization code, most likely passed down from
1084 the client 1642 the client
1085 redirect_uri: string, this is generally set to 'postmessage' to match the 1643 redirect_uri: string, this is generally set to 'postmessage' to match the
1086 redirect_uri that the client specified 1644 redirect_uri that the client specified
1087 http: httplib2.Http, optional http instance to use to do the fetch 1645 http: httplib2.Http, optional http instance to use to do the fetch
1088 token_uri: string, URI for token endpoint. For convenience 1646 token_uri: string, URI for token endpoint. For convenience
1089 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1647 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1090 auth_uri: string, URI for authorization endpoint. For convenience 1648 auth_uri: string, URI for authorization endpoint. For convenience
1091 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1649 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1092 revoke_uri: string, URI for revoke endpoint. For convenience 1650 revoke_uri: string, URI for revoke endpoint. For convenience
1093 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1651 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.
1094 1654
1095 Returns: 1655 Returns:
1096 An OAuth2Credentials object. 1656 An OAuth2Credentials object.
1097 1657
1098 Raises: 1658 Raises:
1099 FlowExchangeError if the authorization code cannot be exchanged for an 1659 FlowExchangeError if the authorization code cannot be exchanged for an
1100 access token 1660 access token
1101 """ 1661 """
1102 flow = OAuth2WebServerFlow(client_id, client_secret, scope, 1662 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
1103 redirect_uri=redirect_uri, user_agent=user_agent, 1663 redirect_uri=redirect_uri, user_agent=user_agent,
1104 auth_uri=auth_uri, token_uri=token_uri, 1664 auth_uri=auth_uri, token_uri=token_uri,
1105 revoke_uri=revoke_uri) 1665 revoke_uri=revoke_uri, device_uri=device_uri)
1106 1666
1107 credentials = flow.step2_exchange(code, http=http) 1667 credentials = flow.step2_exchange(code, http=http)
1108 return credentials 1668 return credentials
1109 1669
1110 1670
1111 @util.positional(3) 1671 @util.positional(3)
1112 def credentials_from_clientsecrets_and_code(filename, scope, code, 1672 def credentials_from_clientsecrets_and_code(filename, scope, code,
1113 message = None, 1673 message = None,
1114 redirect_uri='postmessage', 1674 redirect_uri='postmessage',
1115 http=None, 1675 http=None,
1116 cache=None): 1676 cache=None,
1677 device_uri=None):
1117 """Returns OAuth2Credentials from a clientsecrets file and an auth code. 1678 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
1118 1679
1119 Will create the right kind of Flow based on the contents of the clientsecrets 1680 Will create the right kind of Flow based on the contents of the clientsecrets
1120 file or will raise InvalidClientSecretsError for unknown types of Flows. 1681 file or will raise InvalidClientSecretsError for unknown types of Flows.
1121 1682
1122 Args: 1683 Args:
1123 filename: string, File name of clientsecrets. 1684 filename: string, File name of clientsecrets.
1124 scope: string or iterable of strings, scope(s) to request. 1685 scope: string or iterable of strings, scope(s) to request.
1125 code: string, An authorization code, most likely passed down from 1686 code: string, An authorization code, most likely passed down from
1126 the client 1687 the client
1127 message: string, A friendly string to display to the user if the 1688 message: string, A friendly string to display to the user if the
1128 clientsecrets file is missing or invalid. If message is provided then 1689 clientsecrets file is missing or invalid. If message is provided then
1129 sys.exit will be called in the case of an error. If message in not 1690 sys.exit will be called in the case of an error. If message in not
1130 provided then clientsecrets.InvalidClientSecretsError will be raised. 1691 provided then clientsecrets.InvalidClientSecretsError will be raised.
1131 redirect_uri: string, this is generally set to 'postmessage' to match the 1692 redirect_uri: string, this is generally set to 'postmessage' to match the
1132 redirect_uri that the client specified 1693 redirect_uri that the client specified
1133 http: httplib2.Http, optional http instance to use to do the fetch 1694 http: httplib2.Http, optional http instance to use to do the fetch
1134 cache: An optional cache service client that implements get() and set() 1695 cache: An optional cache service client that implements get() and set()
1135 methods. See clientsecrets.loadfile() for details. 1696 methods. See clientsecrets.loadfile() for details.
1697 device_uri: string, OAuth 2.0 device authorization endpoint
1136 1698
1137 Returns: 1699 Returns:
1138 An OAuth2Credentials object. 1700 An OAuth2Credentials object.
1139 1701
1140 Raises: 1702 Raises:
1141 FlowExchangeError if the authorization code cannot be exchanged for an 1703 FlowExchangeError if the authorization code cannot be exchanged for an
1142 access token 1704 access token
1143 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 1705 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1144 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 1706 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1145 invalid. 1707 invalid.
1146 """ 1708 """
1147 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, 1709 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1148 redirect_uri=redirect_uri) 1710 redirect_uri=redirect_uri,
1711 device_uri=device_uri)
1149 credentials = flow.step2_exchange(code, http=http) 1712 credentials = flow.step2_exchange(code, http=http)
1150 return credentials 1713 return credentials
1151 1714
1152 1715
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
1153 class OAuth2WebServerFlow(Flow): 1753 class OAuth2WebServerFlow(Flow):
1154 """Does the Web Server Flow for OAuth 2.0. 1754 """Does the Web Server Flow for OAuth 2.0.
1155 1755
1156 OAuth2WebServerFlow objects may be safely pickled and unpickled. 1756 OAuth2WebServerFlow objects may be safely pickled and unpickled.
1157 """ 1757 """
1158 1758
1159 @util.positional(4) 1759 @util.positional(4)
1160 def __init__(self, client_id, client_secret, scope, 1760 def __init__(self, client_id, client_secret, scope,
1161 redirect_uri=None, 1761 redirect_uri=None,
1162 user_agent=None, 1762 user_agent=None,
1163 auth_uri=GOOGLE_AUTH_URI, 1763 auth_uri=GOOGLE_AUTH_URI,
1164 token_uri=GOOGLE_TOKEN_URI, 1764 token_uri=GOOGLE_TOKEN_URI,
1165 revoke_uri=GOOGLE_REVOKE_URI, 1765 revoke_uri=GOOGLE_REVOKE_URI,
1766 login_hint=None,
1767 device_uri=GOOGLE_DEVICE_URI,
1166 **kwargs): 1768 **kwargs):
1167 """Constructor for OAuth2WebServerFlow. 1769 """Constructor for OAuth2WebServerFlow.
1168 1770
1169 The kwargs argument is used to set extra query parameters on the 1771 The kwargs argument is used to set extra query parameters on the
1170 auth_uri. For example, the access_type and approval_prompt 1772 auth_uri. For example, the access_type and approval_prompt
1171 query parameters can be set via kwargs. 1773 query parameters can be set via kwargs.
1172 1774
1173 Args: 1775 Args:
1174 client_id: string, client identifier. 1776 client_id: string, client identifier.
1175 client_secret: string client secret. 1777 client_secret: string client secret.
1176 scope: string or iterable of strings, scope(s) of the credentials being 1778 scope: string or iterable of strings, scope(s) of the credentials being
1177 requested. 1779 requested.
1178 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1780 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1179 a non-web-based application, or a URI that handles the callback from 1781 a non-web-based application, or a URI that handles the callback from
1180 the authorization server. 1782 the authorization server.
1181 user_agent: string, HTTP User-Agent to provide for this application. 1783 user_agent: string, HTTP User-Agent to provide for this application.
1182 auth_uri: string, URI for authorization endpoint. For convenience 1784 auth_uri: string, URI for authorization endpoint. For convenience
1183 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1785 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1184 token_uri: string, URI for token endpoint. For convenience 1786 token_uri: string, URI for token endpoint. For convenience
1185 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1787 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1186 revoke_uri: string, URI for revoke endpoint. For convenience 1788 revoke_uri: string, URI for revoke endpoint. For convenience
1187 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1789 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.
1188 **kwargs: dict, The keyword arguments are all optional and required 1795 **kwargs: dict, The keyword arguments are all optional and required
1189 parameters for the OAuth calls. 1796 parameters for the OAuth calls.
1190 """ 1797 """
1191 self.client_id = client_id 1798 self.client_id = client_id
1192 self.client_secret = client_secret 1799 self.client_secret = client_secret
1193 self.scope = util.scopes_to_string(scope) 1800 self.scope = util.scopes_to_string(scope)
1194 self.redirect_uri = redirect_uri 1801 self.redirect_uri = redirect_uri
1802 self.login_hint = login_hint
1195 self.user_agent = user_agent 1803 self.user_agent = user_agent
1196 self.auth_uri = auth_uri 1804 self.auth_uri = auth_uri
1197 self.token_uri = token_uri 1805 self.token_uri = token_uri
1198 self.revoke_uri = revoke_uri 1806 self.revoke_uri = revoke_uri
1807 self.device_uri = device_uri
1199 self.params = { 1808 self.params = {
1200 'access_type': 'offline', 1809 'access_type': 'offline',
1201 'response_type': 'code', 1810 'response_type': 'code',
1202 } 1811 }
1203 self.params.update(kwargs) 1812 self.params.update(kwargs)
1204 1813
1205 @util.positional(1) 1814 @util.positional(1)
1206 def step1_get_authorize_url(self, redirect_uri=None): 1815 def step1_get_authorize_url(self, redirect_uri=None):
1207 """Returns a URI to redirect to the provider. 1816 """Returns a URI to redirect to the provider.
1208 1817
1209 Args: 1818 Args:
1210 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1819 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1211 a non-web-based application, or a URI that handles the callback from 1820 a non-web-based application, or a URI that handles the callback from
1212 the authorization server. This parameter is deprecated, please move to 1821 the authorization server. This parameter is deprecated, please move to
1213 passing the redirect_uri in via the constructor. 1822 passing the redirect_uri in via the constructor.
1214 1823
1215 Returns: 1824 Returns:
1216 A URI as a string to redirect the user to begin the authorization flow. 1825 A URI as a string to redirect the user to begin the authorization flow.
1217 """ 1826 """
1218 if redirect_uri is not None: 1827 if redirect_uri is not None:
1219 logger.warning(('The redirect_uri parameter for' 1828 logger.warning((
1220 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please' 1829 'The redirect_uri parameter for '
1830 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please '
1221 'move to passing the redirect_uri in via the constructor.')) 1831 'move to passing the redirect_uri in via the constructor.'))
1222 self.redirect_uri = redirect_uri 1832 self.redirect_uri = redirect_uri
1223 1833
1224 if self.redirect_uri is None: 1834 if self.redirect_uri is None:
1225 raise ValueError('The value of redirect_uri must not be None.') 1835 raise ValueError('The value of redirect_uri must not be None.')
1226 1836
1227 query_params = { 1837 query_params = {
1228 'client_id': self.client_id, 1838 'client_id': self.client_id,
1229 'redirect_uri': self.redirect_uri, 1839 'redirect_uri': self.redirect_uri,
1230 'scope': self.scope, 1840 'scope': self.scope,
1231 } 1841 }
1842 if self.login_hint is not None:
1843 query_params['login_hint'] = self.login_hint
1232 query_params.update(self.params) 1844 query_params.update(self.params)
1233 return _update_query_params(self.auth_uri, query_params) 1845 return _update_query_params(self.auth_uri, query_params)
1234 1846
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
1235 @util.positional(2) 1893 @util.positional(2)
1236 def step2_exchange(self, code, http=None): 1894 def step2_exchange(self, code=None, http=None, device_flow_info=None):
1237 """Exhanges a code for OAuth2Credentials. 1895 """Exchanges a code for OAuth2Credentials.
1238 1896
1239 Args: 1897 Args:
1240 code: string or dict, either the code as a string, or a dictionary 1898
1241 of the query parameters to the redirect_uri, which contains 1899 code: string, a dict-like object, or None. For a non-device
1242 the code. 1900 flow, this is either the response code as a string, or a
1243 http: httplib2.Http, optional http instance to use to do the fetch 1901 dictionary of query parameters to the redirect_uri. For a
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.
1244 1907
1245 Returns: 1908 Returns:
1246 An OAuth2Credentials object that can be used to authorize requests. 1909 An OAuth2Credentials object that can be used to authorize requests.
1247 1910
1248 Raises: 1911 Raises:
1249 FlowExchangeError if a problem occured exchanging the code for a 1912 FlowExchangeError: if a problem occurred exchanging the code for a
1250 refresh_token. 1913 refresh_token.
1914 ValueError: if code and device_flow_info are both provided or both
1915 missing.
1916
1251 """ 1917 """
1918 if code is None and device_flow_info is None:
1919 raise ValueError('No code or device_flow_info provided.')
1920 if code is not None and device_flow_info is not None:
1921 raise ValueError('Cannot provide both code and device_flow_info.')
1252 1922
1253 if not (isinstance(code, str) or isinstance(code, unicode)): 1923 if code is None:
1924 code = device_flow_info.device_code
1925 elif not isinstance(code, six.string_types):
1254 if 'code' not in code: 1926 if 'code' not in code:
1255 if 'error' in code: 1927 raise FlowExchangeError(code.get(
1256 error_msg = code['error'] 1928 'error', 'No code was supplied in the query parameters.'))
1257 else: 1929 code = code['code']
1258 error_msg = 'No code was supplied in the query parameters.'
1259 raise FlowExchangeError(error_msg)
1260 else:
1261 code = code['code']
1262 1930
1263 body = urllib.urlencode({ 1931 post_data = {
1264 'grant_type': 'authorization_code',
1265 'client_id': self.client_id, 1932 'client_id': self.client_id,
1266 'client_secret': self.client_secret, 1933 'client_secret': self.client_secret,
1267 'code': code, 1934 'code': code,
1268 'redirect_uri': self.redirect_uri,
1269 'scope': self.scope, 1935 'scope': self.scope,
1270 }) 1936 }
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)
1271 headers = { 1943 headers = {
1272 'content-type': 'application/x-www-form-urlencoded', 1944 'content-type': 'application/x-www-form-urlencoded',
1273 } 1945 }
1274 1946
1275 if self.user_agent is not None: 1947 if self.user_agent is not None:
1276 headers['user-agent'] = self.user_agent 1948 headers['user-agent'] = self.user_agent
1277 1949
1278 if http is None: 1950 if http is None:
1279 http = httplib2.Http() 1951 http = httplib2.Http()
1280 1952
1281 resp, content = http.request(self.token_uri, method='POST', body=body, 1953 resp, content = http.request(self.token_uri, method='POST', body=body,
1282 headers=headers) 1954 headers=headers)
1283 d = _parse_exchange_token_response(content) 1955 d = _parse_exchange_token_response(content)
1284 if resp.status == 200 and 'access_token' in d: 1956 if resp.status == 200 and 'access_token' in d:
1285 access_token = d['access_token'] 1957 access_token = d['access_token']
1286 refresh_token = d.get('refresh_token', None) 1958 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'.")
1287 token_expiry = None 1963 token_expiry = None
1288 if 'expires_in' in d: 1964 if 'expires_in' in d:
1289 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( 1965 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
1290 seconds=int(d['expires_in'])) 1966 seconds=int(d['expires_in']))
1291 1967
1968 extracted_id_token = None
1292 if 'id_token' in d: 1969 if 'id_token' in d:
1293 d['id_token'] = _extract_id_token(d['id_token']) 1970 extracted_id_token = _extract_id_token(d['id_token'])
1294 1971
1295 logger.info('Successfully retrieved access token') 1972 logger.info('Successfully retrieved access token')
1296 return OAuth2Credentials(access_token, self.client_id, 1973 return OAuth2Credentials(access_token, self.client_id,
1297 self.client_secret, refresh_token, token_expiry, 1974 self.client_secret, refresh_token, token_expiry,
1298 self.token_uri, self.user_agent, 1975 self.token_uri, self.user_agent,
1299 revoke_uri=self.revoke_uri, 1976 revoke_uri=self.revoke_uri,
1300 id_token=d.get('id_token', None), 1977 id_token=extracted_id_token,
1301 token_response=d) 1978 token_response=d)
1302 else: 1979 else:
1303 logger.info('Failed to retrieve access token: %s' % content) 1980 logger.info('Failed to retrieve access token: %s', content)
1304 if 'error' in d: 1981 if 'error' in d:
1305 # you never know what those providers got to say 1982 # you never know what those providers got to say
1306 error_msg = unicode(d['error']) 1983 error_msg = str(d['error']) + str(d.get('error_description', ''))
1307 else: 1984 else:
1308 error_msg = 'Invalid response: %s.' % str(resp.status) 1985 error_msg = 'Invalid response: %s.' % str(resp.status)
1309 raise FlowExchangeError(error_msg) 1986 raise FlowExchangeError(error_msg)
1310 1987
1311 1988
1312 @util.positional(2) 1989 @util.positional(2)
1313 def flow_from_clientsecrets(filename, scope, redirect_uri=None, 1990 def flow_from_clientsecrets(filename, scope, redirect_uri=None,
1314 message=None, cache=None): 1991 message=None, cache=None, login_hint=None,
1992 device_uri=None):
1315 """Create a Flow from a clientsecrets file. 1993 """Create a Flow from a clientsecrets file.
1316 1994
1317 Will create the right kind of Flow based on the contents of the clientsecrets 1995 Will create the right kind of Flow based on the contents of the clientsecrets
1318 file or will raise InvalidClientSecretsError for unknown types of Flows. 1996 file or will raise InvalidClientSecretsError for unknown types of Flows.
1319 1997
1320 Args: 1998 Args:
1321 filename: string, File name of client secrets. 1999 filename: string, File name of client secrets.
1322 scope: string or iterable of strings, scope(s) to request. 2000 scope: string or iterable of strings, scope(s) to request.
1323 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 2001 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1324 a non-web-based application, or a URI that handles the callback from 2002 a non-web-based application, or a URI that handles the callback from
1325 the authorization server. 2003 the authorization server.
1326 message: string, A friendly string to display to the user if the 2004 message: string, A friendly string to display to the user if the
1327 clientsecrets file is missing or invalid. If message is provided then 2005 clientsecrets file is missing or invalid. If message is provided then
1328 sys.exit will be called in the case of an error. If message in not 2006 sys.exit will be called in the case of an error. If message in not
1329 provided then clientsecrets.InvalidClientSecretsError will be raised. 2007 provided then clientsecrets.InvalidClientSecretsError will be raised.
1330 cache: An optional cache service client that implements get() and set() 2008 cache: An optional cache service client that implements get() and set()
1331 methods. See clientsecrets.loadfile() for details. 2009 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.
1332 2015
1333 Returns: 2016 Returns:
1334 A Flow object. 2017 A Flow object.
1335 2018
1336 Raises: 2019 Raises:
1337 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 2020 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1338 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 2021 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1339 invalid. 2022 invalid.
1340 """ 2023 """
1341 try: 2024 try:
1342 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 2025 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
1343 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): 2026 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED):
1344 constructor_kwargs = { 2027 constructor_kwargs = {
1345 'redirect_uri': redirect_uri, 2028 'redirect_uri': redirect_uri,
1346 'auth_uri': client_info['auth_uri'], 2029 'auth_uri': client_info['auth_uri'],
1347 'token_uri': client_info['token_uri'], 2030 'token_uri': client_info['token_uri'],
2031 'login_hint': login_hint,
1348 } 2032 }
1349 revoke_uri = client_info.get('revoke_uri') 2033 revoke_uri = client_info.get('revoke_uri')
1350 if revoke_uri is not None: 2034 if revoke_uri is not None:
1351 constructor_kwargs['revoke_uri'] = revoke_uri 2035 constructor_kwargs['revoke_uri'] = revoke_uri
2036 if device_uri is not None:
2037 constructor_kwargs['device_uri'] = device_uri
1352 return OAuth2WebServerFlow( 2038 return OAuth2WebServerFlow(
1353 client_info['client_id'], client_info['client_secret'], 2039 client_info['client_id'], client_info['client_secret'],
1354 scope, **constructor_kwargs) 2040 scope, **constructor_kwargs)
1355 2041
1356 except clientsecrets.InvalidClientSecretsError: 2042 except clientsecrets.InvalidClientSecretsError:
1357 if message: 2043 if message:
1358 sys.exit(message) 2044 sys.exit(message)
1359 else: 2045 else:
1360 raise 2046 raise
1361 else: 2047 else:
1362 raise UnknownClientSecretsFlowError( 2048 raise UnknownClientSecretsFlowError(
1363 'This OAuth 2.0 flow is unsupported: %r' % client_type) 2049 '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