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