blob: f819371af8dea008d52771d41cfda9c9bc32a78b [file] [log] [blame]
C.J. Collier37141e42020-02-13 13:49:49 -08001# Copyright 2016 Google LLC
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -07002#
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
Tres Seaver560cf1e2021-08-03 16:35:54 -040027import http.client
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070028import json
Tres Seaver560cf1e2021-08-03 16:35:54 -040029import urllib
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070030
31from google.auth import _helpers
32from google.auth import exceptions
Christophe Tatonb649b432018-02-08 14:12:23 -080033from google.auth import jwt
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070034
Bu Sun Kim9eec0912019-10-21 17:04:21 -070035_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
arithmetic172882293fe2021-04-14 11:22:13 -070036_JSON_CONTENT_TYPE = "application/json"
Bu Sun Kim9eec0912019-10-21 17:04:21 -070037_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
38_REFRESH_GRANT_TYPE = "refresh_token"
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070039
40
arithmetic172882293fe2021-04-14 11:22:13 -070041def _handle_error_response(response_data):
42 """Translates an error response into an exception.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070043
44 Args:
arithmetic172882293fe2021-04-14 11:22:13 -070045 response_data (Mapping): The decoded response data.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070046
47 Raises:
arithmetic172882293fe2021-04-14 11:22:13 -070048 google.auth.exceptions.RefreshError: The errors contained in response_data.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070049 """
50 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -070051 error_details = "{}: {}".format(
arithmetic172882293fe2021-04-14 11:22:13 -070052 response_data["error"], response_data.get("error_description")
Bu Sun Kim9eec0912019-10-21 17:04:21 -070053 )
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070054 # If no details could be extracted, use the response data.
55 except (KeyError, ValueError):
arithmetic172882293fe2021-04-14 11:22:13 -070056 error_details = json.dumps(response_data)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070057
arithmetic172882293fe2021-04-14 11:22:13 -070058 raise exceptions.RefreshError(error_details, response_data)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070059
60
61def _parse_expiry(response_data):
62 """Parses the expiry field from a response into a datetime.
63
64 Args:
65 response_data (Mapping): The JSON-parsed response data.
66
67 Returns:
68 Optional[datetime]: The expiration or ``None`` if no expiration was
69 specified.
70 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -070071 expires_in = response_data.get("expires_in", None)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070072
73 if expires_in is not None:
Bu Sun Kim9eec0912019-10-21 17:04:21 -070074 return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070075 else:
76 return None
77
78
arithmetic172882293fe2021-04-14 11:22:13 -070079def _token_endpoint_request_no_throw(
80 request, token_uri, body, access_token=None, use_json=False
81):
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070082 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
arithmetic172882293fe2021-04-14 11:22:13 -070083 This function doesn't throw on response errors.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070084
85 Args:
86 request (google.auth.transport.Request): A callable used to make
87 HTTP requests.
88 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
89 URI.
90 body (Mapping[str, str]): The parameters to send in the request body.
arithmetic172882293fe2021-04-14 11:22:13 -070091 access_token (Optional(str)): The access token needed to make the request.
92 use_json (Optional(bool)): Use urlencoded format or json format for the
93 content type. The default value is False.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070094
95 Returns:
arithmetic172882293fe2021-04-14 11:22:13 -070096 Tuple(bool, Mapping[str, str]): A boolean indicating if the request is
97 successful, and a mapping for the JSON-decoded response data.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070098 """
arithmetic172882293fe2021-04-14 11:22:13 -070099 if use_json:
100 headers = {"Content-Type": _JSON_CONTENT_TYPE}
101 body = json.dumps(body).encode("utf-8")
102 else:
103 headers = {"Content-Type": _URLENCODED_CONTENT_TYPE}
104 body = urllib.parse.urlencode(body).encode("utf-8")
105
106 if access_token:
107 headers["Authorization"] = "Bearer {}".format(access_token)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700108
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700109 retry = 0
110 # retry to fetch token for maximum of two times if any internal failure
111 # occurs.
112 while True:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700113 response = request(method="POST", url=token_uri, headers=headers, body=body)
Steve3b5d3e22019-12-02 10:57:30 -0800114 response_body = (
115 response.data.decode("utf-8")
116 if hasattr(response.data, "decode")
117 else response.data
118 )
Georgy Savva46bb58e2019-11-13 22:21:57 +0300119 response_data = json.loads(response_body)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700120
Tres Seaver560cf1e2021-08-03 16:35:54 -0400121 if response.status == http.client.OK:
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700122 break
123 else:
Georgy Savva46bb58e2019-11-13 22:21:57 +0300124 error_desc = response_data.get("error_description") or ""
125 error_code = response_data.get("error") or ""
126 if (
127 any(e == "internal_failure" for e in (error_code, error_desc))
128 and retry < 1
129 ):
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700130 retry += 1
131 continue
Tres Seaver560cf1e2021-08-03 16:35:54 -0400132 return response.status == http.client.OK, response_data
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700133
Tres Seaver560cf1e2021-08-03 16:35:54 -0400134 return response.status == http.client.OK, response_data
arithmetic172882293fe2021-04-14 11:22:13 -0700135
136
137def _token_endpoint_request(
138 request, token_uri, body, access_token=None, use_json=False
139):
140 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
141
142 Args:
143 request (google.auth.transport.Request): A callable used to make
144 HTTP requests.
145 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
146 URI.
147 body (Mapping[str, str]): The parameters to send in the request body.
148 access_token (Optional(str)): The access token needed to make the request.
149 use_json (Optional(bool)): Use urlencoded format or json format for the
150 content type. The default value is False.
151
152 Returns:
153 Mapping[str, str]: The JSON-decoded response data.
154
155 Raises:
156 google.auth.exceptions.RefreshError: If the token endpoint returned
157 an error.
158 """
159 response_status_ok, response_data = _token_endpoint_request_no_throw(
160 request, token_uri, body, access_token=access_token, use_json=use_json
161 )
162 if not response_status_ok:
163 _handle_error_response(response_data)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700164 return response_data
165
166
167def jwt_grant(request, token_uri, assertion):
168 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
169
170 For more details, see `rfc7523 section 4`_.
171
172 Args:
173 request (google.auth.transport.Request): A callable used to make
174 HTTP requests.
175 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
176 URI.
177 assertion (str): The OAuth 2.0 assertion.
178
179 Returns:
180 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
181 expiration, and additional data returned by the token endpoint.
182
183 Raises:
184 google.auth.exceptions.RefreshError: If the token endpoint returned
185 an error.
186
187 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
188 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700189 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700190
191 response_data = _token_endpoint_request(request, token_uri, body)
192
193 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700194 access_token = response_data["access_token"]
Danny Hermes895e3692017-11-09 11:35:57 -0800195 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700196 new_exc = exceptions.RefreshError("No access token in response.", response_data)
Tres Seaver560cf1e2021-08-03 16:35:54 -0400197 raise new_exc from caught_exc
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700198
199 expiry = _parse_expiry(response_data)
200
201 return access_token, expiry, response_data
202
203
Christophe Tatonb649b432018-02-08 14:12:23 -0800204def id_token_jwt_grant(request, token_uri, assertion):
205 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
206 requests an OpenID Connect ID Token instead of an access token.
207
208 This is a variant on the standard JWT Profile that is currently unique
209 to Google. This was added for the benefit of authenticating to services
210 that require ID Tokens instead of access tokens or JWT bearer tokens.
211
212 Args:
213 request (google.auth.transport.Request): A callable used to make
214 HTTP requests.
215 token_uri (str): The OAuth 2.0 authorization server's token endpoint
216 URI.
217 assertion (str): JWT token signed by a service account. The token's
218 payload must include a ``target_audience`` claim.
219
220 Returns:
221 Tuple[str, Optional[datetime], Mapping[str, str]]:
222 The (encoded) Open ID Connect ID Token, expiration, and additional
223 data returned by the endpoint.
224
225 Raises:
226 google.auth.exceptions.RefreshError: If the token endpoint returned
227 an error.
228 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700229 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Christophe Tatonb649b432018-02-08 14:12:23 -0800230
231 response_data = _token_endpoint_request(request, token_uri, body)
232
233 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700234 id_token = response_data["id_token"]
Christophe Tatonb649b432018-02-08 14:12:23 -0800235 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700236 new_exc = exceptions.RefreshError("No ID token in response.", response_data)
Tres Seaver560cf1e2021-08-03 16:35:54 -0400237 raise new_exc from caught_exc
Christophe Tatonb649b432018-02-08 14:12:23 -0800238
239 payload = jwt.decode(id_token, verify=False)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700240 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
Christophe Tatonb649b432018-02-08 14:12:23 -0800241
242 return id_token, expiry, response_data
243
244
arithmetic172882293fe2021-04-14 11:22:13 -0700245def _handle_refresh_grant_response(response_data, refresh_token):
246 """Extract tokens from refresh grant response.
247
248 Args:
249 response_data (Mapping[str, str]): Refresh grant response data.
250 refresh_token (str): Current refresh token.
251
252 Returns:
253 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
254 refresh token, expiration, and additional data returned by the token
255 endpoint. If response_data doesn't have refresh token, then the current
256 refresh token will be returned.
257
258 Raises:
259 google.auth.exceptions.RefreshError: If the token endpoint returned
260 an error.
261 """
262 try:
263 access_token = response_data["access_token"]
264 except KeyError as caught_exc:
265 new_exc = exceptions.RefreshError("No access token in response.", response_data)
Tres Seaver560cf1e2021-08-03 16:35:54 -0400266 raise new_exc from caught_exc
arithmetic172882293fe2021-04-14 11:22:13 -0700267
268 refresh_token = response_data.get("refresh_token", refresh_token)
269 expiry = _parse_expiry(response_data)
270
271 return access_token, refresh_token, expiry, response_data
272
273
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700274def refresh_grant(
arithmetic172882293fe2021-04-14 11:22:13 -0700275 request,
276 token_uri,
277 refresh_token,
278 client_id,
279 client_secret,
280 scopes=None,
281 rapt_token=None,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700282):
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700283 """Implements the OAuth 2.0 refresh token grant.
284
285 For more details, see `rfc678 section 6`_.
286
287 Args:
288 request (google.auth.transport.Request): A callable used to make
289 HTTP requests.
290 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
291 URI.
292 refresh_token (str): The refresh token to use to get a new access
293 token.
294 client_id (str): The OAuth 2.0 application's client ID.
295 client_secret (str): The Oauth 2.0 appliaction's client secret.
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400296 scopes (Optional(Sequence[str])): Scopes to request. If present, all
297 scopes must be authorized for the refresh token. Useful if refresh
298 token has a wild card scope (e.g.
299 'https://www.googleapis.com/auth/any-api').
arithmetic172882293fe2021-04-14 11:22:13 -0700300 rapt_token (Optional(str)): The reauth Proof Token.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700301
302 Returns:
arithmetic172882293fe2021-04-14 11:22:13 -0700303 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
304 token, new or current refresh token, expiration, and additional data
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700305 returned by the token endpoint.
306
307 Raises:
308 google.auth.exceptions.RefreshError: If the token endpoint returned
309 an error.
310
311 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
312 """
313 body = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700314 "grant_type": _REFRESH_GRANT_TYPE,
315 "client_id": client_id,
316 "client_secret": client_secret,
317 "refresh_token": refresh_token,
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700318 }
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400319 if scopes:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700320 body["scope"] = " ".join(scopes)
arithmetic172882293fe2021-04-14 11:22:13 -0700321 if rapt_token:
322 body["rapt"] = rapt_token
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700323
324 response_data = _token_endpoint_request(request, token_uri, body)
arithmetic172882293fe2021-04-14 11:22:13 -0700325 return _handle_refresh_grant_response(response_data, refresh_token)