| # Copyright 2014 Google Inc. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """An OAuth 2.0 client. |
| |
| Tools for interacting with OAuth 2.0 protected resources. |
| """ |
| |
| import base64 |
| import collections |
| import copy |
| import datetime |
| import json |
| import logging |
| import os |
| import socket |
| import sys |
| import tempfile |
| import time |
| import shutil |
| import six |
| from six.moves import http_client |
| from six.moves import urllib |
| |
| import httplib2 |
| from oauth2client import GOOGLE_AUTH_URI |
| from oauth2client import GOOGLE_DEVICE_URI |
| from oauth2client import GOOGLE_REVOKE_URI |
| from oauth2client import GOOGLE_TOKEN_URI |
| from oauth2client import GOOGLE_TOKEN_INFO_URI |
| from oauth2client._helpers import _from_bytes |
| from oauth2client._helpers import _to_bytes |
| from oauth2client._helpers import _urlsafe_b64decode |
| from oauth2client import clientsecrets |
| from oauth2client import util |
| |
| |
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| |
| HAS_OPENSSL = False |
| HAS_CRYPTO = False |
| try: |
| from oauth2client import crypt |
| HAS_CRYPTO = True |
| if crypt.OpenSSLVerifier is not None: |
| HAS_OPENSSL = True |
| except ImportError: |
| pass |
| |
| |
| logger = logging.getLogger(__name__) |
| |
| # Expiry is stored in RFC3339 UTC format |
| EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' |
| |
| # Which certs to use to validate id_tokens received. |
| ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' |
| # This symbol previously had a typo in the name; we keep the old name |
| # around for now, but will remove it in the future. |
| ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS |
| |
| # Constant to use for the out of band OAuth 2.0 flow. |
| OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' |
| |
| # Google Data client libraries may need to set this to [401, 403]. |
| REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,) |
| |
| # The value representing user credentials. |
| AUTHORIZED_USER = 'authorized_user' |
| |
| # The value representing service account credentials. |
| SERVICE_ACCOUNT = 'service_account' |
| |
| # The environment variable pointing the file with local |
| # Application Default Credentials. |
| GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS' |
| # The ~/.config subdirectory containing gcloud credentials. Intended |
| # to be swapped out in tests. |
| _CLOUDSDK_CONFIG_DIRECTORY = 'gcloud' |
| # The environment variable name which can replace ~/.config if set. |
| _CLOUDSDK_CONFIG_ENV_VAR = 'CLOUDSDK_CONFIG' |
| |
| # The error message we show users when we can't find the Application |
| # Default Credentials. |
| ADC_HELP_MSG = ( |
| 'The Application Default Credentials are not available. They are ' |
| 'available if running in Google Compute Engine. Otherwise, the ' |
| 'environment variable ' + |
| GOOGLE_APPLICATION_CREDENTIALS + |
| ' must be defined pointing to a file defining the credentials. See ' |
| 'https://developers.google.com/accounts/docs/' |
| 'application-default-credentials for more information.') |
| |
| _WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json' |
| |
| # The access token along with the seconds in which it expires. |
| AccessTokenInfo = collections.namedtuple( |
| 'AccessTokenInfo', ['access_token', 'expires_in']) |
| |
| DEFAULT_ENV_NAME = 'UNKNOWN' |
| |
| # If set to True _get_environment avoid GCE check (_detect_gce_environment) |
| NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False') |
| |
| _SERVER_SOFTWARE = 'SERVER_SOFTWARE' |
| _GCE_METADATA_HOST = '169.254.169.254' |
| _METADATA_FLAVOR_HEADER = 'Metadata-Flavor' |
| _DESIRED_METADATA_FLAVOR = 'Google' |
| |
| # Expose utcnow() at module level to allow for |
| # easier testing (by replacing with a stub). |
| _UTCNOW = datetime.datetime.utcnow |
| |
| |
| class SETTINGS(object): |
| """Settings namespace for globally defined values.""" |
| env_name = None |
| |
| |
| class Error(Exception): |
| """Base error for this module.""" |
| |
| |
| class FlowExchangeError(Error): |
| """Error trying to exchange an authorization grant for an access token.""" |
| |
| |
| class AccessTokenRefreshError(Error): |
| """Error trying to refresh an expired access token.""" |
| |
| |
| class HttpAccessTokenRefreshError(AccessTokenRefreshError): |
| """Error (with HTTP status) trying to refresh an expired access token.""" |
| def __init__(self, *args, **kwargs): |
| super(HttpAccessTokenRefreshError, self).__init__(*args) |
| self.status = kwargs.get('status') |
| |
| |
| class TokenRevokeError(Error): |
| """Error trying to revoke a token.""" |
| |
| |
| class UnknownClientSecretsFlowError(Error): |
| """The client secrets file called for an unknown type of OAuth 2.0 flow.""" |
| |
| |
| class AccessTokenCredentialsError(Error): |
| """Having only the access_token means no refresh is possible.""" |
| |
| |
| class VerifyJwtTokenError(Error): |
| """Could not retrieve certificates for validation.""" |
| |
| |
| class NonAsciiHeaderError(Error): |
| """Header names and values must be ASCII strings.""" |
| |
| |
| class ApplicationDefaultCredentialsError(Error): |
| """Error retrieving the Application Default Credentials.""" |
| |
| |
| class OAuth2DeviceCodeError(Error): |
| """Error trying to retrieve a device code.""" |
| |
| |
| class CryptoUnavailableError(Error, NotImplementedError): |
| """Raised when a crypto library is required, but none is available.""" |
| |
| |
| def _abstract(): |
| raise NotImplementedError('You need to override this function') |
| |
| |
| class MemoryCache(object): |
| """httplib2 Cache implementation which only caches locally.""" |
| |
| def __init__(self): |
| self.cache = {} |
| |
| def get(self, key): |
| return self.cache.get(key) |
| |
| def set(self, key, value): |
| self.cache[key] = value |
| |
| def delete(self, key): |
| self.cache.pop(key, None) |
| |
| |
| def _parse_expiry(expiry): |
| if expiry and isinstance(expiry, datetime.datetime): |
| return expiry.strftime(EXPIRY_FORMAT) |
| else: |
| return None |
| |
| |
| class Credentials(object): |
| """Base class for all Credentials objects. |
| |
| Subclasses must define an authorize() method that applies the credentials |
| to an HTTP transport. |
| |
| Subclasses must also specify a classmethod named 'from_json' that takes a |
| JSON string as input and returns an instantiated Credentials object. |
| """ |
| |
| NON_SERIALIZED_MEMBERS = frozenset(['store']) |
| |
| def authorize(self, http): |
| """Take an httplib2.Http instance (or equivalent) and authorizes it. |
| |
| Authorizes it for the set of credentials, usually by replacing |
| http.request() with a method that adds in the appropriate headers and |
| then delegates to the original Http.request() method. |
| |
| Args: |
| http: httplib2.Http, an http object to be used to make the refresh |
| request. |
| """ |
| _abstract() |
| |
| def refresh(self, http): |
| """Forces a refresh of the access_token. |
| |
| Args: |
| http: httplib2.Http, an http object to be used to make the refresh |
| request. |
| """ |
| _abstract() |
| |
| def revoke(self, http): |
| """Revokes a refresh_token and makes the credentials void. |
| |
| Args: |
| http: httplib2.Http, an http object to be used to make the revoke |
| request. |
| """ |
| _abstract() |
| |
| def apply(self, headers): |
| """Add the authorization to the headers. |
| |
| Args: |
| headers: dict, the headers to add the Authorization header to. |
| """ |
| _abstract() |
| |
| def _to_json(self, strip, to_serialize=None): |
| """Utility function that creates JSON repr. of a Credentials object. |
| |
| Args: |
| strip: array, An array of names of members to exclude from the |
| JSON. |
| to_serialize: dict, (Optional) The properties for this object |
| that will be serialized. This allows callers to modify |
| before serializing. |
| |
| Returns: |
| string, a JSON representation of this instance, suitable to pass to |
| from_json(). |
| """ |
| curr_type = self.__class__ |
| if to_serialize is None: |
| to_serialize = copy.copy(self.__dict__) |
| for member in strip: |
| if member in to_serialize: |
| del to_serialize[member] |
| to_serialize['token_expiry'] = _parse_expiry( |
| to_serialize.get('token_expiry')) |
| # Add in information we will need later to reconstitute this instance. |
| to_serialize['_class'] = curr_type.__name__ |
| to_serialize['_module'] = curr_type.__module__ |
| for key, val in to_serialize.items(): |
| if isinstance(val, bytes): |
| to_serialize[key] = val.decode('utf-8') |
| if isinstance(val, set): |
| to_serialize[key] = list(val) |
| return json.dumps(to_serialize) |
| |
| def to_json(self): |
| """Creating a JSON representation of an instance of Credentials. |
| |
| Returns: |
| string, a JSON representation of this instance, suitable to pass to |
| from_json(). |
| """ |
| return self._to_json(self.NON_SERIALIZED_MEMBERS) |
| |
| @classmethod |
| def new_from_json(cls, json_data): |
| """Utility class method to instantiate a Credentials subclass from JSON. |
| |
| Expects the JSON string to have been produced by to_json(). |
| |
| Args: |
| json_data: string or bytes, JSON from to_json(). |
| |
| Returns: |
| An instance of the subclass of Credentials that was serialized with |
| to_json(). |
| """ |
| json_data_as_unicode = _from_bytes(json_data) |
| data = json.loads(json_data_as_unicode) |
| # Find and call the right classmethod from_json() to restore |
| # the object. |
| module_name = data['_module'] |
| try: |
| module_obj = __import__(module_name) |
| except ImportError: |
| # In case there's an object from the old package structure, |
| # update it |
| module_name = module_name.replace('.googleapiclient', '') |
| module_obj = __import__(module_name) |
| |
| module_obj = __import__(module_name, |
| fromlist=module_name.split('.')[:-1]) |
| kls = getattr(module_obj, data['_class']) |
| return kls.from_json(json_data_as_unicode) |
| |
| @classmethod |
| def from_json(cls, unused_data): |
| """Instantiate a Credentials object from a JSON description of it. |
| |
| The JSON should have been produced by calling .to_json() on the object. |
| |
| Args: |
| unused_data: dict, A deserialized JSON object. |
| |
| Returns: |
| An instance of a Credentials subclass. |
| """ |
| return Credentials() |
| |
| |
| class Flow(object): |
| """Base class for all Flow objects.""" |
| pass |
| |
| |
| class Storage(object): |
| """Base class for all Storage objects. |
| |
| Store and retrieve a single credential. This class supports locking |
| such that multiple processes and threads can operate on a single |
| store. |
| """ |
| def __init__(self, lock=None): |
| """Create a Storage instance. |
| |
| Args: |
| lock: An optional threading.Lock-like object. Must implement at |
| least acquire() and release(). Does not need to be re-entrant. |
| """ |
| self._lock = lock |
| |
| def acquire_lock(self): |
| """Acquires any lock necessary to access this Storage. |
| |
| This lock is not reentrant. |
| """ |
| if self._lock is not None: |
| self._lock.acquire() |
| |
| def release_lock(self): |
| """Release the Storage lock. |
| |
| Trying to release a lock that isn't held will result in a |
| RuntimeError in the case of a threading.Lock or multiprocessing.Lock. |
| """ |
| if self._lock is not None: |
| self._lock.release() |
| |
| def locked_get(self): |
| """Retrieve credential. |
| |
| The Storage lock must be held when this is called. |
| |
| Returns: |
| oauth2client.client.Credentials |
| """ |
| _abstract() |
| |
| def locked_put(self, credentials): |
| """Write a credential. |
| |
| The Storage lock must be held when this is called. |
| |
| Args: |
| credentials: Credentials, the credentials to store. |
| """ |
| _abstract() |
| |
| def locked_delete(self): |
| """Delete a credential. |
| |
| The Storage lock must be held when this is called. |
| """ |
| _abstract() |
| |
| def get(self): |
| """Retrieve credential. |
| |
| The Storage lock must *not* be held when this is called. |
| |
| Returns: |
| oauth2client.client.Credentials |
| """ |
| self.acquire_lock() |
| try: |
| return self.locked_get() |
| finally: |
| self.release_lock() |
| |
| def put(self, credentials): |
| """Write a credential. |
| |
| The Storage lock must be held when this is called. |
| |
| Args: |
| credentials: Credentials, the credentials to store. |
| """ |
| self.acquire_lock() |
| try: |
| self.locked_put(credentials) |
| finally: |
| self.release_lock() |
| |
| def delete(self): |
| """Delete credential. |
| |
| Frees any resources associated with storing the credential. |
| The Storage lock must *not* be held when this is called. |
| |
| Returns: |
| None |
| """ |
| self.acquire_lock() |
| try: |
| return self.locked_delete() |
| finally: |
| self.release_lock() |
| |
| |
| def clean_headers(headers): |
| """Forces header keys and values to be strings, i.e not unicode. |
| |
| The httplib module just concats the header keys and values in a way that |
| may make the message header a unicode string, which, if it then tries to |
| contatenate to a binary request body may result in a unicode decode error. |
| |
| Args: |
| headers: dict, A dictionary of headers. |
| |
| Returns: |
| The same dictionary but with all the keys converted to strings. |
| """ |
| clean = {} |
| try: |
| for k, v in six.iteritems(headers): |
| if not isinstance(k, six.binary_type): |
| k = str(k) |
| if not isinstance(v, six.binary_type): |
| v = str(v) |
| clean[_to_bytes(k)] = _to_bytes(v) |
| except UnicodeEncodeError: |
| raise NonAsciiHeaderError(k, ': ', v) |
| return clean |
| |
| |
| def _update_query_params(uri, params): |
| """Updates a URI with new query parameters. |
| |
| Args: |
| uri: string, A valid URI, with potential existing query parameters. |
| params: dict, A dictionary of query parameters. |
| |
| Returns: |
| The same URI but with the new query parameters added. |
| """ |
| parts = urllib.parse.urlparse(uri) |
| query_params = dict(urllib.parse.parse_qsl(parts.query)) |
| query_params.update(params) |
| new_parts = parts._replace(query=urllib.parse.urlencode(query_params)) |
| return urllib.parse.urlunparse(new_parts) |
| |
| |
| class OAuth2Credentials(Credentials): |
| """Credentials object for OAuth 2.0. |
| |
| Credentials can be applied to an httplib2.Http object using the authorize() |
| method, which then adds the OAuth 2.0 access token to each request. |
| |
| OAuth2Credentials objects may be safely pickled and unpickled. |
| """ |
| |
| @util.positional(8) |
| def __init__(self, access_token, client_id, client_secret, refresh_token, |
| token_expiry, token_uri, user_agent, revoke_uri=None, |
| id_token=None, token_response=None, scopes=None, |
| token_info_uri=None): |
| """Create an instance of OAuth2Credentials. |
| |
| This constructor is not usually called by the user, instead |
| OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. |
| |
| Args: |
| access_token: string, access token. |
| client_id: string, client identifier. |
| client_secret: string, client secret. |
| refresh_token: string, refresh token. |
| token_expiry: datetime, when the access_token expires. |
| token_uri: string, URI of token endpoint. |
| user_agent: string, The HTTP User-Agent to provide for this |
| application. |
| revoke_uri: string, URI for revoke endpoint. Defaults to None; a |
| token can't be revoked if this is None. |
| id_token: object, The identity of the resource owner. |
| token_response: dict, the decoded response to the token request. |
| None if a token hasn't been requested yet. Stored |
| because some providers (e.g. wordpress.com) include |
| extra fields that clients may want. |
| scopes: list, authorized scopes for these credentials. |
| token_info_uri: string, the URI for the token info endpoint. Defaults |
| to None; scopes can not be refreshed if this is None. |
| |
| Notes: |
| store: callable, A callable that when passed a Credential |
| will store the credential back to where it came from. |
| This is needed to store the latest access_token if it |
| has expired and been refreshed. |
| """ |
| self.access_token = access_token |
| self.client_id = client_id |
| self.client_secret = client_secret |
| self.refresh_token = refresh_token |
| self.store = None |
| self.token_expiry = token_expiry |
| self.token_uri = token_uri |
| self.user_agent = user_agent |
| self.revoke_uri = revoke_uri |
| self.id_token = id_token |
| self.token_response = token_response |
| self.scopes = set(util.string_to_scopes(scopes or [])) |
| self.token_info_uri = token_info_uri |
| |
| # True if the credentials have been revoked or expired and can't be |
| # refreshed. |
| self.invalid = False |
| |
| def authorize(self, http): |
| """Authorize an httplib2.Http instance with these credentials. |
| |
| The modified http.request method will add authentication headers to |
| each request and will refresh access_tokens when a 401 is received on a |
| request. In addition the http.request method has a credentials |
| property, http.request.credentials, which is the Credentials object |
| that authorized it. |
| |
| Args: |
| http: An instance of ``httplib2.Http`` or something that acts |
| like it. |
| |
| Returns: |
| A modified instance of http that was passed in. |
| |
| Example:: |
| |
| h = httplib2.Http() |
| h = credentials.authorize(h) |
| |
| You can't create a new OAuth subclass of httplib2.Authentication |
| because it never gets passed the absolute URI, which is needed for |
| signing. So instead we have to overload 'request' with a closure |
| that adds in the Authorization header and then calls the original |
| version of 'request()'. |
| """ |
| request_orig = http.request |
| |
| # The closure that will replace 'httplib2.Http.request'. |
| def new_request(uri, method='GET', body=None, headers=None, |
| redirections=httplib2.DEFAULT_MAX_REDIRECTS, |
| connection_type=None): |
| if not self.access_token: |
| logger.info('Attempting refresh to obtain ' |
| 'initial access_token') |
| self._refresh(request_orig) |
| |
| # Clone and modify the request headers to add the appropriate |
| # Authorization header. |
| if headers is None: |
| headers = {} |
| else: |
| headers = dict(headers) |
| self.apply(headers) |
| |
| if self.user_agent is not None: |
| if 'user-agent' in headers: |
| headers['user-agent'] = (self.user_agent + ' ' + |
| headers['user-agent']) |
| else: |
| headers['user-agent'] = self.user_agent |
| |
| body_stream_position = None |
| if all(getattr(body, stream_prop, None) for stream_prop in |
| ('read', 'seek', 'tell')): |
| body_stream_position = body.tell() |
| |
| resp, content = request_orig(uri, method, body, |
| clean_headers(headers), |
| redirections, connection_type) |
| |
| # A stored token may expire between the time it is retrieved and |
| # the time the request is made, so we may need to try twice. |
| max_refresh_attempts = 2 |
| for refresh_attempt in range(max_refresh_attempts): |
| if resp.status not in REFRESH_STATUS_CODES: |
| break |
| logger.info('Refreshing due to a %s (attempt %s/%s)', |
| resp.status, refresh_attempt + 1, |
| max_refresh_attempts) |
| self._refresh(request_orig) |
| self.apply(headers) |
| if body_stream_position is not None: |
| body.seek(body_stream_position) |
| |
| resp, content = request_orig(uri, method, body, |
| clean_headers(headers), |
| redirections, connection_type) |
| |
| return (resp, content) |
| |
| # Replace the request method with our own closure. |
| http.request = new_request |
| |
| # Set credentials as a property of the request method. |
| setattr(http.request, 'credentials', self) |
| |
| return http |
| |
| def refresh(self, http): |
| """Forces a refresh of the access_token. |
| |
| Args: |
| http: httplib2.Http, an http object to be used to make the refresh |
| request. |
| """ |
| self._refresh(http.request) |
| |
| def revoke(self, http): |
| """Revokes a refresh_token and makes the credentials void. |
| |
| Args: |
| http: httplib2.Http, an http object to be used to make the revoke |
| request. |
| """ |
| self._revoke(http.request) |
| |
| def apply(self, headers): |
| """Add the authorization to the headers. |
| |
| Args: |
| headers: dict, the headers to add the Authorization header to. |
| """ |
| headers['Authorization'] = 'Bearer ' + self.access_token |
| |
| def has_scopes(self, scopes): |
| """Verify that the credentials are authorized for the given scopes. |
| |
| Returns True if the credentials authorized scopes contain all of the |
| scopes given. |
| |
| Args: |
| scopes: list or string, the scopes to check. |
| |
| Notes: |
| There are cases where the credentials are unaware of which scopes |
| are authorized. Notably, credentials obtained and stored before |
| this code was added will not have scopes, AccessTokenCredentials do |
| not have scopes. In both cases, you can use refresh_scopes() to |
| obtain the canonical set of scopes. |
| """ |
| scopes = util.string_to_scopes(scopes) |
| return set(scopes).issubset(self.scopes) |
| |
| def retrieve_scopes(self, http): |
| """Retrieves the canonical list of scopes for this access token. |
| |
| Gets the scopes from the OAuth2 provider. |
| |
| Args: |
| http: httplib2.Http, an http object to be used to make the refresh |
| request. |
| |
| Returns: |
| A set of strings containing the canonical list of scopes. |
| """ |
| self._retrieve_scopes(http.request) |
| return self.scopes |
| |
| @classmethod |
| def from_json(cls, json_data): |
| """Instantiate a Credentials object from a JSON description of it. |
| |
| The JSON should have been produced by calling .to_json() on the object. |
| |
| Args: |
| json_data: string or bytes, JSON to deserialize. |
| |
| Returns: |
| An instance of a Credentials subclass. |
| """ |
| data = json.loads(_from_bytes(json_data)) |
| if (data.get('token_expiry') and |
| not isinstance(data['token_expiry'], datetime.datetime)): |
| try: |
| data['token_expiry'] = datetime.datetime.strptime( |
| data['token_expiry'], EXPIRY_FORMAT) |
| except ValueError: |
| data['token_expiry'] = None |
| retval = cls( |
| data['access_token'], |
| data['client_id'], |
| data['client_secret'], |
| data['refresh_token'], |
| data['token_expiry'], |
| data['token_uri'], |
| data['user_agent'], |
| revoke_uri=data.get('revoke_uri', None), |
| id_token=data.get('id_token', None), |
| token_response=data.get('token_response', None), |
| scopes=data.get('scopes', None), |
| token_info_uri=data.get('token_info_uri', None)) |
| retval.invalid = data['invalid'] |
| return retval |
| |
| @property |
| def access_token_expired(self): |
| """True if the credential is expired or invalid. |
| |
| If the token_expiry isn't set, we assume the token doesn't expire. |
| """ |
| if self.invalid: |
| return True |
| |
| if not self.token_expiry: |
| return False |
| |
| now = _UTCNOW() |
| if now >= self.token_expiry: |
| logger.info('access_token is expired. Now: %s, token_expiry: %s', |
| now, self.token_expiry) |
| return True |
| return False |
| |
| def get_access_token(self, http=None): |
| """Return the access token and its expiration information. |
| |
| If the token does not exist, get one. |
| If the token expired, refresh it. |
| """ |
| if not self.access_token or self.access_token_expired: |
| if not http: |
| http = httplib2.Http() |
| self.refresh(http) |
| return AccessTokenInfo(access_token=self.access_token, |
| expires_in=self._expires_in()) |
| |
| def set_store(self, store): |
| """Set the Storage for the credential. |
| |
| Args: |
| store: Storage, an implementation of Storage object. |
| This is needed to store the latest access_token if it |
| has expired and been refreshed. This implementation uses |
| locking to check for updates before updating the |
| access_token. |
| """ |
| self.store = store |
| |
| def _expires_in(self): |
| """Return the number of seconds until this token expires. |
| |
| If token_expiry is in the past, this method will return 0, meaning the |
| token has already expired. |
| |
| If token_expiry is None, this method will return None. Note that |
| returning 0 in such a case would not be fair: the token may still be |
| valid; we just don't know anything about it. |
| """ |
| if self.token_expiry: |
| now = _UTCNOW() |
| if self.token_expiry > now: |
| time_delta = self.token_expiry - now |
| # TODO(orestica): return time_delta.total_seconds() |
| # once dropping support for Python 2.6 |
| return time_delta.days * 86400 + time_delta.seconds |
| else: |
| return 0 |
| |
| def _updateFromCredential(self, other): |
| """Update this Credential from another instance.""" |
| self.__dict__.update(other.__getstate__()) |
| |
| def __getstate__(self): |
| """Trim the state down to something that can be pickled.""" |
| d = copy.copy(self.__dict__) |
| del d['store'] |
| return d |
| |
| def __setstate__(self, state): |
| """Reconstitute the state of the object from being pickled.""" |
| self.__dict__.update(state) |
| self.store = None |
| |
| def _generate_refresh_request_body(self): |
| """Generate the body that will be used in the refresh request.""" |
| body = urllib.parse.urlencode({ |
| 'grant_type': 'refresh_token', |
| 'client_id': self.client_id, |
| 'client_secret': self.client_secret, |
| 'refresh_token': self.refresh_token, |
| }) |
| return body |
| |
| def _generate_refresh_request_headers(self): |
| """Generate the headers that will be used in the refresh request.""" |
| headers = { |
| 'content-type': 'application/x-www-form-urlencoded', |
| } |
| |
| if self.user_agent is not None: |
| headers['user-agent'] = self.user_agent |
| |
| return headers |
| |
| def _refresh(self, http_request): |
| """Refreshes the access_token. |
| |
| This method first checks by reading the Storage object if available. |
| If a refresh is still needed, it holds the Storage lock until the |
| refresh is completed. |
| |
| Args: |
| http_request: callable, a callable that matches the method |
| signature of httplib2.Http.request, used to make the |
| refresh request. |
| |
| Raises: |
| HttpAccessTokenRefreshError: When the refresh fails. |
| """ |
| if not self.store: |
| self._do_refresh_request(http_request) |
| else: |
| self.store.acquire_lock() |
| try: |
| new_cred = self.store.locked_get() |
| |
| if (new_cred and not new_cred.invalid and |
| new_cred.access_token != self.access_token and |
| not new_cred.access_token_expired): |
| logger.info('Updated access_token read from Storage') |
| self._updateFromCredential(new_cred) |
| else: |
| self._do_refresh_request(http_request) |
| finally: |
| self.store.release_lock() |
| |
| def _do_refresh_request(self, http_request): |
| """Refresh the access_token using the refresh_token. |
| |
| Args: |
| http_request: callable, a callable that matches the method |
| signature of httplib2.Http.request, used to make the |
| refresh request. |
| |
| Raises: |
| HttpAccessTokenRefreshError: When the refresh fails. |
| """ |
| body = self._generate_refresh_request_body() |
| headers = self._generate_refresh_request_headers() |
| |
| logger.info('Refreshing access_token') |
| resp, content = http_request( |
| self.token_uri, method='POST', body=body, headers=headers) |
| content = _from_bytes(content) |
| if resp.status == http_client.OK: |
| d = json.loads(content) |
| self.token_response = d |
| self.access_token = d['access_token'] |
| self.refresh_token = d.get('refresh_token', self.refresh_token) |
| if 'expires_in' in d: |
| delta = datetime.timedelta(seconds=int(d['expires_in'])) |
| self.token_expiry = delta + _UTCNOW() |
| else: |
| self.token_expiry = None |
| if 'id_token' in d: |
| self.id_token = _extract_id_token(d['id_token']) |
| else: |
| self.id_token = None |
| # On temporary refresh errors, the user does not actually have to |
| # re-authorize, so we unflag here. |
| self.invalid = False |
| if self.store: |
| self.store.locked_put(self) |
| else: |
| # An {'error':...} response body means the token is expired or |
| # revoked, so we flag the credentials as such. |
| logger.info('Failed to retrieve access token: %s', content) |
| error_msg = 'Invalid response %s.' % resp['status'] |
| try: |
| d = json.loads(content) |
| if 'error' in d: |
| error_msg = d['error'] |
| if 'error_description' in d: |
| error_msg += ': ' + d['error_description'] |
| self.invalid = True |
| if self.store: |
| self.store.locked_put(self) |
| except (TypeError, ValueError): |
| pass |
| raise HttpAccessTokenRefreshError(error_msg, status=resp.status) |
| |
| def _revoke(self, http_request): |
| """Revokes this credential and deletes the stored copy (if it exists). |
| |
| Args: |
| http_request: callable, a callable that matches the method |
| signature of httplib2.Http.request, used to make the |
| revoke request. |
| """ |
| self._do_revoke(http_request, self.refresh_token or self.access_token) |
| |
| def _do_revoke(self, http_request, token): |
| """Revokes this credential and deletes the stored copy (if it exists). |
| |
| Args: |
| http_request: callable, a callable that matches the method |
| signature of httplib2.Http.request, used to make the |
| refresh request. |
| token: A string used as the token to be revoked. Can be either an |
| access_token or refresh_token. |
| |
| Raises: |
| TokenRevokeError: If the revoke request does not return with a |
| 200 OK. |
| """ |
| logger.info('Revoking token') |
| query_params = {'token': token} |
| token_revoke_uri = _update_query_params(self.revoke_uri, query_params) |
| resp, content = http_request(token_revoke_uri) |
| if resp.status == http_client.OK: |
| self.invalid = True |
| else: |
| error_msg = 'Invalid response %s.' % resp.status |
| try: |
| d = json.loads(_from_bytes(content)) |
| if 'error' in d: |
| error_msg = d['error'] |
| except (TypeError, ValueError): |
| pass |
| raise TokenRevokeError(error_msg) |
| |
| if self.store: |
| self.store.delete() |
| |
| def _retrieve_scopes(self, http_request): |
| """Retrieves the list of authorized scopes from the OAuth2 provider. |
| |
| Args: |
| http_request: callable, a callable that matches the method |
| signature of httplib2.Http.request, used to make the |
| revoke request. |
| """ |
| self._do_retrieve_scopes(http_request, self.access_token) |
| |
| def _do_retrieve_scopes(self, http_request, token): |
| """Retrieves the list of authorized scopes from the OAuth2 provider. |
| |
| Args: |
| http_request: callable, a callable that matches the method |
| signature of httplib2.Http.request, used to make the |
| refresh request. |
| token: A string used as the token to identify the credentials to |
| the provider. |
| |
| Raises: |
| Error: When refresh fails, indicating the the access token is |
| invalid. |
| """ |
| logger.info('Refreshing scopes') |
| query_params = {'access_token': token, 'fields': 'scope'} |
| token_info_uri = _update_query_params(self.token_info_uri, |
| query_params) |
| resp, content = http_request(token_info_uri) |
| content = _from_bytes(content) |
| if resp.status == http_client.OK: |
| d = json.loads(content) |
| self.scopes = set(util.string_to_scopes(d.get('scope', ''))) |
| else: |
| error_msg = 'Invalid response %s.' % (resp.status,) |
| try: |
| d = json.loads(content) |
| if 'error_description' in d: |
| error_msg = d['error_description'] |
| except (TypeError, ValueError): |
| pass |
| raise Error(error_msg) |
| |
| |
| class AccessTokenCredentials(OAuth2Credentials): |
| """Credentials object for OAuth 2.0. |
| |
| Credentials can be applied to an httplib2.Http object using the |
| authorize() method, which then signs each request from that object |
| with the OAuth 2.0 access token. This set of credentials is for the |
| use case where you have acquired an OAuth 2.0 access_token from |
| another place such as a JavaScript client or another web |
| application, and wish to use it from Python. Because only the |
| access_token is present it can not be refreshed and will in time |
| expire. |
| |
| AccessTokenCredentials objects may be safely pickled and unpickled. |
| |
| Usage:: |
| |
| credentials = AccessTokenCredentials('<an access token>', |
| 'my-user-agent/1.0') |
| http = httplib2.Http() |
| http = credentials.authorize(http) |
| |
| Raises: |
| AccessTokenCredentialsExpired: raised when the access_token expires or |
| is revoked. |
| """ |
| |
| def __init__(self, access_token, user_agent, revoke_uri=None): |
| """Create an instance of OAuth2Credentials |
| |
| This is one of the few types if Credentials that you should contrust, |
| Credentials objects are usually instantiated by a Flow. |
| |
| Args: |
| access_token: string, access token. |
| user_agent: string, The HTTP User-Agent to provide for this |
| application. |
| revoke_uri: string, URI for revoke endpoint. Defaults to None; a |
| token can't be revoked if this is None. |
| """ |
| super(AccessTokenCredentials, self).__init__( |
| access_token, |
| None, |
| None, |
| None, |
| None, |
| None, |
| user_agent, |
| revoke_uri=revoke_uri) |
| |
| @classmethod |
| def from_json(cls, json_data): |
| data = json.loads(_from_bytes(json_data)) |
| retval = AccessTokenCredentials( |
| data['access_token'], |
| data['user_agent']) |
| return retval |
| |
| def _refresh(self, http_request): |
| raise AccessTokenCredentialsError( |
| 'The access_token is expired or invalid and can\'t be refreshed.') |
| |
| def _revoke(self, http_request): |
| """Revokes the access_token and deletes the store if available. |
| |
| Args: |
| http_request: callable, a callable that matches the method |
| signature of httplib2.Http.request, used to make the |
| revoke request. |
| """ |
| self._do_revoke(http_request, self.access_token) |
| |
| |
| def _detect_gce_environment(): |
| """Determine if the current environment is Compute Engine. |
| |
| Returns: |
| Boolean indicating whether or not the current environment is Google |
| Compute Engine. |
| """ |
| # NOTE: The explicit ``timeout`` is a workaround. The underlying |
| # issue is that resolving an unknown host on some networks will take |
| # 20-30 seconds; making this timeout short fixes the issue, but |
| # could lead to false negatives in the event that we are on GCE, but |
| # the metadata resolution was particularly slow. The latter case is |
| # "unlikely". |
| connection = six.moves.http_client.HTTPConnection( |
| _GCE_METADATA_HOST, timeout=1) |
| |
| try: |
| headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR} |
| connection.request('GET', '/', headers=headers) |
| response = connection.getresponse() |
| if response.status == http_client.OK: |
| return (response.getheader(_METADATA_FLAVOR_HEADER) == |
| _DESIRED_METADATA_FLAVOR) |
| except socket.error: # socket.timeout or socket.error(64, 'Host is down') |
| logger.info('Timeout attempting to reach GCE metadata service.') |
| return False |
| finally: |
| connection.close() |
| |
| |
| def _in_gae_environment(): |
| """Detects if the code is running in the App Engine environment. |
| |
| Returns: |
| True if running in the GAE environment, False otherwise. |
| """ |
| if SETTINGS.env_name is not None: |
| return SETTINGS.env_name in ('GAE_PRODUCTION', 'GAE_LOCAL') |
| |
| try: |
| import google.appengine # noqa: unused import |
| except ImportError: |
| pass |
| else: |
| server_software = os.environ.get(_SERVER_SOFTWARE, '') |
| if server_software.startswith('Google App Engine/'): |
| SETTINGS.env_name = 'GAE_PRODUCTION' |
| return True |
| elif server_software.startswith('Development/'): |
| SETTINGS.env_name = 'GAE_LOCAL' |
| return True |
| |
| return False |
| |
| |
| def _in_gce_environment(): |
| """Detect if the code is running in the Compute Engine environment. |
| |
| Returns: |
| True if running in the GCE environment, False otherwise. |
| """ |
| if SETTINGS.env_name is not None: |
| return SETTINGS.env_name == 'GCE_PRODUCTION' |
| |
| if NO_GCE_CHECK != 'True' and _detect_gce_environment(): |
| SETTINGS.env_name = 'GCE_PRODUCTION' |
| return True |
| return False |
| |
| |
| class GoogleCredentials(OAuth2Credentials): |
| """Application Default Credentials for use in calling Google APIs. |
| |
| The Application Default Credentials are being constructed as a function of |
| the environment where the code is being run. |
| More details can be found on this page: |
| https://developers.google.com/accounts/docs/application-default-credentials |
| |
| Here is an example of how to use the Application Default Credentials for a |
| service that requires authentication:: |
| |
| from googleapiclient.discovery import build |
| from oauth2client.client import GoogleCredentials |
| |
| credentials = GoogleCredentials.get_application_default() |
| service = build('compute', 'v1', credentials=credentials) |
| |
| PROJECT = 'bamboo-machine-422' |
| ZONE = 'us-central1-a' |
| request = service.instances().list(project=PROJECT, zone=ZONE) |
| response = request.execute() |
| |
| print(response) |
| """ |
| |
| NON_SERIALIZED_MEMBERS = ( |
| frozenset(['_private_key']) | |
| OAuth2Credentials.NON_SERIALIZED_MEMBERS) |
| """Members that aren't serialized when object is converted to JSON.""" |
| |
| def __init__(self, access_token, client_id, client_secret, refresh_token, |
| token_expiry, token_uri, user_agent, |
| revoke_uri=GOOGLE_REVOKE_URI): |
| """Create an instance of GoogleCredentials. |
| |
| This constructor is not usually called by the user, instead |
| GoogleCredentials objects are instantiated by |
| GoogleCredentials.from_stream() or |
| GoogleCredentials.get_application_default(). |
| |
| Args: |
| access_token: string, access token. |
| client_id: string, client identifier. |
| client_secret: string, client secret. |
| refresh_token: string, refresh token. |
| token_expiry: datetime, when the access_token expires. |
| token_uri: string, URI of token endpoint. |
| user_agent: string, The HTTP User-Agent to provide for this |
| application. |
| revoke_uri: string, URI for revoke endpoint. Defaults to |
| GOOGLE_REVOKE_URI; a token can't be revoked if this |
| is None. |
| """ |
| super(GoogleCredentials, self).__init__( |
| access_token, client_id, client_secret, refresh_token, |
| token_expiry, token_uri, user_agent, revoke_uri=revoke_uri) |
| |
| def create_scoped_required(self): |
| """Whether this Credentials object is scopeless. |
| |
| create_scoped(scopes) method needs to be called in order to create |
| a Credentials object for API calls. |
| """ |
| return False |
| |
| def create_scoped(self, scopes): |
| """Create a Credentials object for the given scopes. |
| |
| The Credentials type is preserved. |
| """ |
| return self |
| |
| @classmethod |
| def from_json(cls, json_data): |
| # TODO(issue 388): eliminate the circularity that is the reason for |
| # this non-top-level import. |
| from oauth2client.service_account import ServiceAccountCredentials |
| data = json.loads(_from_bytes(json_data)) |
| |
| # We handle service_account.ServiceAccountCredentials since it is a |
| # possible return type of GoogleCredentials.get_application_default() |
| if (data['_module'] == 'oauth2client.service_account' and |
| data['_class'] == 'ServiceAccountCredentials'): |
| return ServiceAccountCredentials.from_json(data) |
| |
| token_expiry = _parse_expiry(data.get('token_expiry')) |
| google_credentials = cls( |
| data['access_token'], |
| data['client_id'], |
| data['client_secret'], |
| data['refresh_token'], |
| token_expiry, |
| data['token_uri'], |
| data['user_agent'], |
| revoke_uri=data.get('revoke_uri', None)) |
| google_credentials.invalid = data['invalid'] |
| return google_credentials |
| |
| @property |
| def serialization_data(self): |
| """Get the fields and values identifying the current credentials.""" |
| return { |
| 'type': 'authorized_user', |
| 'client_id': self.client_id, |
| 'client_secret': self.client_secret, |
| 'refresh_token': self.refresh_token |
| } |
| |
| @staticmethod |
| def _implicit_credentials_from_gae(): |
| """Attempts to get implicit credentials in Google App Engine env. |
| |
| If the current environment is not detected as App Engine, returns None, |
| indicating no Google App Engine credentials can be detected from the |
| current environment. |
| |
| Returns: |
| None, if not in GAE, else an appengine.AppAssertionCredentials |
| object. |
| """ |
| if not _in_gae_environment(): |
| return None |
| |
| return _get_application_default_credential_GAE() |
| |
| @staticmethod |
| def _implicit_credentials_from_gce(): |
| """Attempts to get implicit credentials in Google Compute Engine env. |
| |
| If the current environment is not detected as Compute Engine, returns |
| None, indicating no Google Compute Engine credentials can be detected |
| from the current environment. |
| |
| Returns: |
| None, if not in GCE, else a gce.AppAssertionCredentials object. |
| """ |
| if not _in_gce_environment(): |
| return None |
| |
| return _get_application_default_credential_GCE() |
| |
| @staticmethod |
| def _implicit_credentials_from_files(): |
| """Attempts to get implicit credentials from local credential files. |
| |
| First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS |
| is set with a filename and then falls back to a configuration file (the |
| "well known" file) associated with the 'gcloud' command line tool. |
| |
| Returns: |
| Credentials object associated with the |
| GOOGLE_APPLICATION_CREDENTIALS file or the "well known" file if |
| either exist. If neither file is define, returns None, indicating |
| no credentials from a file can detected from the current |
| environment. |
| """ |
| credentials_filename = _get_environment_variable_file() |
| if not credentials_filename: |
| credentials_filename = _get_well_known_file() |
| if os.path.isfile(credentials_filename): |
| extra_help = (' (produced automatically when running' |
| ' "gcloud auth login" command)') |
| else: |
| credentials_filename = None |
| else: |
| extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS + |
| ' environment variable)') |
| |
| if not credentials_filename: |
| return |
| |
| # If we can read the credentials from a file, we don't need to know |
| # what environment we are in. |
| SETTINGS.env_name = DEFAULT_ENV_NAME |
| |
| try: |
| return _get_application_default_credential_from_file( |
| credentials_filename) |
| except (ApplicationDefaultCredentialsError, ValueError) as error: |
| _raise_exception_for_reading_json(credentials_filename, |
| extra_help, error) |
| |
| @classmethod |
| def _get_implicit_credentials(cls): |
| """Gets credentials implicitly from the environment. |
| |
| Checks environment in order of precedence: |
| - Google App Engine (production and testing) |
| - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to |
| a file with stored credentials information. |
| - Stored "well known" file associated with `gcloud` command line tool. |
| - Google Compute Engine production environment. |
| |
| Raises: |
| ApplicationDefaultCredentialsError: raised when the credentials |
| fail to be retrieved. |
| """ |
| # Environ checks (in order). |
| environ_checkers = [ |
| cls._implicit_credentials_from_gae, |
| cls._implicit_credentials_from_files, |
| cls._implicit_credentials_from_gce, |
| ] |
| |
| for checker in environ_checkers: |
| credentials = checker() |
| if credentials is not None: |
| return credentials |
| |
| # If no credentials, fail. |
| raise ApplicationDefaultCredentialsError(ADC_HELP_MSG) |
| |
| @staticmethod |
| def get_application_default(): |
| """Get the Application Default Credentials for the current environment. |
| |
| Raises: |
| ApplicationDefaultCredentialsError: raised when the credentials |
| fail to be retrieved. |
| """ |
| return GoogleCredentials._get_implicit_credentials() |
| |
| @staticmethod |
| def from_stream(credential_filename): |
| """Create a Credentials object by reading information from a file. |
| |
| It returns an object of type GoogleCredentials. |
| |
| Args: |
| credential_filename: the path to the file from where the |
| credentials are to be read |
| |
| Raises: |
| ApplicationDefaultCredentialsError: raised when the credentials |
| fail to be retrieved. |
| """ |
| if credential_filename and os.path.isfile(credential_filename): |
| try: |
| return _get_application_default_credential_from_file( |
| credential_filename) |
| except (ApplicationDefaultCredentialsError, ValueError) as error: |
| extra_help = (' (provided as parameter to the ' |
| 'from_stream() method)') |
| _raise_exception_for_reading_json(credential_filename, |
| extra_help, |
| error) |
| else: |
| raise ApplicationDefaultCredentialsError( |
| 'The parameter passed to the from_stream() ' |
| 'method should point to a file.') |
| |
| |
| def _save_private_file(filename, json_contents): |
| """Saves a file with read-write permissions on for the owner. |
| |
| Args: |
| filename: String. Absolute path to file. |
| json_contents: JSON serializable object to be saved. |
| """ |
| temp_filename = tempfile.mktemp() |
| file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600) |
| with os.fdopen(file_desc, 'w') as file_handle: |
| json.dump(json_contents, file_handle, sort_keys=True, |
| indent=2, separators=(',', ': ')) |
| shutil.move(temp_filename, filename) |
| |
| |
| def save_to_well_known_file(credentials, well_known_file=None): |
| """Save the provided GoogleCredentials to the well known file. |
| |
| Args: |
| credentials: the credentials to be saved to the well known file; |
| it should be an instance of GoogleCredentials |
| well_known_file: the name of the file where the credentials are to be |
| saved; this parameter is supposed to be used for |
| testing only |
| """ |
| # TODO(orestica): move this method to tools.py |
| # once the argparse import gets fixed (it is not present in Python 2.6) |
| |
| if well_known_file is None: |
| well_known_file = _get_well_known_file() |
| |
| config_dir = os.path.dirname(well_known_file) |
| if not os.path.isdir(config_dir): |
| raise OSError('Config directory does not exist: %s' % config_dir) |
| |
| credentials_data = credentials.serialization_data |
| _save_private_file(well_known_file, credentials_data) |
| |
| |
| def _get_environment_variable_file(): |
| application_default_credential_filename = ( |
| os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, |
| None)) |
| |
| if application_default_credential_filename: |
| if os.path.isfile(application_default_credential_filename): |
| return application_default_credential_filename |
| else: |
| raise ApplicationDefaultCredentialsError( |
| 'File ' + application_default_credential_filename + |
| ' (pointed by ' + |
| GOOGLE_APPLICATION_CREDENTIALS + |
| ' environment variable) does not exist!') |
| |
| |
| def _get_well_known_file(): |
| """Get the well known file produced by command 'gcloud auth login'.""" |
| # TODO(orestica): Revisit this method once gcloud provides a better way |
| # of pinpointing the exact location of the file. |
| default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR) |
| if default_config_dir is None: |
| if os.name == 'nt': |
| try: |
| default_config_dir = os.path.join(os.environ['APPDATA'], |
| _CLOUDSDK_CONFIG_DIRECTORY) |
| except KeyError: |
| # This should never happen unless someone is really |
| # messing with things. |
| drive = os.environ.get('SystemDrive', 'C:') |
| default_config_dir = os.path.join(drive, '\\', |
| _CLOUDSDK_CONFIG_DIRECTORY) |
| else: |
| default_config_dir = os.path.join(os.path.expanduser('~'), |
| '.config', |
| _CLOUDSDK_CONFIG_DIRECTORY) |
| |
| return os.path.join(default_config_dir, _WELL_KNOWN_CREDENTIALS_FILE) |
| |
| |
| def _get_application_default_credential_from_file(filename): |
| """Build the Application Default Credentials from file.""" |
| # read the credentials from the file |
| with open(filename) as file_obj: |
| client_credentials = json.load(file_obj) |
| |
| credentials_type = client_credentials.get('type') |
| if credentials_type == AUTHORIZED_USER: |
| required_fields = set(['client_id', 'client_secret', 'refresh_token']) |
| elif credentials_type == SERVICE_ACCOUNT: |
| required_fields = set(['client_id', 'client_email', 'private_key_id', |
| 'private_key']) |
| else: |
| raise ApplicationDefaultCredentialsError( |
| "'type' field should be defined (and have one of the '" + |
| AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)") |
| |
| missing_fields = required_fields.difference(client_credentials.keys()) |
| |
| if missing_fields: |
| _raise_exception_for_missing_fields(missing_fields) |
| |
| if client_credentials['type'] == AUTHORIZED_USER: |
| return GoogleCredentials( |
| access_token=None, |
| client_id=client_credentials['client_id'], |
| client_secret=client_credentials['client_secret'], |
| refresh_token=client_credentials['refresh_token'], |
| token_expiry=None, |
| token_uri=GOOGLE_TOKEN_URI, |
| user_agent='Python client library') |
| else: # client_credentials['type'] == SERVICE_ACCOUNT |
| from oauth2client.service_account import ServiceAccountCredentials |
| return ServiceAccountCredentials.from_json_keyfile_dict( |
| client_credentials) |
| |
| |
| def _raise_exception_for_missing_fields(missing_fields): |
| raise ApplicationDefaultCredentialsError( |
| 'The following field(s) must be defined: ' + ', '.join(missing_fields)) |
| |
| |
| def _raise_exception_for_reading_json(credential_file, |
| extra_help, |
| error): |
| raise ApplicationDefaultCredentialsError( |
| 'An error was encountered while reading json file: ' + |
| credential_file + extra_help + ': ' + str(error)) |
| |
| |
| def _get_application_default_credential_GAE(): |
| from oauth2client.contrib.appengine import AppAssertionCredentials |
| |
| return AppAssertionCredentials([]) |
| |
| |
| def _get_application_default_credential_GCE(): |
| from oauth2client.contrib.gce import AppAssertionCredentials |
| |
| return AppAssertionCredentials([]) |
| |
| |
| class AssertionCredentials(GoogleCredentials): |
| """Abstract Credentials object used for OAuth 2.0 assertion grants. |
| |
| This credential does not require a flow to instantiate because it |
| represents a two legged flow, and therefore has all of the required |
| information to generate and refresh its own access tokens. It must |
| be subclassed to generate the appropriate assertion string. |
| |
| AssertionCredentials objects may be safely pickled and unpickled. |
| """ |
| |
| @util.positional(2) |
| def __init__(self, assertion_type, user_agent=None, |
| token_uri=GOOGLE_TOKEN_URI, |
| revoke_uri=GOOGLE_REVOKE_URI, |
| **unused_kwargs): |
| """Constructor for AssertionFlowCredentials. |
| |
| Args: |
| assertion_type: string, assertion type that will be declared to the |
| auth server |
| user_agent: string, The HTTP User-Agent to provide for this |
| application. |
| token_uri: string, URI for token endpoint. For convenience defaults |
| to Google's endpoints but any OAuth 2.0 provider can be |
| used. |
| revoke_uri: string, URI for revoke endpoint. |
| """ |
| super(AssertionCredentials, self).__init__( |
| None, |
| None, |
| None, |
| None, |
| None, |
| token_uri, |
| user_agent, |
| revoke_uri=revoke_uri) |
| self.assertion_type = assertion_type |
| |
| def _generate_refresh_request_body(self): |
| assertion = self._generate_assertion() |
| |
| body = urllib.parse.urlencode({ |
| 'assertion': assertion, |
| 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', |
| }) |
| |
| return body |
| |
| def _generate_assertion(self): |
| """Generate assertion string to be used in the access token request.""" |
| _abstract() |
| |
| def _revoke(self, http_request): |
| """Revokes the access_token and deletes the store if available. |
| |
| Args: |
| http_request: callable, a callable that matches the method |
| signature of httplib2.Http.request, used to make the |
| revoke request. |
| """ |
| self._do_revoke(http_request, self.access_token) |
| |
| |
| def _RequireCryptoOrDie(): |
| """Ensure we have a crypto library, or throw CryptoUnavailableError. |
| |
| The oauth2client.crypt module requires either PyCrypto or PyOpenSSL |
| to be available in order to function, but these are optional |
| dependencies. |
| """ |
| if not HAS_CRYPTO: |
| raise CryptoUnavailableError('No crypto library available') |
| |
| |
| # Only used in verify_id_token(), which is always calling to the same URI |
| # for the certs. |
| _cached_http = httplib2.Http(MemoryCache()) |
| |
| |
| @util.positional(2) |
| def verify_id_token(id_token, audience, http=None, |
| cert_uri=ID_TOKEN_VERIFICATION_CERTS): |
| """Verifies a signed JWT id_token. |
| |
| This function requires PyOpenSSL and because of that it does not work on |
| App Engine. |
| |
| Args: |
| id_token: string, A Signed JWT. |
| audience: string, The audience 'aud' that the token should be for. |
| http: httplib2.Http, instance to use to make the HTTP request. Callers |
| should supply an instance that has caching enabled. |
| cert_uri: string, URI of the certificates in JSON format to |
| verify the JWT against. |
| |
| Returns: |
| The deserialized JSON in the JWT. |
| |
| Raises: |
| oauth2client.crypt.AppIdentityError: if the JWT fails to verify. |
| CryptoUnavailableError: if no crypto library is available. |
| """ |
| _RequireCryptoOrDie() |
| if http is None: |
| http = _cached_http |
| |
| resp, content = http.request(cert_uri) |
| if resp.status == http_client.OK: |
| certs = json.loads(_from_bytes(content)) |
| return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) |
| else: |
| raise VerifyJwtTokenError('Status code: %d' % resp.status) |
| |
| |
| def _extract_id_token(id_token): |
| """Extract the JSON payload from a JWT. |
| |
| Does the extraction w/o checking the signature. |
| |
| Args: |
| id_token: string or bytestring, OAuth 2.0 id_token. |
| |
| Returns: |
| object, The deserialized JSON payload. |
| """ |
| if type(id_token) == bytes: |
| segments = id_token.split(b'.') |
| else: |
| segments = id_token.split(u'.') |
| |
| if len(segments) != 3: |
| raise VerifyJwtTokenError( |
| 'Wrong number of segments in token: %s' % id_token) |
| |
| return json.loads(_from_bytes(_urlsafe_b64decode(segments[1]))) |
| |
| |
| def _parse_exchange_token_response(content): |
| """Parses response of an exchange token request. |
| |
| Most providers return JSON but some (e.g. Facebook) return a |
| url-encoded string. |
| |
| Args: |
| content: The body of a response |
| |
| Returns: |
| Content as a dictionary object. Note that the dict could be empty, |
| i.e. {}. That basically indicates a failure. |
| """ |
| resp = {} |
| content = _from_bytes(content) |
| try: |
| resp = json.loads(content) |
| except Exception: |
| # different JSON libs raise different exceptions, |
| # so we just do a catch-all here |
| resp = dict(urllib.parse.parse_qsl(content)) |
| |
| # some providers respond with 'expires', others with 'expires_in' |
| if resp and 'expires' in resp: |
| resp['expires_in'] = resp.pop('expires') |
| |
| return resp |
| |
| |
| @util.positional(4) |
| def credentials_from_code(client_id, client_secret, scope, code, |
| redirect_uri='postmessage', http=None, |
| user_agent=None, token_uri=GOOGLE_TOKEN_URI, |
| auth_uri=GOOGLE_AUTH_URI, |
| revoke_uri=GOOGLE_REVOKE_URI, |
| device_uri=GOOGLE_DEVICE_URI, |
| token_info_uri=GOOGLE_TOKEN_INFO_URI): |
| """Exchanges an authorization code for an OAuth2Credentials object. |
| |
| Args: |
| client_id: string, client identifier. |
| client_secret: string, client secret. |
| scope: string or iterable of strings, scope(s) to request. |
| code: string, An authorization code, most likely passed down from |
| the client |
| redirect_uri: string, this is generally set to 'postmessage' to match |
| the redirect_uri that the client specified |
| http: httplib2.Http, optional http instance to use to do the fetch |
| token_uri: string, URI for token endpoint. For convenience defaults |
| to Google's endpoints but any OAuth 2.0 provider can be |
| used. |
| auth_uri: string, URI for authorization endpoint. For convenience |
| defaults to Google's endpoints but any OAuth 2.0 provider |
| can be used. |
| revoke_uri: string, URI for revoke endpoint. For convenience |
| defaults to Google's endpoints but any OAuth 2.0 provider |
| can be used. |
| device_uri: string, URI for device authorization endpoint. For |
| convenience defaults to Google's endpoints but any OAuth |
| 2.0 provider can be used. |
| |
| Returns: |
| An OAuth2Credentials object. |
| |
| Raises: |
| FlowExchangeError if the authorization code cannot be exchanged for an |
| access token |
| """ |
| flow = OAuth2WebServerFlow(client_id, client_secret, scope, |
| redirect_uri=redirect_uri, |
| user_agent=user_agent, auth_uri=auth_uri, |
| token_uri=token_uri, revoke_uri=revoke_uri, |
| device_uri=device_uri, |
| token_info_uri=token_info_uri) |
| |
| credentials = flow.step2_exchange(code, http=http) |
| return credentials |
| |
| |
| @util.positional(3) |
| def credentials_from_clientsecrets_and_code(filename, scope, code, |
| message=None, |
| redirect_uri='postmessage', |
| http=None, |
| cache=None, |
| device_uri=None): |
| """Returns OAuth2Credentials from a clientsecrets file and an auth code. |
| |
| Will create the right kind of Flow based on the contents of the |
| clientsecrets file or will raise InvalidClientSecretsError for unknown |
| types of Flows. |
| |
| Args: |
| filename: string, File name of clientsecrets. |
| scope: string or iterable of strings, scope(s) to request. |
| code: string, An authorization code, most likely passed down from |
| the client |
| message: string, A friendly string to display to the user if the |
| clientsecrets file is missing or invalid. If message is |
| provided then sys.exit will be called in the case of an error. |
| If message in not provided then |
| clientsecrets.InvalidClientSecretsError will be raised. |
| redirect_uri: string, this is generally set to 'postmessage' to match |
| the redirect_uri that the client specified |
| http: httplib2.Http, optional http instance to use to do the fetch |
| cache: An optional cache service client that implements get() and set() |
| methods. See clientsecrets.loadfile() for details. |
| device_uri: string, OAuth 2.0 device authorization endpoint |
| |
| Returns: |
| An OAuth2Credentials object. |
| |
| Raises: |
| FlowExchangeError: if the authorization code cannot be exchanged for an |
| access token |
| UnknownClientSecretsFlowError: if the file describes an unknown kind |
| of Flow. |
| clientsecrets.InvalidClientSecretsError: if the clientsecrets file is |
| invalid. |
| """ |
| flow = flow_from_clientsecrets(filename, scope, message=message, |
| cache=cache, redirect_uri=redirect_uri, |
| device_uri=device_uri) |
| credentials = flow.step2_exchange(code, http=http) |
| return credentials |
| |
| |
| class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', ( |
| 'device_code', 'user_code', 'interval', 'verification_url', |
| 'user_code_expiry'))): |
| """Intermediate information the OAuth2 for devices flow.""" |
| |
| @classmethod |
| def FromResponse(cls, response): |
| """Create a DeviceFlowInfo from a server response. |
| |
| The response should be a dict containing entries as described here: |
| |
| http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1 |
| """ |
| # device_code, user_code, and verification_url are required. |
| kwargs = { |
| 'device_code': response['device_code'], |
| 'user_code': response['user_code'], |
| } |
| # The response may list the verification address as either |
| # verification_url or verification_uri, so we check for both. |
| verification_url = response.get( |
| 'verification_url', response.get('verification_uri')) |
| if verification_url is None: |
| raise OAuth2DeviceCodeError( |
| 'No verification_url provided in server response') |
| kwargs['verification_url'] = verification_url |
| # expires_in and interval are optional. |
| kwargs.update({ |
| 'interval': response.get('interval'), |
| 'user_code_expiry': None, |
| }) |
| if 'expires_in' in response: |
| kwargs['user_code_expiry'] = ( |
| datetime.datetime.now() + |
| datetime.timedelta(seconds=int(response['expires_in']))) |
| return cls(**kwargs) |
| |
| |
| class OAuth2WebServerFlow(Flow): |
| """Does the Web Server Flow for OAuth 2.0. |
| |
| OAuth2WebServerFlow objects may be safely pickled and unpickled. |
| """ |
| |
| @util.positional(4) |
| def __init__(self, client_id, |
| client_secret=None, |
| scope=None, |
| redirect_uri=None, |
| user_agent=None, |
| auth_uri=GOOGLE_AUTH_URI, |
| token_uri=GOOGLE_TOKEN_URI, |
| revoke_uri=GOOGLE_REVOKE_URI, |
| login_hint=None, |
| device_uri=GOOGLE_DEVICE_URI, |
| token_info_uri=GOOGLE_TOKEN_INFO_URI, |
| authorization_header=None, |
| **kwargs): |
| """Constructor for OAuth2WebServerFlow. |
| |
| The kwargs argument is used to set extra query parameters on the |
| auth_uri. For example, the access_type and approval_prompt |
| query parameters can be set via kwargs. |
| |
| Args: |
| client_id: string, client identifier. |
| client_secret: string client secret. |
| scope: string or iterable of strings, scope(s) of the credentials |
| being requested. |
| redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' |
| for a non-web-based application, or a URI that |
| handles the callback from the authorization server. |
| user_agent: string, HTTP User-Agent to provide for this |
| application. |
| auth_uri: string, URI for authorization endpoint. For convenience |
| defaults to Google's endpoints but any OAuth 2.0 provider |
| can be used. |
| token_uri: string, URI for token endpoint. For convenience |
| defaults to Google's endpoints but any OAuth 2.0 |
| provider can be used. |
| revoke_uri: string, URI for revoke endpoint. For convenience |
| defaults to Google's endpoints but any OAuth 2.0 |
| provider can be used. |
| login_hint: string, Either an email address or domain. Passing this |
| hint will either pre-fill the email box on the sign-in |
| form or select the proper multi-login session, thereby |
| simplifying the login flow. |
| device_uri: string, URI for device authorization endpoint. For |
| convenience defaults to Google's endpoints but any |
| OAuth 2.0 provider can be used. |
| authorization_header: string, For use with OAuth 2.0 providers that |
| require a client to authenticate using a |
| header value instead of passing client_secret |
| in the POST body. |
| **kwargs: dict, The keyword arguments are all optional and required |
| parameters for the OAuth calls. |
| """ |
| # scope is a required argument, but to preserve backwards-compatibility |
| # we don't want to rearrange the positional arguments |
| if scope is None: |
| raise TypeError("The value of scope must not be None") |
| self.client_id = client_id |
| self.client_secret = client_secret |
| self.scope = util.scopes_to_string(scope) |
| self.redirect_uri = redirect_uri |
| self.login_hint = login_hint |
| self.user_agent = user_agent |
| self.auth_uri = auth_uri |
| self.token_uri = token_uri |
| self.revoke_uri = revoke_uri |
| self.device_uri = device_uri |
| self.token_info_uri = token_info_uri |
| self.authorization_header = authorization_header |
| self.params = { |
| 'access_type': 'offline', |
| 'response_type': 'code', |
| } |
| self.params.update(kwargs) |
| |
| @util.positional(1) |
| def step1_get_authorize_url(self, redirect_uri=None, state=None): |
| """Returns a URI to redirect to the provider. |
| |
| Args: |
| redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' |
| for a non-web-based application, or a URI that |
| handles the callback from the authorization server. |
| This parameter is deprecated, please move to passing |
| the redirect_uri in via the constructor. |
| state: string, Opaque state string which is passed through the |
| OAuth2 flow and returned to the client as a query parameter |
| in the callback. |
| |
| Returns: |
| A URI as a string to redirect the user to begin the authorization |
| flow. |
| """ |
| if redirect_uri is not None: |
| logger.warning(( |
| 'The redirect_uri parameter for ' |
| 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. ' |
| 'Please move to passing the redirect_uri in via the ' |
| 'constructor.')) |
| self.redirect_uri = redirect_uri |
| |
| if self.redirect_uri is None: |
| raise ValueError('The value of redirect_uri must not be None.') |
| |
| query_params = { |
| 'client_id': self.client_id, |
| 'redirect_uri': self.redirect_uri, |
| 'scope': self.scope, |
| } |
| if state is not None: |
| query_params['state'] = state |
| if self.login_hint is not None: |
| query_params['login_hint'] = self.login_hint |
| query_params.update(self.params) |
| return _update_query_params(self.auth_uri, query_params) |
| |
| @util.positional(1) |
| def step1_get_device_and_user_codes(self, http=None): |
| """Returns a user code and the verification URL where to enter it |
| |
| Returns: |
| A user code as a string for the user to authorize the application |
| An URL as a string where the user has to enter the code |
| """ |
| if self.device_uri is None: |
| raise ValueError('The value of device_uri must not be None.') |
| |
| body = urllib.parse.urlencode({ |
| 'client_id': self.client_id, |
| 'scope': self.scope, |
| }) |
| headers = { |
| 'content-type': 'application/x-www-form-urlencoded', |
| } |
| |
| if self.user_agent is not None: |
| headers['user-agent'] = self.user_agent |
| |
| if http is None: |
| http = httplib2.Http() |
| |
| resp, content = http.request(self.device_uri, method='POST', body=body, |
| headers=headers) |
| content = _from_bytes(content) |
| if resp.status == http_client.OK: |
| try: |
| flow_info = json.loads(content) |
| except ValueError as e: |
| raise OAuth2DeviceCodeError( |
| 'Could not parse server response as JSON: "%s", ' |
| 'error: "%s"' % (content, e)) |
| return DeviceFlowInfo.FromResponse(flow_info) |
| else: |
| error_msg = 'Invalid response %s.' % resp.status |
| try: |
| d = json.loads(content) |
| if 'error' in d: |
| error_msg += ' Error: %s' % d['error'] |
| except ValueError: |
| # Couldn't decode a JSON response, stick with the |
| # default message. |
| pass |
| raise OAuth2DeviceCodeError(error_msg) |
| |
| @util.positional(2) |
| def step2_exchange(self, code=None, http=None, device_flow_info=None): |
| """Exchanges a code for OAuth2Credentials. |
| |
| Args: |
| code: string, a dict-like object, or None. For a non-device |
| flow, this is either the response code as a string, or a |
| dictionary of query parameters to the redirect_uri. For a |
| device flow, this should be None. |
| http: httplib2.Http, optional http instance to use when fetching |
| credentials. |
| device_flow_info: DeviceFlowInfo, return value from step1 in the |
| case of a device flow. |
| |
| Returns: |
| An OAuth2Credentials object that can be used to authorize requests. |
| |
| Raises: |
| FlowExchangeError: if a problem occurred exchanging the code for a |
| refresh_token. |
| ValueError: if code and device_flow_info are both provided or both |
| missing. |
| """ |
| if code is None and device_flow_info is None: |
| raise ValueError('No code or device_flow_info provided.') |
| if code is not None and device_flow_info is not None: |
| raise ValueError('Cannot provide both code and device_flow_info.') |
| |
| if code is None: |
| code = device_flow_info.device_code |
| elif not isinstance(code, six.string_types): |
| if 'code' not in code: |
| raise FlowExchangeError(code.get( |
| 'error', 'No code was supplied in the query parameters.')) |
| code = code['code'] |
| |
| post_data = { |
| 'client_id': self.client_id, |
| 'code': code, |
| 'scope': self.scope, |
| } |
| if self.client_secret is not None: |
| post_data['client_secret'] = self.client_secret |
| if device_flow_info is not None: |
| post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' |
| else: |
| post_data['grant_type'] = 'authorization_code' |
| post_data['redirect_uri'] = self.redirect_uri |
| body = urllib.parse.urlencode(post_data) |
| headers = { |
| 'content-type': 'application/x-www-form-urlencoded', |
| } |
| if self.authorization_header is not None: |
| headers['Authorization'] = self.authorization_header |
| if self.user_agent is not None: |
| headers['user-agent'] = self.user_agent |
| |
| if http is None: |
| http = httplib2.Http() |
| |
| resp, content = http.request(self.token_uri, method='POST', body=body, |
| headers=headers) |
| d = _parse_exchange_token_response(content) |
| if resp.status == http_client.OK and 'access_token' in d: |
| access_token = d['access_token'] |
| refresh_token = d.get('refresh_token', None) |
| if not refresh_token: |
| logger.info( |
| 'Received token response with no refresh_token. Consider ' |
| "reauthenticating with approval_prompt='force'.") |
| token_expiry = None |
| if 'expires_in' in d: |
| delta = datetime.timedelta(seconds=int(d['expires_in'])) |
| token_expiry = delta + _UTCNOW() |
| |
| extracted_id_token = None |
| if 'id_token' in d: |
| extracted_id_token = _extract_id_token(d['id_token']) |
| |
| logger.info('Successfully retrieved access token') |
| return OAuth2Credentials( |
| access_token, self.client_id, self.client_secret, |
| refresh_token, token_expiry, self.token_uri, self.user_agent, |
| revoke_uri=self.revoke_uri, id_token=extracted_id_token, |
| token_response=d, scopes=self.scope, |
| token_info_uri=self.token_info_uri) |
| else: |
| logger.info('Failed to retrieve access token: %s', content) |
| if 'error' in d: |
| # you never know what those providers got to say |
| error_msg = (str(d['error']) + |
| str(d.get('error_description', ''))) |
| else: |
| error_msg = 'Invalid response: %s.' % str(resp.status) |
| raise FlowExchangeError(error_msg) |
| |
| |
| @util.positional(2) |
| def flow_from_clientsecrets(filename, scope, redirect_uri=None, |
| message=None, cache=None, login_hint=None, |
| device_uri=None): |
| """Create a Flow from a clientsecrets file. |
| |
| Will create the right kind of Flow based on the contents of the |
| clientsecrets file or will raise InvalidClientSecretsError for unknown |
| types of Flows. |
| |
| Args: |
| filename: string, File name of client secrets. |
| scope: string or iterable of strings, scope(s) to request. |
| redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for |
| a non-web-based application, or a URI that handles the |
| callback from the authorization server. |
| message: string, A friendly string to display to the user if the |
| clientsecrets file is missing or invalid. If message is |
| provided then sys.exit will be called in the case of an error. |
| If message in not provided then |
| clientsecrets.InvalidClientSecretsError will be raised. |
| cache: An optional cache service client that implements get() and set() |
| methods. See clientsecrets.loadfile() for details. |
| login_hint: string, Either an email address or domain. Passing this |
| hint will either pre-fill the email box on the sign-in form |
| or select the proper multi-login session, thereby |
| simplifying the login flow. |
| device_uri: string, URI for device authorization endpoint. For |
| convenience defaults to Google's endpoints but any |
| OAuth 2.0 provider can be used. |
| |
| Returns: |
| A Flow object. |
| |
| Raises: |
| UnknownClientSecretsFlowError: if the file describes an unknown kind of |
| Flow. |
| clientsecrets.InvalidClientSecretsError: if the clientsecrets file is |
| invalid. |
| """ |
| try: |
| client_type, client_info = clientsecrets.loadfile(filename, |
| cache=cache) |
| if client_type in (clientsecrets.TYPE_WEB, |
| clientsecrets.TYPE_INSTALLED): |
| constructor_kwargs = { |
| 'redirect_uri': redirect_uri, |
| 'auth_uri': client_info['auth_uri'], |
| 'token_uri': client_info['token_uri'], |
| 'login_hint': login_hint, |
| } |
| revoke_uri = client_info.get('revoke_uri') |
| if revoke_uri is not None: |
| constructor_kwargs['revoke_uri'] = revoke_uri |
| if device_uri is not None: |
| constructor_kwargs['device_uri'] = device_uri |
| return OAuth2WebServerFlow( |
| client_info['client_id'], client_info['client_secret'], |
| scope, **constructor_kwargs) |
| |
| except clientsecrets.InvalidClientSecretsError: |
| if message: |
| sys.exit(message) |
| else: |
| raise |
| else: |
| raise UnknownClientSecretsFlowError( |
| 'This OAuth 2.0 flow is unsupported: %r' % client_type) |