blob: 4cf7a7fe90f44a296b98af629f0628a810884bfd [file] [log] [blame]
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -07001# Copyright 2016 Google Inc.
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
Danny Hermes895e3692017-11-09 11:35:57 -080029import six
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070030from six.moves import http_client
31from six.moves import urllib
32
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"
38_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
39_REFRESH_GRANT_TYPE = "refresh_token"
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070040
41
42def _handle_error_response(response_body):
43 """"Translates an error response into an exception.
44
45 Args:
46 response_body (str): The decoded response data.
47
48 Raises:
49 google.auth.exceptions.RefreshError
50 """
51 try:
52 error_data = json.loads(response_body)
Bu Sun Kim9eec0912019-10-21 17:04:21 -070053 error_details = "{}: {}".format(
54 error_data["error"], error_data.get("error_description")
55 )
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070056 # If no details could be extracted, use the response data.
57 except (KeyError, ValueError):
58 error_details = response_body
59
Bu Sun Kim9eec0912019-10-21 17:04:21 -070060 raise exceptions.RefreshError(error_details, response_body)
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
81def _token_endpoint_request(request, token_uri, body):
82 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
83
84 Args:
85 request (google.auth.transport.Request): A callable used to make
86 HTTP requests.
87 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
88 URI.
89 body (Mapping[str, str]): The parameters to send in the request body.
90
91 Returns:
92 Mapping[str, str]: The JSON-decoded response data.
93
94 Raises:
95 google.auth.exceptions.RefreshError: If the token endpoint returned
96 an error.
97 """
98 body = urllib.parse.urlencode(body)
Bu Sun Kim9eec0912019-10-21 17:04:21 -070099 headers = {"content-type": _URLENCODED_CONTENT_TYPE}
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700100
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700101 retry = 0
102 # retry to fetch token for maximum of two times if any internal failure
103 # occurs.
104 while True:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700105 response = request(method="POST", url=token_uri, headers=headers, body=body)
Steve3b5d3e22019-12-02 10:57:30 -0800106 response_body = (
107 response.data.decode("utf-8")
108 if hasattr(response.data, "decode")
109 else response.data
110 )
Georgy Savva46bb58e2019-11-13 22:21:57 +0300111 response_data = json.loads(response_body)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700112
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700113 if response.status == http_client.OK:
114 break
115 else:
Georgy Savva46bb58e2019-11-13 22:21:57 +0300116 error_desc = response_data.get("error_description") or ""
117 error_code = response_data.get("error") or ""
118 if (
119 any(e == "internal_failure" for e in (error_code, error_desc))
120 and retry < 1
121 ):
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700122 retry += 1
123 continue
124 _handle_error_response(response_body)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700125
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700126 return response_data
127
128
129def jwt_grant(request, token_uri, assertion):
130 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
131
132 For more details, see `rfc7523 section 4`_.
133
134 Args:
135 request (google.auth.transport.Request): A callable used to make
136 HTTP requests.
137 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
138 URI.
139 assertion (str): The OAuth 2.0 assertion.
140
141 Returns:
142 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
143 expiration, and additional data returned by the token endpoint.
144
145 Raises:
146 google.auth.exceptions.RefreshError: If the token endpoint returned
147 an error.
148
149 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
150 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700151 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700152
153 response_data = _token_endpoint_request(request, token_uri, body)
154
155 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700156 access_token = response_data["access_token"]
Danny Hermes895e3692017-11-09 11:35:57 -0800157 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700158 new_exc = exceptions.RefreshError("No access token in response.", response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800159 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700160
161 expiry = _parse_expiry(response_data)
162
163 return access_token, expiry, response_data
164
165
Christophe Tatonb649b432018-02-08 14:12:23 -0800166def id_token_jwt_grant(request, token_uri, assertion):
167 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
168 requests an OpenID Connect ID Token instead of an access token.
169
170 This is a variant on the standard JWT Profile that is currently unique
171 to Google. This was added for the benefit of authenticating to services
172 that require ID Tokens instead of access tokens or JWT bearer tokens.
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 authorization server's token endpoint
178 URI.
179 assertion (str): JWT token signed by a service account. The token's
180 payload must include a ``target_audience`` claim.
181
182 Returns:
183 Tuple[str, Optional[datetime], Mapping[str, str]]:
184 The (encoded) Open ID Connect ID Token, expiration, and additional
185 data returned by the endpoint.
186
187 Raises:
188 google.auth.exceptions.RefreshError: If the token endpoint returned
189 an error.
190 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700191 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Christophe Tatonb649b432018-02-08 14:12:23 -0800192
193 response_data = _token_endpoint_request(request, token_uri, body)
194
195 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700196 id_token = response_data["id_token"]
Christophe Tatonb649b432018-02-08 14:12:23 -0800197 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700198 new_exc = exceptions.RefreshError("No ID token in response.", response_data)
Christophe Tatonb649b432018-02-08 14:12:23 -0800199 six.raise_from(new_exc, caught_exc)
200
201 payload = jwt.decode(id_token, verify=False)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700202 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
Christophe Tatonb649b432018-02-08 14:12:23 -0800203
204 return id_token, expiry, response_data
205
206
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700207def refresh_grant(
208 request, token_uri, refresh_token, client_id, client_secret, scopes=None
209):
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700210 """Implements the OAuth 2.0 refresh token grant.
211
212 For more details, see `rfc678 section 6`_.
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 authorizations server's token endpoint
218 URI.
219 refresh_token (str): The refresh token to use to get a new access
220 token.
221 client_id (str): The OAuth 2.0 application's client ID.
222 client_secret (str): The Oauth 2.0 appliaction's client secret.
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400223 scopes (Optional(Sequence[str])): Scopes to request. If present, all
224 scopes must be authorized for the refresh token. Useful if refresh
225 token has a wild card scope (e.g.
226 'https://www.googleapis.com/auth/any-api').
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700227
228 Returns:
229 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
230 access token, new refresh token, expiration, and additional data
231 returned by the token endpoint.
232
233 Raises:
234 google.auth.exceptions.RefreshError: If the token endpoint returned
235 an error.
236
237 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
238 """
239 body = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700240 "grant_type": _REFRESH_GRANT_TYPE,
241 "client_id": client_id,
242 "client_secret": client_secret,
243 "refresh_token": refresh_token,
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700244 }
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400245 if scopes:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700246 body["scope"] = " ".join(scopes)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700247
248 response_data = _token_endpoint_request(request, token_uri, body)
249
250 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700251 access_token = response_data["access_token"]
Danny Hermes895e3692017-11-09 11:35:57 -0800252 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700253 new_exc = exceptions.RefreshError("No access token in response.", response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800254 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700255
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700256 refresh_token = response_data.get("refresh_token", refresh_token)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700257 expiry = _parse_expiry(response_data)
258
259 return access_token, refresh_token, expiry, response_data