1# Copyright 2016 Google LLC 
    2# 
    3# Licensed under the Apache License, Version 2.0 (the "License"); 
    4# you may not use this file except in compliance with the License. 
    5# You may obtain a copy of the License at 
    6# 
    7#      http://www.apache.org/licenses/LICENSE-2.0 
    8# 
    9# Unless required by applicable law or agreed to in writing, software 
    10# distributed under the License is distributed on an "AS IS" BASIS, 
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
    12# See the License for the specific language governing permissions and 
    13# limitations under the License. 
    14 
    15"""OAuth 2.0 Credentials. 
    16 
    17This module provides credentials based on OAuth 2.0 access and refresh tokens. 
    18These credentials usually access resources on behalf of a user (resource 
    19owner). 
    20 
    21Specifically, this is intended to use access tokens acquired using the 
    22`Authorization Code grant`_ and can refresh those tokens using a 
    23optional `refresh token`_. 
    24 
    25Obtaining the initial access and refresh token is outside of the scope of this 
    26module. Consult `rfc6749 section 4.1`_ for complete details on the 
    27Authorization Code grant flow. 
    28 
    29.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1 
    30.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6 
    31.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1 
    32""" 
    33 
    34from datetime import datetime 
    35import io 
    36import json 
    37import logging 
    38import warnings 
    39 
    40from google.auth import _cloud_sdk 
    41from google.auth import _helpers 
    42from google.auth import credentials 
    43from google.auth import exceptions 
    44from google.auth import metrics 
    45from google.oauth2 import reauth 
    46 
    47_LOGGER = logging.getLogger(__name__) 
    48 
    49 
    50# The Google OAuth 2.0 token endpoint. Used for authorized user credentials. 
    51_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" 
    52 
    53# The Google OAuth 2.0 token info endpoint. Used for getting token info JSON from access tokens. 
    54_GOOGLE_OAUTH2_TOKEN_INFO_ENDPOINT = "https://oauth2.googleapis.com/tokeninfo" 
    55 
    56 
    57class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject): 
    58    """Credentials using OAuth 2.0 access and refresh tokens. 
    59 
    60    The credentials are considered immutable except the tokens and the token 
    61    expiry, which are updated after refresh. If you want to modify the quota 
    62    project, use :meth:`with_quota_project` or :: 
    63 
    64        credentials = credentials.with_quota_project('myproject-123') 
    65 
    66    Reauth is disabled by default. To enable reauth, set the 
    67    `enable_reauth_refresh` parameter to True in the constructor. Note that 
    68    reauth feature is intended for gcloud to use only. 
    69    If reauth is enabled, `pyu2f` dependency has to be installed in order to use security 
    70    key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install 
    71    google-auth[reauth]`. 
    72    """ 
    73 
    74    def __init__( 
    75        self, 
    76        token, 
    77        refresh_token=None, 
    78        id_token=None, 
    79        token_uri=None, 
    80        client_id=None, 
    81        client_secret=None, 
    82        scopes=None, 
    83        default_scopes=None, 
    84        quota_project_id=None, 
    85        expiry=None, 
    86        rapt_token=None, 
    87        refresh_handler=None, 
    88        enable_reauth_refresh=False, 
    89        granted_scopes=None, 
    90        trust_boundary=None, 
    91        universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN, 
    92        account=None, 
    93    ): 
    94        """ 
    95        Args: 
    96            token (Optional(str)): The OAuth 2.0 access token. Can be None 
    97                if refresh information is provided. 
    98            refresh_token (str): The OAuth 2.0 refresh token. If specified, 
    99                credentials can be refreshed. 
    100            id_token (str): The Open ID Connect ID Token. 
    101            token_uri (str): The OAuth 2.0 authorization server's token 
    102                endpoint URI. Must be specified for refresh, can be left as 
    103                None if the token can not be refreshed. 
    104            client_id (str): The OAuth 2.0 client ID. Must be specified for 
    105                refresh, can be left as None if the token can not be refreshed. 
    106            client_secret(str): The OAuth 2.0 client secret. Must be specified 
    107                for refresh, can be left as None if the token can not be 
    108                refreshed. 
    109            scopes (Sequence[str]): The scopes used to obtain authorization. 
    110                This parameter is used by :meth:`has_scopes`. OAuth 2.0 
    111                credentials can not request additional scopes after 
    112                authorization. The scopes must be derivable from the refresh 
    113                token if refresh information is provided (e.g. The refresh 
    114                token scopes are a superset of this or contain a wild card 
    115                scope like 'https://www.googleapis.com/auth/any-api'). 
    116            default_scopes (Sequence[str]): Default scopes passed by a 
    117                Google client library. Use 'scopes' for user-defined scopes. 
    118            quota_project_id (Optional[str]): The project ID used for quota and billing. 
    119                This project may be different from the project used to 
    120                create the credentials. 
    121            rapt_token (Optional[str]): The reauth Proof Token. 
    122            refresh_handler (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]): 
    123                A callable which takes in the HTTP request callable and the list of 
    124                OAuth scopes and when called returns an access token string for the 
    125                requested scopes and its expiry datetime. This is useful when no 
    126                refresh tokens are provided and tokens are obtained by calling 
    127                some external process on demand. It is particularly useful for 
    128                retrieving downscoped tokens from a token broker. 
    129            enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow 
    130                should be used. This flag is for gcloud to use only. 
    131            granted_scopes (Optional[Sequence[str]]): The scopes that were consented/granted by the user. 
    132                This could be different from the requested scopes and it could be empty if granted 
    133                and requested scopes were same. 
    134            trust_boundary (str): String representation of trust boundary meta. 
    135            universe_domain (Optional[str]): The universe domain. The default 
    136                universe domain is googleapis.com. 
    137            account (Optional[str]): The account associated with the credential. 
    138        """ 
    139        super(Credentials, self).__init__() 
    140        self.token = token 
    141        self.expiry = expiry 
    142        self._refresh_token = refresh_token 
    143        self._id_token = id_token 
    144        self._scopes = scopes 
    145        self._default_scopes = default_scopes 
    146        self._granted_scopes = granted_scopes 
    147        self._token_uri = token_uri 
    148        self._client_id = client_id 
    149        self._client_secret = client_secret 
    150        self._quota_project_id = quota_project_id 
    151        self._rapt_token = rapt_token 
    152        self.refresh_handler = refresh_handler 
    153        self._enable_reauth_refresh = enable_reauth_refresh 
    154        self._trust_boundary = trust_boundary 
    155        self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN 
    156        self._account = account or "" 
    157        self._cred_file_path = None 
    158 
    159    def __getstate__(self): 
    160        """A __getstate__ method must exist for the __setstate__ to be called 
    161        This is identical to the default implementation. 
    162        See https://docs.python.org/3.7/library/pickle.html#object.__setstate__ 
    163        """ 
    164        state_dict = self.__dict__.copy() 
    165        # Remove _refresh_handler function as there are limitations pickling and 
    166        # unpickling certain callables (lambda, functools.partial instances) 
    167        # because they need to be importable. 
    168        # Instead, the refresh_handler setter should be used to repopulate this. 
    169        if "_refresh_handler" in state_dict: 
    170            del state_dict["_refresh_handler"] 
    171 
    172        if "_refresh_worker" in state_dict: 
    173            del state_dict["_refresh_worker"] 
    174        return state_dict 
    175 
    176    def __setstate__(self, d): 
    177        """Credentials pickled with older versions of the class do not have 
    178        all the attributes.""" 
    179        self.token = d.get("token") 
    180        self.expiry = d.get("expiry") 
    181        self._refresh_token = d.get("_refresh_token") 
    182        self._id_token = d.get("_id_token") 
    183        self._scopes = d.get("_scopes") 
    184        self._default_scopes = d.get("_default_scopes") 
    185        self._granted_scopes = d.get("_granted_scopes") 
    186        self._token_uri = d.get("_token_uri") 
    187        self._client_id = d.get("_client_id") 
    188        self._client_secret = d.get("_client_secret") 
    189        self._quota_project_id = d.get("_quota_project_id") 
    190        self._rapt_token = d.get("_rapt_token") 
    191        self._enable_reauth_refresh = d.get("_enable_reauth_refresh") 
    192        self._trust_boundary = d.get("_trust_boundary") 
    193        self._universe_domain = ( 
    194            d.get("_universe_domain") or credentials.DEFAULT_UNIVERSE_DOMAIN 
    195        ) 
    196        self._cred_file_path = d.get("_cred_file_path") 
    197        # The refresh_handler setter should be used to repopulate this. 
    198        self._refresh_handler = None 
    199        self._refresh_worker = None 
    200        self._use_non_blocking_refresh = d.get("_use_non_blocking_refresh", False) 
    201        self._account = d.get("_account", "") 
    202 
    203    @property 
    204    def refresh_token(self): 
    205        """Optional[str]: The OAuth 2.0 refresh token.""" 
    206        return self._refresh_token 
    207 
    208    @property 
    209    def scopes(self): 
    210        """Optional[str]: The OAuth 2.0 permission scopes.""" 
    211        return self._scopes 
    212 
    213    @property 
    214    def granted_scopes(self): 
    215        """Optional[Sequence[str]]: The OAuth 2.0 permission scopes that were granted by the user.""" 
    216        return self._granted_scopes 
    217 
    218    @property 
    219    def token_uri(self): 
    220        """Optional[str]: The OAuth 2.0 authorization server's token endpoint 
    221        URI.""" 
    222        return self._token_uri 
    223 
    224    @property 
    225    def id_token(self): 
    226        """Optional[str]: The Open ID Connect ID Token. 
    227 
    228        Depending on the authorization server and the scopes requested, this 
    229        may be populated when credentials are obtained and updated when 
    230        :meth:`refresh` is called. This token is a JWT. It can be verified 
    231        and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`. 
    232        """ 
    233        return self._id_token 
    234 
    235    @property 
    236    def client_id(self): 
    237        """Optional[str]: The OAuth 2.0 client ID.""" 
    238        return self._client_id 
    239 
    240    @property 
    241    def client_secret(self): 
    242        """Optional[str]: The OAuth 2.0 client secret.""" 
    243        return self._client_secret 
    244 
    245    @property 
    246    def requires_scopes(self): 
    247        """False: OAuth 2.0 credentials have their scopes set when 
    248        the initial token is requested and can not be changed.""" 
    249        return False 
    250 
    251    @property 
    252    def rapt_token(self): 
    253        """Optional[str]: The reauth Proof Token.""" 
    254        return self._rapt_token 
    255 
    256    @property 
    257    def refresh_handler(self): 
    258        """Returns the refresh handler if available. 
    259 
    260        Returns: 
    261           Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]: 
    262               The current refresh handler. 
    263        """ 
    264        return self._refresh_handler 
    265 
    266    @refresh_handler.setter 
    267    def refresh_handler(self, value): 
    268        """Updates the current refresh handler. 
    269 
    270        Args: 
    271            value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]): 
    272                The updated value of the refresh handler. 
    273 
    274        Raises: 
    275            TypeError: If the value is not a callable or None. 
    276        """ 
    277        if not callable(value) and value is not None: 
    278            raise TypeError("The provided refresh_handler is not a callable or None.") 
    279        self._refresh_handler = value 
    280 
    281    @property 
    282    def account(self): 
    283        """str: The user account associated with the credential. If the account is unknown an empty string is returned.""" 
    284        return self._account 
    285 
    286    def _make_copy(self): 
    287        cred = self.__class__( 
    288            self.token, 
    289            refresh_token=self.refresh_token, 
    290            id_token=self.id_token, 
    291            token_uri=self.token_uri, 
    292            client_id=self.client_id, 
    293            client_secret=self.client_secret, 
    294            scopes=self.scopes, 
    295            default_scopes=self.default_scopes, 
    296            granted_scopes=self.granted_scopes, 
    297            quota_project_id=self.quota_project_id, 
    298            rapt_token=self.rapt_token, 
    299            enable_reauth_refresh=self._enable_reauth_refresh, 
    300            trust_boundary=self._trust_boundary, 
    301            universe_domain=self._universe_domain, 
    302            account=self._account, 
    303        ) 
    304        cred._cred_file_path = self._cred_file_path 
    305        return cred 
    306 
    307    @_helpers.copy_docstring(credentials.Credentials) 
    308    def get_cred_info(self): 
    309        if self._cred_file_path: 
    310            cred_info = { 
    311                "credential_source": self._cred_file_path, 
    312                "credential_type": "user credentials", 
    313            } 
    314            if self.account: 
    315                cred_info["principal"] = self.account 
    316            return cred_info 
    317        return None 
    318 
    319    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 
    320    def with_quota_project(self, quota_project_id): 
    321        cred = self._make_copy() 
    322        cred._quota_project_id = quota_project_id 
    323        return cred 
    324 
    325    @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) 
    326    def with_token_uri(self, token_uri): 
    327        cred = self._make_copy() 
    328        cred._token_uri = token_uri 
    329        return cred 
    330 
    331    def with_account(self, account): 
    332        """Returns a copy of these credentials with a modified account. 
    333 
    334        Args: 
    335            account (str): The account to set 
    336 
    337        Returns: 
    338            google.oauth2.credentials.Credentials: A new credentials instance. 
    339        """ 
    340        cred = self._make_copy() 
    341        cred._account = account 
    342        return cred 
    343 
    344    @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain) 
    345    def with_universe_domain(self, universe_domain): 
    346        cred = self._make_copy() 
    347        cred._universe_domain = universe_domain 
    348        return cred 
    349 
    350    def _metric_header_for_usage(self): 
    351        return metrics.CRED_TYPE_USER 
    352 
    353    @_helpers.copy_docstring(credentials.Credentials) 
    354    def refresh(self, request): 
    355        if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN: 
    356            raise exceptions.RefreshError( 
    357                "User credential refresh is only supported in the default " 
    358                "googleapis.com universe domain, but the current universe " 
    359                "domain is {}. If you created the credential with an access " 
    360                "token, it's likely that the provided token is expired now, " 
    361                "please update your code with a valid token.".format( 
    362                    self._universe_domain 
    363                ) 
    364            ) 
    365 
    366        scopes = self._scopes if self._scopes is not None else self._default_scopes 
    367        # Use refresh handler if available and no refresh token is 
    368        # available. This is useful in general when tokens are obtained by calling 
    369        # some external process on demand. It is particularly useful for retrieving 
    370        # downscoped tokens from a token broker. 
    371        if self._refresh_token is None and self.refresh_handler: 
    372            token, expiry = self.refresh_handler(request, scopes=scopes) 
    373            # Validate returned data. 
    374            if not isinstance(token, str): 
    375                raise exceptions.RefreshError( 
    376                    "The refresh_handler returned token is not a string." 
    377                ) 
    378            if not isinstance(expiry, datetime): 
    379                raise exceptions.RefreshError( 
    380                    "The refresh_handler returned expiry is not a datetime object." 
    381                ) 
    382            if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD: 
    383                raise exceptions.RefreshError( 
    384                    "The credentials returned by the refresh_handler are " 
    385                    "already expired." 
    386                ) 
    387            self.token = token 
    388            self.expiry = expiry 
    389            return 
    390 
    391        if ( 
    392            self._refresh_token is None 
    393            or self._token_uri is None 
    394            or self._client_id is None 
    395            or self._client_secret is None 
    396        ): 
    397            raise exceptions.RefreshError( 
    398                "The credentials do not contain the necessary fields need to " 
    399                "refresh the access token. You must specify refresh_token, " 
    400                "token_uri, client_id, and client_secret." 
    401            ) 
    402 
    403        ( 
    404            access_token, 
    405            refresh_token, 
    406            expiry, 
    407            grant_response, 
    408            rapt_token, 
    409        ) = reauth.refresh_grant( 
    410            request, 
    411            self._token_uri, 
    412            self._refresh_token, 
    413            self._client_id, 
    414            self._client_secret, 
    415            scopes=scopes, 
    416            rapt_token=self._rapt_token, 
    417            enable_reauth_refresh=self._enable_reauth_refresh, 
    418        ) 
    419 
    420        self.token = access_token 
    421        self.expiry = expiry 
    422        self._refresh_token = refresh_token 
    423        self._id_token = grant_response.get("id_token") 
    424        self._rapt_token = rapt_token 
    425 
    426        if scopes and "scope" in grant_response: 
    427            requested_scopes = frozenset(scopes) 
    428            self._granted_scopes = grant_response["scope"].split() 
    429            granted_scopes = frozenset(self._granted_scopes) 
    430            scopes_requested_but_not_granted = requested_scopes - granted_scopes 
    431            if scopes_requested_but_not_granted: 
    432                # User might be presented with unbundled scopes at the time of 
    433                # consent. So it is a valid scenario to not have all the requested 
    434                # scopes as part of granted scopes but log a warning in case the 
    435                # developer wants to debug the scenario. 
    436                _LOGGER.warning( 
    437                    "Not all requested scopes were granted by the " 
    438                    "authorization server, missing scopes {}.".format( 
    439                        ", ".join(scopes_requested_but_not_granted) 
    440                    ) 
    441                ) 
    442 
    443    @classmethod 
    444    def from_authorized_user_info(cls, info, scopes=None): 
    445        """Creates a Credentials instance from parsed authorized user info. 
    446 
    447        Args: 
    448            info (Mapping[str, str]): The authorized user info in Google 
    449                format. 
    450            scopes (Sequence[str]): Optional list of scopes to include in the 
    451                credentials. 
    452 
    453        Returns: 
    454            google.oauth2.credentials.Credentials: The constructed 
    455                credentials. 
    456 
    457        Raises: 
    458            ValueError: If the info is not in the expected format. 
    459        """ 
    460        keys_needed = set(("refresh_token", "client_id", "client_secret")) 
    461        missing = keys_needed.difference(info.keys()) 
    462 
    463        if missing: 
    464            raise ValueError( 
    465                "Authorized user info was not in the expected format, missing " 
    466                "fields {}.".format(", ".join(missing)) 
    467            ) 
    468 
    469        # access token expiry (datetime obj); auto-expire if not saved 
    470        expiry = info.get("expiry") 
    471        if expiry: 
    472            expiry = datetime.strptime( 
    473                expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S" 
    474            ) 
    475        else: 
    476            expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD 
    477 
    478        # process scopes, which needs to be a seq 
    479        if scopes is None and "scopes" in info: 
    480            scopes = info.get("scopes") 
    481            if isinstance(scopes, str): 
    482                scopes = scopes.split(" ") 
    483 
    484        return cls( 
    485            token=info.get("token"), 
    486            refresh_token=info.get("refresh_token"), 
    487            token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,  # always overrides 
    488            scopes=scopes, 
    489            client_id=info.get("client_id"), 
    490            client_secret=info.get("client_secret"), 
    491            quota_project_id=info.get("quota_project_id"),  # may not exist 
    492            expiry=expiry, 
    493            rapt_token=info.get("rapt_token"),  # may not exist 
    494            trust_boundary=info.get("trust_boundary"),  # may not exist 
    495            universe_domain=info.get("universe_domain"),  # may not exist 
    496            account=info.get("account", ""),  # may not exist 
    497        ) 
    498 
    499    @classmethod 
    500    def from_authorized_user_file(cls, filename, scopes=None): 
    501        """Creates a Credentials instance from an authorized user json file. 
    502 
    503        Args: 
    504            filename (str): The path to the authorized user json file. 
    505            scopes (Sequence[str]): Optional list of scopes to include in the 
    506                credentials. 
    507 
    508        Returns: 
    509            google.oauth2.credentials.Credentials: The constructed 
    510                credentials. 
    511 
    512        Raises: 
    513            ValueError: If the file is not in the expected format. 
    514        """ 
    515        with io.open(filename, "r", encoding="utf-8") as json_file: 
    516            data = json.load(json_file) 
    517            return cls.from_authorized_user_info(data, scopes) 
    518 
    519    def to_json(self, strip=None): 
    520        """Utility function that creates a JSON representation of a Credentials 
    521        object. 
    522 
    523        Args: 
    524            strip (Sequence[str]): Optional list of members to exclude from the 
    525                                   generated JSON. 
    526 
    527        Returns: 
    528            str: A JSON representation of this instance. When converted into 
    529            a dictionary, it can be passed to from_authorized_user_info() 
    530            to create a new credential instance. 
    531        """ 
    532        prep = { 
    533            "token": self.token, 
    534            "refresh_token": self.refresh_token, 
    535            "token_uri": self.token_uri, 
    536            "client_id": self.client_id, 
    537            "client_secret": self.client_secret, 
    538            "scopes": self.scopes, 
    539            "rapt_token": self.rapt_token, 
    540            "universe_domain": self._universe_domain, 
    541            "account": self._account, 
    542        } 
    543        if self.expiry:  # flatten expiry timestamp 
    544            prep["expiry"] = self.expiry.isoformat() + "Z" 
    545 
    546        # Remove empty entries (those which are None) 
    547        prep = {k: v for k, v in prep.items() if v is not None} 
    548 
    549        # Remove entries that explicitely need to be removed 
    550        if strip is not None: 
    551            prep = {k: v for k, v in prep.items() if k not in strip} 
    552 
    553        return json.dumps(prep) 
    554 
    555 
    556class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject): 
    557    """Access token credentials for user account. 
    558 
    559    Obtain the access token for a given user account or the current active 
    560    user account with the ``gcloud auth print-access-token`` command. 
    561 
    562    Args: 
    563        account (Optional[str]): Account to get the access token for. If not 
    564            specified, the current active account will be used. 
    565        quota_project_id (Optional[str]): The project ID used for quota 
    566            and billing. 
    567    """ 
    568 
    569    def __init__(self, account=None, quota_project_id=None): 
    570        warnings.warn( 
    571            "UserAccessTokenCredentials is deprecated, please use " 
    572            "google.oauth2.credentials.Credentials instead. To use " 
    573            "that credential type, simply run " 
    574            "`gcloud auth application-default login` and let the " 
    575            "client libraries pick up the application default credentials." 
    576        ) 
    577        super(UserAccessTokenCredentials, self).__init__() 
    578        self._account = account 
    579        self._quota_project_id = quota_project_id 
    580 
    581    def with_account(self, account): 
    582        """Create a new instance with the given account. 
    583 
    584        Args: 
    585            account (str): Account to get the access token for. 
    586 
    587        Returns: 
    588            google.oauth2.credentials.UserAccessTokenCredentials: The created 
    589                credentials with the given account. 
    590        """ 
    591        return self.__class__(account=account, quota_project_id=self._quota_project_id) 
    592 
    593    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 
    594    def with_quota_project(self, quota_project_id): 
    595        return self.__class__(account=self._account, quota_project_id=quota_project_id) 
    596 
    597    def refresh(self, request): 
    598        """Refreshes the access token. 
    599 
    600        Args: 
    601            request (google.auth.transport.Request): This argument is required 
    602                by the base class interface but not used in this implementation, 
    603                so just set it to `None`. 
    604 
    605        Raises: 
    606            google.auth.exceptions.UserAccessTokenError: If the access token 
    607                refresh failed. 
    608        """ 
    609        self.token = _cloud_sdk.get_auth_access_token(self._account) 
    610 
    611    @_helpers.copy_docstring(credentials.Credentials) 
    612    def before_request(self, request, method, url, headers): 
    613        self.refresh(request) 
    614        self.apply(headers)