blob: 5121a32746515ea8801c9c4427bfbb74f4a0c4b2 [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
105 response = request(
106 method='POST', url=token_uri, headers=headers, body=body)
107
108 response_body = response.data.decode('utf-8')
109
110 if response.status != http_client.OK:
111 _handle_error_response(response_body)
112
113 response_data = json.loads(response_body)
114
115 return response_data
116
117
118def jwt_grant(request, token_uri, assertion):
119 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
120
121 For more details, see `rfc7523 section 4`_.
122
123 Args:
124 request (google.auth.transport.Request): A callable used to make
125 HTTP requests.
126 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
127 URI.
128 assertion (str): The OAuth 2.0 assertion.
129
130 Returns:
131 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
132 expiration, and additional data returned by the token endpoint.
133
134 Raises:
135 google.auth.exceptions.RefreshError: If the token endpoint returned
136 an error.
137
138 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
139 """
140 body = {
141 'assertion': assertion,
142 'grant_type': _JWT_GRANT_TYPE,
143 }
144
145 response_data = _token_endpoint_request(request, token_uri, body)
146
147 try:
148 access_token = response_data['access_token']
Danny Hermes895e3692017-11-09 11:35:57 -0800149 except KeyError as caught_exc:
150 new_exc = exceptions.RefreshError(
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700151 '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 """
184 body = {
185 'assertion': assertion,
186 'grant_type': _JWT_GRANT_TYPE,
187 }
188
189 response_data = _token_endpoint_request(request, token_uri, body)
190
191 try:
192 id_token = response_data['id_token']
193 except KeyError as caught_exc:
194 new_exc = exceptions.RefreshError(
195 'No ID token in response.', response_data)
196 six.raise_from(new_exc, caught_exc)
197
198 payload = jwt.decode(id_token, verify=False)
199 expiry = datetime.datetime.utcfromtimestamp(payload['exp'])
200
201 return id_token, expiry, response_data
202
203
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400204def refresh_grant(request, token_uri, refresh_token, client_id, client_secret,
205 scopes=None):
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 = {
236 'grant_type': _REFRESH_GRANT_TYPE,
237 'client_id': client_id,
238 'client_secret': client_secret,
239 'refresh_token': refresh_token,
240 }
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400241 if scopes:
242 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:
247 access_token = response_data['access_token']
Danny Hermes895e3692017-11-09 11:35:57 -0800248 except KeyError as caught_exc:
249 new_exc = exceptions.RefreshError(
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700250 'No access token in response.', response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800251 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700252
253 refresh_token = response_data.get('refresh_token', refresh_token)
254 expiry = _parse_expiry(response_data)
255
256 return access_token, refresh_token, expiry, response_data