blob: 66251df4135db0f1fe93d775dcd4fc7efdf24227 [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
35
36_URLENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded'
37_JWT_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
38_REFRESH_GRANT_TYPE = 'refresh_token'
39
40
41def _handle_error_response(response_body):
42 """"Translates an error response into an exception.
43
44 Args:
45 response_body (str): The decoded response data.
46
47 Raises:
48 google.auth.exceptions.RefreshError
49 """
50 try:
51 error_data = json.loads(response_body)
Jon Wayne Parrottb6075072016-12-13 14:53:25 -080052 error_details = '{}: {}'.format(
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070053 error_data['error'],
Jon Wayne Parrottb6075072016-12-13 14:53:25 -080054 error_data.get('error_description'))
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -070055 # If no details could be extracted, use the response data.
56 except (KeyError, ValueError):
57 error_details = response_body
58
59 raise exceptions.RefreshError(
60 error_details, response_body)
61
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 """
73 expires_in = response_data.get('expires_in', None)
74
75 if expires_in is not None:
76 return _helpers.utcnow() + datetime.timedelta(
77 seconds=expires_in)
78 else:
79 return None
80
81
82def _token_endpoint_request(request, token_uri, body):
83 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
84
85 Args:
86 request (google.auth.transport.Request): A callable used to make
87 HTTP requests.
88 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
89 URI.
90 body (Mapping[str, str]): The parameters to send in the request body.
91
92 Returns:
93 Mapping[str, str]: The JSON-decoded response data.
94
95 Raises:
96 google.auth.exceptions.RefreshError: If the token endpoint returned
97 an error.
98 """
99 body = urllib.parse.urlencode(body)
100 headers = {
101 'content-type': _URLENCODED_CONTENT_TYPE,
102 }
103
104 response = request(
105 method='POST', url=token_uri, headers=headers, body=body)
106
107 response_body = response.data.decode('utf-8')
108
109 if response.status != http_client.OK:
110 _handle_error_response(response_body)
111
112 response_data = json.loads(response_body)
113
114 return response_data
115
116
117def jwt_grant(request, token_uri, assertion):
118 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
119
120 For more details, see `rfc7523 section 4`_.
121
122 Args:
123 request (google.auth.transport.Request): A callable used to make
124 HTTP requests.
125 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
126 URI.
127 assertion (str): The OAuth 2.0 assertion.
128
129 Returns:
130 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
131 expiration, and additional data returned by the token endpoint.
132
133 Raises:
134 google.auth.exceptions.RefreshError: If the token endpoint returned
135 an error.
136
137 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
138 """
139 body = {
140 'assertion': assertion,
141 'grant_type': _JWT_GRANT_TYPE,
142 }
143
144 response_data = _token_endpoint_request(request, token_uri, body)
145
146 try:
147 access_token = response_data['access_token']
Danny Hermes895e3692017-11-09 11:35:57 -0800148 except KeyError as caught_exc:
149 new_exc = exceptions.RefreshError(
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700150 'No access token in response.', response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800151 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700152
153 expiry = _parse_expiry(response_data)
154
155 return access_token, expiry, response_data
156
157
158def refresh_grant(request, token_uri, refresh_token, client_id, client_secret):
159 """Implements the OAuth 2.0 refresh token grant.
160
161 For more details, see `rfc678 section 6`_.
162
163 Args:
164 request (google.auth.transport.Request): A callable used to make
165 HTTP requests.
166 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
167 URI.
168 refresh_token (str): The refresh token to use to get a new access
169 token.
170 client_id (str): The OAuth 2.0 application's client ID.
171 client_secret (str): The Oauth 2.0 appliaction's client secret.
172
173 Returns:
174 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
175 access token, new refresh token, expiration, and additional data
176 returned by the token endpoint.
177
178 Raises:
179 google.auth.exceptions.RefreshError: If the token endpoint returned
180 an error.
181
182 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
183 """
184 body = {
185 'grant_type': _REFRESH_GRANT_TYPE,
186 'client_id': client_id,
187 'client_secret': client_secret,
188 'refresh_token': refresh_token,
189 }
190
191 response_data = _token_endpoint_request(request, token_uri, body)
192
193 try:
194 access_token = response_data['access_token']
Danny Hermes895e3692017-11-09 11:35:57 -0800195 except KeyError as caught_exc:
196 new_exc = exceptions.RefreshError(
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700197 'No access token in response.', response_data)
Danny Hermes895e3692017-11-09 11:35:57 -0800198 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott123a48b2016-10-07 15:32:49 -0700199
200 refresh_token = response_data.get('refresh_token', refresh_token)
201 expiry = _parse_expiry(response_data)
202
203 return access_token, refresh_token, expiry, response_data