blob: f92b0970ad69240618706ecd82b105e15d34d4ac [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")
Georgy Savva46bb58e2019-11-13 22:21:57 +0300107 response_data = json.loads(response_body)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700108
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700109 if response.status == http_client.OK:
110 break
111 else:
Georgy Savva46bb58e2019-11-13 22:21:57 +0300112 error_desc = response_data.get("error_description") or ""
113 error_code = response_data.get("error") or ""
114 if (
115 any(e == "internal_failure" for e in (error_code, error_desc))
116 and retry < 1
117 ):
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700118 retry += 1
119 continue
120 _handle_error_response(response_body)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700121
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700122 return response_data
123
124
125def jwt_grant(request, token_uri, assertion):
126 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
127
128 For more details, see `rfc7523 section 4`_.
129
130 Args:
131 request (google.auth.transport.Request): A callable used to make
132 HTTP requests.
133 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
134 URI.
135 assertion (str): The OAuth 2.0 assertion.
136
137 Returns:
138 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
139 expiration, and additional data returned by the token endpoint.
140
141 Raises:
142 google.auth.exceptions.RefreshError: If the token endpoint returned
143 an error.
144
145 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
146 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700147 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700148
149 response_data = _token_endpoint_request(request, token_uri, body)
150
151 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700152 access_token = response_data["access_token"]
Danny Hermes895e3692017-11-09 11:35:57 -0800153 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700154 new_exc = exceptions.RefreshError("No access token in response.", response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800155 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700156
157 expiry = _parse_expiry(response_data)
158
159 return access_token, expiry, response_data
160
161
Christophe Tatonb649b432018-02-08 14:12:23 -0800162def id_token_jwt_grant(request, token_uri, assertion):
163 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
164 requests an OpenID Connect ID Token instead of an access token.
165
166 This is a variant on the standard JWT Profile that is currently unique
167 to Google. This was added for the benefit of authenticating to services
168 that require ID Tokens instead of access tokens or JWT bearer tokens.
169
170 Args:
171 request (google.auth.transport.Request): A callable used to make
172 HTTP requests.
173 token_uri (str): The OAuth 2.0 authorization server's token endpoint
174 URI.
175 assertion (str): JWT token signed by a service account. The token's
176 payload must include a ``target_audience`` claim.
177
178 Returns:
179 Tuple[str, Optional[datetime], Mapping[str, str]]:
180 The (encoded) Open ID Connect ID Token, expiration, and additional
181 data returned by the endpoint.
182
183 Raises:
184 google.auth.exceptions.RefreshError: If the token endpoint returned
185 an error.
186 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700187 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
Christophe Tatonb649b432018-02-08 14:12:23 -0800188
189 response_data = _token_endpoint_request(request, token_uri, body)
190
191 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700192 id_token = response_data["id_token"]
Christophe Tatonb649b432018-02-08 14:12:23 -0800193 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700194 new_exc = exceptions.RefreshError("No ID token in response.", response_data)
Christophe Tatonb649b432018-02-08 14:12:23 -0800195 six.raise_from(new_exc, caught_exc)
196
197 payload = jwt.decode(id_token, verify=False)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700198 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
Christophe Tatonb649b432018-02-08 14:12:23 -0800199
200 return id_token, expiry, response_data
201
202
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700203def refresh_grant(
204 request, token_uri, refresh_token, client_id, client_secret, scopes=None
205):
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700206 """Implements the OAuth 2.0 refresh token grant.
207
208 For more details, see `rfc678 section 6`_.
209
210 Args:
211 request (google.auth.transport.Request): A callable used to make
212 HTTP requests.
213 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
214 URI.
215 refresh_token (str): The refresh token to use to get a new access
216 token.
217 client_id (str): The OAuth 2.0 application's client ID.
218 client_secret (str): The Oauth 2.0 appliaction's client secret.
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400219 scopes (Optional(Sequence[str])): Scopes to request. If present, all
220 scopes must be authorized for the refresh token. Useful if refresh
221 token has a wild card scope (e.g.
222 'https://www.googleapis.com/auth/any-api').
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700223
224 Returns:
225 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
226 access token, new refresh token, expiration, and additional data
227 returned by the token endpoint.
228
229 Raises:
230 google.auth.exceptions.RefreshError: If the token endpoint returned
231 an error.
232
233 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
234 """
235 body = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700236 "grant_type": _REFRESH_GRANT_TYPE,
237 "client_id": client_id,
238 "client_secret": client_secret,
239 "refresh_token": refresh_token,
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700240 }
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400241 if scopes:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700242 body["scope"] = " ".join(scopes)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700243
244 response_data = _token_endpoint_request(request, token_uri, body)
245
246 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700247 access_token = response_data["access_token"]
Danny Hermes895e3692017-11-09 11:35:57 -0800248 except KeyError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700249 new_exc = exceptions.RefreshError("No access token in response.", response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800250 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700251
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700252 refresh_token = response_data.get("refresh_token", refresh_token)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700253 expiry = _parse_expiry(response_data)
254
255 return access_token, refresh_token, expiry, response_data