blob: 2f4e8474b55748e6f397fc6d14dd8f147bd22819 [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
27import json
arithmetic17285bd5ccf2021-10-21 15:25:46 -070028
29import six
30from six.moves import http_client
31from six.moves import urllib
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070032
33from google.auth import _helpers
34from google.auth import exceptions
Christophe Tatonb649b432018-02-08 14:12:23 -080035from google.auth import jwt
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070036
Bu Sun Kim9eec0912019-10-21 17:04:21 -070037_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
arithmetic172882293fe2021-04-14 11:22:13 -070038_JSON_CONTENT_TYPE = "application/json"
Bu Sun Kim9eec0912019-10-21 17:04:21 -070039_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
40_REFRESH_GRANT_TYPE = "refresh_token"
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070041
42
arithmetic172882293fe2021-04-14 11:22:13 -070043def _handle_error_response(response_data):
44 """Translates an error response into an exception.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070045
46 Args:
arithmetic172882293fe2021-04-14 11:22:13 -070047 response_data (Mapping): The decoded response data.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070048
49 Raises:
arithmetic172882293fe2021-04-14 11:22:13 -070050 google.auth.exceptions.RefreshError: The errors contained in response_data.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070051 """
52 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -070053 error_details = "{}: {}".format(
arithmetic172882293fe2021-04-14 11:22:13 -070054 response_data["error"], response_data.get("error_description")
Bu Sun Kim9eec0912019-10-21 17:04:21 -070055 )
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070056 # If no details could be extracted, use the response data.
57 except (KeyError, ValueError):
arithmetic172882293fe2021-04-14 11:22:13 -070058 error_details = json.dumps(response_data)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070059
arithmetic172882293fe2021-04-14 11:22:13 -070060 raise exceptions.RefreshError(error_details, response_data)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070061
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 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -070073 expires_in = response_data.get("expires_in", None)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070074
75 if expires_in is not None:
Bu Sun Kim9eec0912019-10-21 17:04:21 -070076 return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070077 else:
78 return None
79
80
arithmetic172882293fe2021-04-14 11:22:13 -070081def _token_endpoint_request_no_throw(
82 request, token_uri, body, access_token=None, use_json=False
83):
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070084 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
arithmetic172882293fe2021-04-14 11:22:13 -070085 This function doesn't throw on response errors.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070086
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.
arithmetic172882293fe2021-04-14 11:22:13 -070093 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.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070096
97 Returns:
arithmetic172882293fe2021-04-14 11:22:13 -070098 Tuple(bool, Mapping[str, str]): A boolean indicating if the request is
99 successful, and a mapping for the JSON-decoded response data.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700100 """
arithmetic172882293fe2021-04-14 11:22:13 -0700101 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)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700110
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700111 retry = 0
112 # retry to fetch token for maximum of two times if any internal failure
113 # occurs.
114 while True:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700115 response = request(method="POST", url=token_uri, headers=headers, body=body)
Steve3b5d3e22019-12-02 10:57:30 -0800116 response_body = (
117 response.data.decode("utf-8")
118 if hasattr(response.data, "decode")
119 else response.data
120 )
Georgy Savva46bb58e2019-11-13 22:21:57 +0300121 response_data = json.loads(response_body)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700122
arithmetic17285bd5ccf2021-10-21 15:25:46 -0700123 if response.status == http_client.OK:
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700124 break
125 else:
Georgy Savva46bb58e2019-11-13 22:21:57 +0300126 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 ):
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700132 retry += 1
133 continue
arithmetic17285bd5ccf2021-10-21 15:25:46 -0700134 return response.status == http_client.OK, response_data
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700135
arithmetic17285bd5ccf2021-10-21 15:25:46 -0700136 return response.status == http_client.OK, response_data
arithmetic172882293fe2021-04-14 11:22:13 -0700137
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)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700166 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 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700191 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700192
193 response_data = _token_endpoint_request(request, token_uri, body)
194
195 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700196 access_token = response_data["access_token"]
Danny Hermes895e3692017-11-09 11:35:57 -0800197 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700198 new_exc = exceptions.RefreshError("No access token in response.", response_data)
arithmetic17285bd5ccf2021-10-21 15:25:46 -0700199 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700200
201 expiry = _parse_expiry(response_data)
202
203 return access_token, expiry, response_data
204
205
Christophe Tatonb649b432018-02-08 14:12:23 -0800206def 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 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700231 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Christophe Tatonb649b432018-02-08 14:12:23 -0800232
233 response_data = _token_endpoint_request(request, token_uri, body)
234
235 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700236 id_token = response_data["id_token"]
Christophe Tatonb649b432018-02-08 14:12:23 -0800237 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700238 new_exc = exceptions.RefreshError("No ID token in response.", response_data)
arithmetic17285bd5ccf2021-10-21 15:25:46 -0700239 six.raise_from(new_exc, caught_exc)
Christophe Tatonb649b432018-02-08 14:12:23 -0800240
241 payload = jwt.decode(id_token, verify=False)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700242 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
Christophe Tatonb649b432018-02-08 14:12:23 -0800243
244 return id_token, expiry, response_data
245
246
arithmetic172882293fe2021-04-14 11:22:13 -0700247def _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)
arithmetic17285bd5ccf2021-10-21 15:25:46 -0700268 six.raise_from(new_exc, caught_exc)
arithmetic172882293fe2021-04-14 11:22:13 -0700269
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
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700276def refresh_grant(
arithmetic172882293fe2021-04-14 11:22:13 -0700277 request,
278 token_uri,
279 refresh_token,
280 client_id,
281 client_secret,
282 scopes=None,
283 rapt_token=None,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700284):
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700285 """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.
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400298 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').
arithmetic172882293fe2021-04-14 11:22:13 -0700302 rapt_token (Optional(str)): The reauth Proof Token.
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700303
304 Returns:
arithmetic172882293fe2021-04-14 11:22:13 -0700305 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
306 token, new or current refresh token, expiration, and additional data
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700307 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 = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700316 "grant_type": _REFRESH_GRANT_TYPE,
317 "client_id": client_id,
318 "client_secret": client_secret,
319 "refresh_token": refresh_token,
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700320 }
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400321 if scopes:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700322 body["scope"] = " ".join(scopes)
arithmetic172882293fe2021-04-14 11:22:13 -0700323 if rapt_token:
324 body["rapt"] = rapt_token
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700325
326 response_data = _token_endpoint_request(request, token_uri, body)
arithmetic172882293fe2021-04-14 11:22:13 -0700327 return _handle_refresh_grant_response(response_data, refresh_token)