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