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