blob: 996f9b7e2961240136624ebc328bb49442e4a715 [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)
106 response_body = response.data.decode("utf-8")
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700107
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700108 if response.status == http_client.OK:
109 break
110 else:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700111 error_desc = json.loads(response_body).get("error_description") or ""
112 if error_desc == "internal_failure" and retry < 1:
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700113 retry += 1
114 continue
115 _handle_error_response(response_body)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700116
117 response_data = json.loads(response_body)
118
119 return response_data
120
121
122def jwt_grant(request, token_uri, assertion):
123 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
124
125 For more details, see `rfc7523 section 4`_.
126
127 Args:
128 request (google.auth.transport.Request): A callable used to make
129 HTTP requests.
130 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
131 URI.
132 assertion (str): The OAuth 2.0 assertion.
133
134 Returns:
135 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
136 expiration, and additional data returned by the token endpoint.
137
138 Raises:
139 google.auth.exceptions.RefreshError: If the token endpoint returned
140 an error.
141
142 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
143 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700144 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700145
146 response_data = _token_endpoint_request(request, token_uri, body)
147
148 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700149 access_token = response_data["access_token"]
Danny Hermes895e3692017-11-09 11:35:57 -0800150 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700151 new_exc = exceptions.RefreshError("No access token in response.", response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800152 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700153
154 expiry = _parse_expiry(response_data)
155
156 return access_token, expiry, response_data
157
158
Christophe Tatonb649b432018-02-08 14:12:23 -0800159def id_token_jwt_grant(request, token_uri, assertion):
160 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
161 requests an OpenID Connect ID Token instead of an access token.
162
163 This is a variant on the standard JWT Profile that is currently unique
164 to Google. This was added for the benefit of authenticating to services
165 that require ID Tokens instead of access tokens or JWT bearer tokens.
166
167 Args:
168 request (google.auth.transport.Request): A callable used to make
169 HTTP requests.
170 token_uri (str): The OAuth 2.0 authorization server's token endpoint
171 URI.
172 assertion (str): JWT token signed by a service account. The token's
173 payload must include a ``target_audience`` claim.
174
175 Returns:
176 Tuple[str, Optional[datetime], Mapping[str, str]]:
177 The (encoded) Open ID Connect ID Token, expiration, and additional
178 data returned by the endpoint.
179
180 Raises:
181 google.auth.exceptions.RefreshError: If the token endpoint returned
182 an error.
183 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700184 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Christophe Tatonb649b432018-02-08 14:12:23 -0800185
186 response_data = _token_endpoint_request(request, token_uri, body)
187
188 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700189 id_token = response_data["id_token"]
Christophe Tatonb649b432018-02-08 14:12:23 -0800190 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700191 new_exc = exceptions.RefreshError("No ID token in response.", response_data)
Christophe Tatonb649b432018-02-08 14:12:23 -0800192 six.raise_from(new_exc, caught_exc)
193
194 payload = jwt.decode(id_token, verify=False)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700195 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
Christophe Tatonb649b432018-02-08 14:12:23 -0800196
197 return id_token, expiry, response_data
198
199
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700200def refresh_grant(
201 request, token_uri, refresh_token, client_id, client_secret, scopes=None
202):
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700203 """Implements the OAuth 2.0 refresh token grant.
204
205 For more details, see `rfc678 section 6`_.
206
207 Args:
208 request (google.auth.transport.Request): A callable used to make
209 HTTP requests.
210 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
211 URI.
212 refresh_token (str): The refresh token to use to get a new access
213 token.
214 client_id (str): The OAuth 2.0 application's client ID.
215 client_secret (str): The Oauth 2.0 appliaction's client secret.
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400216 scopes (Optional(Sequence[str])): Scopes to request. If present, all
217 scopes must be authorized for the refresh token. Useful if refresh
218 token has a wild card scope (e.g.
219 'https://www.googleapis.com/auth/any-api').
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700220
221 Returns:
222 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
223 access token, new refresh token, expiration, and additional data
224 returned by the token endpoint.
225
226 Raises:
227 google.auth.exceptions.RefreshError: If the token endpoint returned
228 an error.
229
230 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
231 """
232 body = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700233 "grant_type": _REFRESH_GRANT_TYPE,
234 "client_id": client_id,
235 "client_secret": client_secret,
236 "refresh_token": refresh_token,
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700237 }
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400238 if scopes:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700239 body["scope"] = " ".join(scopes)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700240
241 response_data = _token_endpoint_request(request, token_uri, body)
242
243 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700244 access_token = response_data["access_token"]
Danny Hermes895e3692017-11-09 11:35:57 -0800245 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700246 new_exc = exceptions.RefreshError("No access token in response.", response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800247 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700248
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700249 refresh_token = response_data.get("refresh_token", refresh_token)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700250 expiry = _parse_expiry(response_data)
251
252 return access_token, refresh_token, expiry, response_data