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 client. 
    16 
    17This is a client for interacting with an OAuth 2.0 authorization server's 
    18token endpoint. 
    19 
    20For more information about the token endpoint, see 
    21`Section 3.1 of rfc6749`_ 
    22 
    23.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2 
    24""" 
    25 
    26import datetime 
    27import http.client as http_client 
    28import json 
    29import urllib 
    30 
    31from google.auth import _exponential_backoff 
    32from google.auth import _helpers 
    33from google.auth import credentials 
    34from google.auth import exceptions 
    35from google.auth import jwt 
    36from google.auth import metrics 
    37from google.auth import transport 
    38 
    39_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded" 
    40_JSON_CONTENT_TYPE = "application/json" 
    41_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer" 
    42_REFRESH_GRANT_TYPE = "refresh_token" 
    43 
    44 
    45def _handle_error_response(response_data, retryable_error): 
    46    """Translates an error response into an exception. 
    47 
    48    Args: 
    49        response_data (Mapping | str): The decoded response data. 
    50        retryable_error Optional[bool]: A boolean indicating if an error is retryable. 
    51            Defaults to False. 
    52 
    53    Raises: 
    54        google.auth.exceptions.RefreshError: The errors contained in response_data. 
    55    """ 
    56 
    57    retryable_error = retryable_error if retryable_error else False 
    58 
    59    if isinstance(response_data, str): 
    60        raise exceptions.RefreshError(response_data, retryable=retryable_error) 
    61    try: 
    62        error_details = "{}: {}".format( 
    63            response_data["error"], response_data.get("error_description") 
    64        ) 
    65    # If no details could be extracted, use the response data. 
    66    except (KeyError, ValueError): 
    67        error_details = json.dumps(response_data) 
    68 
    69    raise exceptions.RefreshError( 
    70        error_details, response_data, retryable=retryable_error 
    71    ) 
    72 
    73 
    74def _can_retry(status_code, response_data): 
    75    """Checks if a request can be retried by inspecting the status code 
    76    and response body of the request. 
    77 
    78    Args: 
    79        status_code (int): The response status code. 
    80        response_data (Mapping | str): The decoded response data. 
    81 
    82    Returns: 
    83      bool: True if the response is retryable. False otherwise. 
    84    """ 
    85    if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES: 
    86        return True 
    87 
    88    try: 
    89        # For a failed response, response_body could be a string 
    90        error_desc = response_data.get("error_description") or "" 
    91        error_code = response_data.get("error") or "" 
    92 
    93        if not isinstance(error_code, str) or not isinstance(error_desc, str): 
    94            return False 
    95 
    96        # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1 
    97        # This is needed because a redirect will not return a 500 status code. 
    98        retryable_error_descriptions = { 
    99            "internal_failure", 
    100            "server_error", 
    101            "temporarily_unavailable", 
    102        } 
    103 
    104        if any(e in retryable_error_descriptions for e in (error_code, error_desc)): 
    105            return True 
    106 
    107    except AttributeError: 
    108        pass 
    109 
    110    return False 
    111 
    112 
    113def _parse_expiry(response_data): 
    114    """Parses the expiry field from a response into a datetime. 
    115 
    116    Args: 
    117        response_data (Mapping): The JSON-parsed response data. 
    118 
    119    Returns: 
    120        Optional[datetime]: The expiration or ``None`` if no expiration was 
    121            specified. 
    122    """ 
    123    expires_in = response_data.get("expires_in", None) 
    124 
    125    if expires_in is not None: 
    126        # Some services do not respect the OAUTH2.0 RFC and send expires_in as a 
    127        # JSON String. 
    128        if isinstance(expires_in, str): 
    129            expires_in = int(expires_in) 
    130 
    131        return _helpers.utcnow() + datetime.timedelta(seconds=expires_in) 
    132    else: 
    133        return None 
    134 
    135 
    136def _token_endpoint_request_no_throw( 
    137    request, 
    138    token_uri, 
    139    body, 
    140    access_token=None, 
    141    use_json=False, 
    142    can_retry=True, 
    143    headers=None, 
    144    **kwargs 
    145): 
    146    """Makes a request to the OAuth 2.0 authorization server's token endpoint. 
    147    This function doesn't throw on response errors. 
    148 
    149    Args: 
    150        request (google.auth.transport.Request): A callable used to make 
    151            HTTP requests. 
    152        token_uri (str): The OAuth 2.0 authorizations server's token endpoint 
    153            URI. 
    154        body (Mapping[str, str]): The parameters to send in the request body. 
    155        access_token (Optional(str)): The access token needed to make the request. 
    156        use_json (Optional(bool)): Use urlencoded format or json format for the 
    157            content type. The default value is False. 
    158        can_retry (bool): Enable or disable request retry behavior. 
    159        headers (Optional[Mapping[str, str]]): The headers for the request. 
    160        kwargs: Additional arguments passed on to the request method. The 
    161            kwargs will be passed to `requests.request` method, see: 
    162            https://docs.python-requests.org/en/latest/api/#requests.request. 
    163            For example, you can use `cert=("cert_pem_path", "key_pem_path")` 
    164            to set up client side SSL certificate, and use 
    165            `verify="ca_bundle_path"` to set up the CA certificates for sever 
    166            side SSL certificate verification. 
    167 
    168    Returns: 
    169        Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating 
    170          if the request is successful, a mapping for the JSON-decoded response 
    171          data and in the case of an error a boolean indicating if the error 
    172          is retryable. 
    173    """ 
    174    if use_json: 
    175        headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE} 
    176        body = json.dumps(body).encode("utf-8") 
    177    else: 
    178        headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE} 
    179        body = urllib.parse.urlencode(body).encode("utf-8") 
    180 
    181    if access_token: 
    182        headers_to_use["Authorization"] = "Bearer {}".format(access_token) 
    183 
    184    if headers: 
    185        headers_to_use.update(headers) 
    186 
    187    response_data = {} 
    188    retryable_error = False 
    189 
    190    retries = _exponential_backoff.ExponentialBackoff() 
    191    for _ in retries: 
    192        response = request( 
    193            method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs 
    194        ) 
    195        response_body = ( 
    196            response.data.decode("utf-8") 
    197            if hasattr(response.data, "decode") 
    198            else response.data 
    199        ) 
    200 
    201        try: 
    202            # response_body should be a JSON 
    203            response_data = json.loads(response_body) 
    204        except ValueError: 
    205            response_data = response_body 
    206 
    207        if response.status == http_client.OK: 
    208            return True, response_data, None 
    209 
    210        retryable_error = _can_retry( 
    211            status_code=response.status, response_data=response_data 
    212        ) 
    213 
    214        if not can_retry or not retryable_error: 
    215            return False, response_data, retryable_error 
    216 
    217    return False, response_data, retryable_error 
    218 
    219 
    220def _token_endpoint_request( 
    221    request, 
    222    token_uri, 
    223    body, 
    224    access_token=None, 
    225    use_json=False, 
    226    can_retry=True, 
    227    headers=None, 
    228    **kwargs 
    229): 
    230    """Makes a request to the OAuth 2.0 authorization server's token endpoint. 
    231 
    232    Args: 
    233        request (google.auth.transport.Request): A callable used to make 
    234            HTTP requests. 
    235        token_uri (str): The OAuth 2.0 authorizations server's token endpoint 
    236            URI. 
    237        body (Mapping[str, str]): The parameters to send in the request body. 
    238        access_token (Optional(str)): The access token needed to make the request. 
    239        use_json (Optional(bool)): Use urlencoded format or json format for the 
    240            content type. The default value is False. 
    241        can_retry (bool): Enable or disable request retry behavior. 
    242        headers (Optional[Mapping[str, str]]): The headers for the request. 
    243        kwargs: Additional arguments passed on to the request method. The 
    244            kwargs will be passed to `requests.request` method, see: 
    245            https://docs.python-requests.org/en/latest/api/#requests.request. 
    246            For example, you can use `cert=("cert_pem_path", "key_pem_path")` 
    247            to set up client side SSL certificate, and use 
    248            `verify="ca_bundle_path"` to set up the CA certificates for sever 
    249            side SSL certificate verification. 
    250 
    251    Returns: 
    252        Mapping[str, str]: The JSON-decoded response data. 
    253 
    254    Raises: 
    255        google.auth.exceptions.RefreshError: If the token endpoint returned 
    256            an error. 
    257    """ 
    258 
    259    response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw( 
    260        request, 
    261        token_uri, 
    262        body, 
    263        access_token=access_token, 
    264        use_json=use_json, 
    265        can_retry=can_retry, 
    266        headers=headers, 
    267        **kwargs 
    268    ) 
    269    if not response_status_ok: 
    270        _handle_error_response(response_data, retryable_error) 
    271    return response_data 
    272 
    273 
    274def jwt_grant(request, token_uri, assertion, can_retry=True): 
    275    """Implements the JWT Profile for OAuth 2.0 Authorization Grants. 
    276 
    277    For more details, see `rfc7523 section 4`_. 
    278 
    279    Args: 
    280        request (google.auth.transport.Request): A callable used to make 
    281            HTTP requests. 
    282        token_uri (str): The OAuth 2.0 authorizations server's token endpoint 
    283            URI. 
    284        assertion (str): The OAuth 2.0 assertion. 
    285        can_retry (bool): Enable or disable request retry behavior. 
    286 
    287    Returns: 
    288        Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, 
    289            expiration, and additional data returned by the token endpoint. 
    290 
    291    Raises: 
    292        google.auth.exceptions.RefreshError: If the token endpoint returned 
    293            an error. 
    294 
    295    .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4 
    296    """ 
    297    body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} 
    298 
    299    response_data = _token_endpoint_request( 
    300        request, 
    301        token_uri, 
    302        body, 
    303        can_retry=can_retry, 
    304        headers={ 
    305            metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion() 
    306        }, 
    307    ) 
    308 
    309    try: 
    310        access_token = response_data["access_token"] 
    311    except KeyError as caught_exc: 
    312        new_exc = exceptions.RefreshError( 
    313            "No access token in response.", response_data, retryable=False 
    314        ) 
    315        raise new_exc from caught_exc 
    316 
    317    expiry = _parse_expiry(response_data) 
    318 
    319    return access_token, expiry, response_data 
    320 
    321 
    322def call_iam_generate_id_token_endpoint( 
    323    request, 
    324    iam_id_token_endpoint, 
    325    signer_email, 
    326    audience, 
    327    access_token, 
    328    universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN, 
    329): 
    330    """Call iam.generateIdToken endpoint to get ID token. 
    331 
    332    Args: 
    333        request (google.auth.transport.Request): A callable used to make 
    334            HTTP requests. 
    335        iam_id_token_endpoint (str): The IAM ID token endpoint to use. 
    336        signer_email (str): The signer email used to form the IAM 
    337            generateIdToken endpoint. 
    338        audience (str): The audience for the ID token. 
    339        access_token (str): The access token used to call the IAM endpoint. 
    340        universe_domain (str): The universe domain for the request. The 
    341            default is ``googleapis.com``. 
    342 
    343    Returns: 
    344        Tuple[str, datetime]: The ID token and expiration. 
    345    """ 
    346    body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"} 
    347 
    348    response_data = _token_endpoint_request( 
    349        request, 
    350        iam_id_token_endpoint.replace( 
    351            credentials.DEFAULT_UNIVERSE_DOMAIN, universe_domain 
    352        ).format(signer_email), 
    353        body, 
    354        access_token=access_token, 
    355        use_json=True, 
    356    ) 
    357 
    358    try: 
    359        id_token = response_data["token"] 
    360    except KeyError as caught_exc: 
    361        new_exc = exceptions.RefreshError( 
    362            "No ID token in response.", response_data, retryable=False 
    363        ) 
    364        raise new_exc from caught_exc 
    365 
    366    payload = jwt.decode(id_token, verify=False) 
    367    expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) 
    368 
    369    return id_token, expiry 
    370 
    371 
    372def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): 
    373    """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but 
    374    requests an OpenID Connect ID Token instead of an access token. 
    375 
    376    This is a variant on the standard JWT Profile that is currently unique 
    377    to Google. This was added for the benefit of authenticating to services 
    378    that require ID Tokens instead of access tokens or JWT bearer tokens. 
    379 
    380    Args: 
    381        request (google.auth.transport.Request): A callable used to make 
    382            HTTP requests. 
    383        token_uri (str): The OAuth 2.0 authorization server's token endpoint 
    384            URI. 
    385        assertion (str): JWT token signed by a service account. The token's 
    386            payload must include a ``target_audience`` claim. 
    387        can_retry (bool): Enable or disable request retry behavior. 
    388 
    389    Returns: 
    390        Tuple[str, Optional[datetime], Mapping[str, str]]: 
    391            The (encoded) Open ID Connect ID Token, expiration, and additional 
    392            data returned by the endpoint. 
    393 
    394    Raises: 
    395        google.auth.exceptions.RefreshError: If the token endpoint returned 
    396            an error. 
    397    """ 
    398    body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} 
    399 
    400    response_data = _token_endpoint_request( 
    401        request, 
    402        token_uri, 
    403        body, 
    404        can_retry=can_retry, 
    405        headers={ 
    406            metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion() 
    407        }, 
    408    ) 
    409 
    410    try: 
    411        id_token = response_data["id_token"] 
    412    except KeyError as caught_exc: 
    413        new_exc = exceptions.RefreshError( 
    414            "No ID token in response.", response_data, retryable=False 
    415        ) 
    416        raise new_exc from caught_exc 
    417 
    418    payload = jwt.decode(id_token, verify=False) 
    419    expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) 
    420 
    421    return id_token, expiry, response_data 
    422 
    423 
    424def _handle_refresh_grant_response(response_data, refresh_token): 
    425    """Extract tokens from refresh grant response. 
    426 
    427    Args: 
    428        response_data (Mapping[str, str]): Refresh grant response data. 
    429        refresh_token (str): Current refresh token. 
    430 
    431    Returns: 
    432        Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token, 
    433            refresh token, expiration, and additional data returned by the token 
    434            endpoint. If response_data doesn't have refresh token, then the current 
    435            refresh token will be returned. 
    436 
    437    Raises: 
    438        google.auth.exceptions.RefreshError: If the token endpoint returned 
    439            an error. 
    440    """ 
    441    try: 
    442        access_token = response_data["access_token"] 
    443    except KeyError as caught_exc: 
    444        new_exc = exceptions.RefreshError( 
    445            "No access token in response.", response_data, retryable=False 
    446        ) 
    447        raise new_exc from caught_exc 
    448 
    449    refresh_token = response_data.get("refresh_token", refresh_token) 
    450    expiry = _parse_expiry(response_data) 
    451 
    452    return access_token, refresh_token, expiry, response_data 
    453 
    454 
    455def refresh_grant( 
    456    request, 
    457    token_uri, 
    458    refresh_token, 
    459    client_id, 
    460    client_secret, 
    461    scopes=None, 
    462    rapt_token=None, 
    463    can_retry=True, 
    464): 
    465    """Implements the OAuth 2.0 refresh token grant. 
    466 
    467    For more details, see `rfc678 section 6`_. 
    468 
    469    Args: 
    470        request (google.auth.transport.Request): A callable used to make 
    471            HTTP requests. 
    472        token_uri (str): The OAuth 2.0 authorizations server's token endpoint 
    473            URI. 
    474        refresh_token (str): The refresh token to use to get a new access 
    475            token. 
    476        client_id (str): The OAuth 2.0 application's client ID. 
    477        client_secret (str): The Oauth 2.0 appliaction's client secret. 
    478        scopes (Optional(Sequence[str])): Scopes to request. If present, all 
    479            scopes must be authorized for the refresh token. Useful if refresh 
    480            token has a wild card scope (e.g. 
    481            'https://www.googleapis.com/auth/any-api'). 
    482        rapt_token (Optional(str)): The reauth Proof Token. 
    483        can_retry (bool): Enable or disable request retry behavior. 
    484 
    485    Returns: 
    486        Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access 
    487            token, new or current refresh token, expiration, and additional data 
    488            returned by the token endpoint. 
    489 
    490    Raises: 
    491        google.auth.exceptions.RefreshError: If the token endpoint returned 
    492            an error. 
    493 
    494    .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6 
    495    """ 
    496    body = { 
    497        "grant_type": _REFRESH_GRANT_TYPE, 
    498        "client_id": client_id, 
    499        "client_secret": client_secret, 
    500        "refresh_token": refresh_token, 
    501    } 
    502    if scopes: 
    503        body["scope"] = " ".join(scopes) 
    504    if rapt_token: 
    505        body["rapt"] = rapt_token 
    506 
    507    response_data = _token_endpoint_request( 
    508        request, token_uri, body, can_retry=can_retry 
    509    ) 
    510    return _handle_refresh_grant_response(response_data, refresh_token) 
    511 
    512 
    513def _lookup_trust_boundary(request, url, headers=None): 
    514    """Implements the global lookup of a credential trust boundary. 
    515    For the lookup, we send a request to the global lookup endpoint and then 
    516    parse the response. Service account credentials, workload identity 
    517    pools and workforce pools implementation may have trust boundaries configured. 
    518    Args: 
    519        request (google.auth.transport.Request): A callable used to make 
    520            HTTP requests. 
    521        url (str): The trust boundary lookup url. 
    522        headers (Optional[Mapping[str, str]]): The headers for the request. 
    523    Returns: 
    524        Mapping[str,list|str]: A dictionary containing 
    525            "locations" as a list of allowed locations as strings and 
    526            "encodedLocations" as a hex string. 
    527            e.g: 
    528            { 
    529                "locations": [ 
    530                    "us-central1", "us-east1", "europe-west1", "asia-east1" 
    531                ], 
    532                "encodedLocations": "0xA30" 
    533            } 
    534            If the credential is not set up with explicit trust boundaries, a trust boundary 
    535            of "all" will be returned as a default response. 
    536            { 
    537                "locations": [], 
    538                "encodedLocations": "0x0" 
    539            } 
    540    Raises: 
    541        exceptions.RefreshError: If the response status code is not 200. 
    542        exceptions.MalformedError: If the response is not in a valid format. 
    543    """ 
    544 
    545    response_data = _lookup_trust_boundary_request(request, url, headers=headers) 
    546    # In case of no-op response, the "locations" list may or may not be present as an empty list. 
    547    if "encodedLocations" not in response_data: 
    548        raise exceptions.MalformedError( 
    549            "Invalid trust boundary info: {}".format(response_data) 
    550        ) 
    551    return response_data 
    552 
    553 
    554def _lookup_trust_boundary_request(request, url, can_retry=True, headers=None): 
    555    """Makes a request to the trust boundary lookup endpoint. 
    556 
    557    Args: 
    558        request (google.auth.transport.Request): A callable used to make 
    559            HTTP requests. 
    560        url (str): The trust boundary lookup url. 
    561        can_retry (bool): Enable or disable request retry behavior. Defaults to true. 
    562        headers (Optional[Mapping[str, str]]): The headers for the request. 
    563 
    564    Returns: 
    565        Mapping[str, str]: The JSON-decoded response data. 
    566 
    567    Raises: 
    568        google.auth.exceptions.RefreshError: If the token endpoint returned 
    569            an error. 
    570    """ 
    571    response_status_ok, response_data, retryable_error = _lookup_trust_boundary_request_no_throw( 
    572        request, url, can_retry, headers 
    573    ) 
    574    if not response_status_ok: 
    575        _handle_error_response(response_data, retryable_error) 
    576    return response_data 
    577 
    578 
    579def _lookup_trust_boundary_request_no_throw(request, url, can_retry=True, headers=None): 
    580    """Makes a request to the trust boundary lookup endpoint. This 
    581        function doesn't throw on response errors. 
    582 
    583    Args: 
    584        request (google.auth.transport.Request): A callable used to make 
    585            HTTP requests. 
    586        url (str): The trust boundary lookup url. 
    587        can_retry (bool): Enable or disable request retry behavior. Defaults to true. 
    588        headers (Optional[Mapping[str, str]]): The headers for the request. 
    589 
    590    Returns: 
    591        Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating 
    592          if the request is successful, a mapping for the JSON-decoded response 
    593          data and in the case of an error a boolean indicating if the error 
    594          is retryable. 
    595    """ 
    596 
    597    response_data = {} 
    598    retryable_error = False 
    599 
    600    retries = _exponential_backoff.ExponentialBackoff() 
    601    for _ in retries: 
    602        response = request(method="GET", url=url, headers=headers) 
    603        response_body = ( 
    604            response.data.decode("utf-8") 
    605            if hasattr(response.data, "decode") 
    606            else response.data 
    607        ) 
    608 
    609        try: 
    610            # response_body should be a JSON 
    611            response_data = json.loads(response_body) 
    612        except ValueError: 
    613            response_data = response_body 
    614 
    615        if response.status == http_client.OK: 
    616            return True, response_data, None 
    617 
    618        retryable_error = _can_retry( 
    619            status_code=response.status, response_data=response_data 
    620        ) 
    621 
    622        if not can_retry or not retryable_error: 
    623            return False, response_data, retryable_error 
    624 
    625    return False, response_data, retryable_error