blob: eac01b7ecd9efe403fd16818af337f0217127e8b [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
37_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'
40
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)
Jon Wayne Parrottb6075072016-12-13 14:53:25 -080053 error_details = '{}: {}'.format(
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070054 error_data['error'],
Jon Wayne Parrottb6075072016-12-13 14:53:25 -080055 error_data.get('error_description'))
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
60 raise exceptions.RefreshError(
61 error_details, response_body)
62
63
64def _parse_expiry(response_data):
65 """Parses the expiry field from a response into a datetime.
66
67 Args:
68 response_data (Mapping): The JSON-parsed response data.
69
70 Returns:
71 Optional[datetime]: The expiration or ``None`` if no expiration was
72 specified.
73 """
74 expires_in = response_data.get('expires_in', None)
75
76 if expires_in is not None:
77 return _helpers.utcnow() + datetime.timedelta(
78 seconds=expires_in)
79 else:
80 return None
81
82
83def _token_endpoint_request(request, token_uri, body):
84 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
85
86 Args:
87 request (google.auth.transport.Request): A callable used to make
88 HTTP requests.
89 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
90 URI.
91 body (Mapping[str, str]): The parameters to send in the request body.
92
93 Returns:
94 Mapping[str, str]: The JSON-decoded response data.
95
96 Raises:
97 google.auth.exceptions.RefreshError: If the token endpoint returned
98 an error.
99 """
100 body = urllib.parse.urlencode(body)
101 headers = {
102 'content-type': _URLENCODED_CONTENT_TYPE,
103 }
104
Anjali Doneriaeae1dcb2019-09-09 16:36:10 -0700105 retry = 0
106 # retry to fetch token for maximum of two times if any internal failure
107 # occurs.
108 while True:
109 response = request(
110 method='POST', url=token_uri, headers=headers, body=body)
111 response_body = response.data.decode('utf-8')
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:
116 error_desc = json.loads(
117 response_body).get('error_description') or ''
118 if error_desc == 'internal_failure' and retry < 1:
119 retry += 1
120 continue
121 _handle_error_response(response_body)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700122
123 response_data = json.loads(response_body)
124
125 return response_data
126
127
128def jwt_grant(request, token_uri, assertion):
129 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
130
131 For more details, see `rfc7523 section 4`_.
132
133 Args:
134 request (google.auth.transport.Request): A callable used to make
135 HTTP requests.
136 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
137 URI.
138 assertion (str): The OAuth 2.0 assertion.
139
140 Returns:
141 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
142 expiration, and additional data returned by the token endpoint.
143
144 Raises:
145 google.auth.exceptions.RefreshError: If the token endpoint returned
146 an error.
147
148 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
149 """
150 body = {
151 'assertion': assertion,
152 'grant_type': _JWT_GRANT_TYPE,
153 }
154
155 response_data = _token_endpoint_request(request, token_uri, body)
156
157 try:
158 access_token = response_data['access_token']
Danny Hermes895e3692017-11-09 11:35:57 -0800159 except KeyError as caught_exc:
160 new_exc = exceptions.RefreshError(
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700161 'No access token in response.', response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800162 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700163
164 expiry = _parse_expiry(response_data)
165
166 return access_token, expiry, response_data
167
168
Christophe Tatonb649b432018-02-08 14:12:23 -0800169def id_token_jwt_grant(request, token_uri, assertion):
170 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
171 requests an OpenID Connect ID Token instead of an access token.
172
173 This is a variant on the standard JWT Profile that is currently unique
174 to Google. This was added for the benefit of authenticating to services
175 that require ID Tokens instead of access tokens or JWT bearer tokens.
176
177 Args:
178 request (google.auth.transport.Request): A callable used to make
179 HTTP requests.
180 token_uri (str): The OAuth 2.0 authorization server's token endpoint
181 URI.
182 assertion (str): JWT token signed by a service account. The token's
183 payload must include a ``target_audience`` claim.
184
185 Returns:
186 Tuple[str, Optional[datetime], Mapping[str, str]]:
187 The (encoded) Open ID Connect ID Token, expiration, and additional
188 data returned by the endpoint.
189
190 Raises:
191 google.auth.exceptions.RefreshError: If the token endpoint returned
192 an error.
193 """
194 body = {
195 'assertion': assertion,
196 'grant_type': _JWT_GRANT_TYPE,
197 }
198
199 response_data = _token_endpoint_request(request, token_uri, body)
200
201 try:
202 id_token = response_data['id_token']
203 except KeyError as caught_exc:
204 new_exc = exceptions.RefreshError(
205 'No ID token in response.', response_data)
206 six.raise_from(new_exc, caught_exc)
207
208 payload = jwt.decode(id_token, verify=False)
209 expiry = datetime.datetime.utcfromtimestamp(payload['exp'])
210
211 return id_token, expiry, response_data
212
213
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400214def refresh_grant(request, token_uri, refresh_token, client_id, client_secret,
215 scopes=None):
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700216 """Implements the OAuth 2.0 refresh token grant.
217
218 For more details, see `rfc678 section 6`_.
219
220 Args:
221 request (google.auth.transport.Request): A callable used to make
222 HTTP requests.
223 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
224 URI.
225 refresh_token (str): The refresh token to use to get a new access
226 token.
227 client_id (str): The OAuth 2.0 application's client ID.
228 client_secret (str): The Oauth 2.0 appliaction's client secret.
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400229 scopes (Optional(Sequence[str])): Scopes to request. If present, all
230 scopes must be authorized for the refresh token. Useful if refresh
231 token has a wild card scope (e.g.
232 'https://www.googleapis.com/auth/any-api').
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700233
234 Returns:
235 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
236 access token, new refresh token, expiration, and additional data
237 returned by the token endpoint.
238
239 Raises:
240 google.auth.exceptions.RefreshError: If the token endpoint returned
241 an error.
242
243 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
244 """
245 body = {
246 'grant_type': _REFRESH_GRANT_TYPE,
247 'client_id': client_id,
248 'client_secret': client_secret,
249 'refresh_token': refresh_token,
250 }
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400251 if scopes:
252 body['scope'] = ' '.join(scopes)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700253
254 response_data = _token_endpoint_request(request, token_uri, body)
255
256 try:
257 access_token = response_data['access_token']
Danny Hermes895e3692017-11-09 11:35:57 -0800258 except KeyError as caught_exc:
259 new_exc = exceptions.RefreshError(
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700260 'No access token in response.', response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800261 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700262
263 refresh_token = response_data.get('refresh_token', refresh_token)
264 expiry = _parse_expiry(response_data)
265
266 return access_token, refresh_token, expiry, response_data