1# Copyright 2020 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 async 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 json
28
29import six
30from six.moves import http_client
31from six.moves import urllib
32
33from google.auth import exceptions
34from google.auth import jwt
35from google.oauth2 import _client as client
36
37
38async def _token_endpoint_request_no_throw(
39    request, token_uri, body, access_token=None, use_json=False
40):
41    """Makes a request to the OAuth 2.0 authorization server's token endpoint.
42    This function doesn't throw on response errors.
43
44    Args:
45        request (google.auth.transport.Request): A callable used to make
46            HTTP requests.
47        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
48            URI.
49        body (Mapping[str, str]): The parameters to send in the request body.
50        access_token (Optional(str)): The access token needed to make the request.
51        use_json (Optional(bool)): Use urlencoded format or json format for the
52            content type. The default value is False.
53
54    Returns:
55        Tuple(bool, Mapping[str, str]): A boolean indicating if the request is
56            successful, and a mapping for the JSON-decoded response data.
57    """
58    if use_json:
59        headers = {"Content-Type": client._JSON_CONTENT_TYPE}
60        body = json.dumps(body).encode("utf-8")
61    else:
62        headers = {"Content-Type": client._URLENCODED_CONTENT_TYPE}
63        body = urllib.parse.urlencode(body).encode("utf-8")
64
65    if access_token:
66        headers["Authorization"] = "Bearer {}".format(access_token)
67
68    retry = 0
69    # retry to fetch token for maximum of two times if any internal failure
70    # occurs.
71    while True:
72
73        response = await request(
74            method="POST", url=token_uri, headers=headers, body=body
75        )
76
77        # Using data.read() resulted in zlib decompression errors. This may require future investigation.
78        response_body1 = await response.content()
79
80        response_body = (
81            response_body1.decode("utf-8")
82            if hasattr(response_body1, "decode")
83            else response_body1
84        )
85
86        response_data = json.loads(response_body)
87
88        if response.status == http_client.OK:
89            break
90        else:
91            error_desc = response_data.get("error_description") or ""
92            error_code = response_data.get("error") or ""
93            if (
94                any(e == "internal_failure" for e in (error_code, error_desc))
95                and retry < 1
96            ):
97                retry += 1
98                continue
99            return response.status == http_client.OK, response_data
100
101    return response.status == http_client.OK, response_data
102
103
104async def _token_endpoint_request(
105    request, token_uri, body, access_token=None, use_json=False
106):
107    """Makes a request to the OAuth 2.0 authorization server's token endpoint.
108
109    Args:
110        request (google.auth.transport.Request): A callable used to make
111            HTTP requests.
112        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
113            URI.
114        body (Mapping[str, str]): The parameters to send in the request body.
115        access_token (Optional(str)): The access token needed to make the request.
116        use_json (Optional(bool)): Use urlencoded format or json format for the
117            content type. The default value is False.
118
119    Returns:
120        Mapping[str, str]: The JSON-decoded response data.
121
122    Raises:
123        google.auth.exceptions.RefreshError: If the token endpoint returned
124            an error.
125    """
126    response_status_ok, response_data = await _token_endpoint_request_no_throw(
127        request, token_uri, body, access_token=access_token, use_json=use_json
128    )
129    if not response_status_ok:
130        client._handle_error_response(response_data)
131    return response_data
132
133
134async def jwt_grant(request, token_uri, assertion):
135    """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
136
137    For more details, see `rfc7523 section 4`_.
138
139    Args:
140        request (google.auth.transport.Request): A callable used to make
141            HTTP requests.
142        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
143            URI.
144        assertion (str): The OAuth 2.0 assertion.
145
146    Returns:
147        Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
148            expiration, and additional data returned by the token endpoint.
149
150    Raises:
151        google.auth.exceptions.RefreshError: If the token endpoint returned
152            an error.
153
154    .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
155    """
156    body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
157
158    response_data = await _token_endpoint_request(request, token_uri, body)
159
160    try:
161        access_token = response_data["access_token"]
162    except KeyError as caught_exc:
163        new_exc = exceptions.RefreshError("No access token in response.", response_data)
164        six.raise_from(new_exc, caught_exc)
165
166    expiry = client._parse_expiry(response_data)
167
168    return access_token, expiry, response_data
169
170
171async def id_token_jwt_grant(request, token_uri, assertion):
172    """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
173    requests an OpenID Connect ID Token instead of an access token.
174
175    This is a variant on the standard JWT Profile that is currently unique
176    to Google. This was added for the benefit of authenticating to services
177    that require ID Tokens instead of access tokens or JWT bearer tokens.
178
179    Args:
180        request (google.auth.transport.Request): A callable used to make
181            HTTP requests.
182        token_uri (str): The OAuth 2.0 authorization server's token endpoint
183            URI.
184        assertion (str): JWT token signed by a service account. The token's
185            payload must include a ``target_audience`` claim.
186
187    Returns:
188        Tuple[str, Optional[datetime], Mapping[str, str]]:
189            The (encoded) Open ID Connect ID Token, expiration, and additional
190            data returned by the endpoint.
191
192    Raises:
193        google.auth.exceptions.RefreshError: If the token endpoint returned
194            an error.
195    """
196    body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
197
198    response_data = await _token_endpoint_request(request, token_uri, body)
199
200    try:
201        id_token = response_data["id_token"]
202    except KeyError as caught_exc:
203        new_exc = exceptions.RefreshError("No ID token in response.", response_data)
204        six.raise_from(new_exc, caught_exc)
205
206    payload = jwt.decode(id_token, verify=False)
207    expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
208
209    return id_token, expiry, response_data
210
211
212async def refresh_grant(
213    request,
214    token_uri,
215    refresh_token,
216    client_id,
217    client_secret,
218    scopes=None,
219    rapt_token=None,
220):
221    """Implements the OAuth 2.0 refresh token grant.
222
223    For more details, see `rfc678 section 6`_.
224
225    Args:
226        request (google.auth.transport.Request): A callable used to make
227            HTTP requests.
228        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
229            URI.
230        refresh_token (str): The refresh token to use to get a new access
231            token.
232        client_id (str): The OAuth 2.0 application's client ID.
233        client_secret (str): The Oauth 2.0 appliaction's client secret.
234        scopes (Optional(Sequence[str])): Scopes to request. If present, all
235            scopes must be authorized for the refresh token. Useful if refresh
236            token has a wild card scope (e.g.
237            'https://www.googleapis.com/auth/any-api').
238        rapt_token (Optional(str)): The reauth Proof Token.
239
240    Returns:
241        Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
242            access token, new or current refresh token, expiration, and additional data
243            returned by the token endpoint.
244
245    Raises:
246        google.auth.exceptions.RefreshError: If the token endpoint returned
247            an error.
248
249    .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
250    """
251    body = {
252        "grant_type": client._REFRESH_GRANT_TYPE,
253        "client_id": client_id,
254        "client_secret": client_secret,
255        "refresh_token": refresh_token,
256    }
257    if scopes:
258        body["scope"] = " ".join(scopes)
259    if rapt_token:
260        body["rapt"] = rapt_token
261
262    response_data = await _token_endpoint_request(request, token_uri, body)
263    return client._handle_refresh_grant_response(response_data, refresh_token)
264