1# Copyright 2016 Google LLC 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 29import six 30from six.moves import http_client 31from six.moves import urllib 32 33from google.auth import _helpers 34from google.auth import exceptions 35from google.auth import jwt 36 37_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded" 38_JSON_CONTENT_TYPE = "application/json" 39_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer" 40_REFRESH_GRANT_TYPE = "refresh_token" 41 42 43def _handle_error_response(response_data): 44 """Translates an error response into an exception. 45 46 Args: 47 response_data (Mapping): The decoded response data. 48 49 Raises: 50 google.auth.exceptions.RefreshError: The errors contained in response_data. 51 """ 52 try: 53 error_details = "{}: {}".format( 54 response_data["error"], response_data.get("error_description") 55 ) 56 # If no details could be extracted, use the response data. 57 except (KeyError, ValueError): 58 error_details = json.dumps(response_data) 59 60 raise exceptions.RefreshError(error_details, response_data) 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(seconds=expires_in) 77 else: 78 return None 79 80 81def _token_endpoint_request_no_throw( 82 request, token_uri, body, access_token=None, use_json=False 83): 84 """Makes a request to the OAuth 2.0 authorization server's token endpoint. 85 This function doesn't throw on response errors. 86 87 Args: 88 request (google.auth.transport.Request): A callable used to make 89 HTTP requests. 90 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 91 URI. 92 body (Mapping[str, str]): The parameters to send in the request body. 93 access_token (Optional(str)): The access token needed to make the request. 94 use_json (Optional(bool)): Use urlencoded format or json format for the 95 content type. The default value is False. 96 97 Returns: 98 Tuple(bool, Mapping[str, str]): A boolean indicating if the request is 99 successful, and a mapping for the JSON-decoded response data. 100 """ 101 if use_json: 102 headers = {"Content-Type": _JSON_CONTENT_TYPE} 103 body = json.dumps(body).encode("utf-8") 104 else: 105 headers = {"Content-Type": _URLENCODED_CONTENT_TYPE} 106 body = urllib.parse.urlencode(body).encode("utf-8") 107 108 if access_token: 109 headers["Authorization"] = "Bearer {}".format(access_token) 110 111 retry = 0 112 # retry to fetch token for maximum of two times if any internal failure 113 # occurs. 114 while True: 115 response = request(method="POST", url=token_uri, headers=headers, body=body) 116 response_body = ( 117 response.data.decode("utf-8") 118 if hasattr(response.data, "decode") 119 else response.data 120 ) 121 response_data = json.loads(response_body) 122 123 if response.status == http_client.OK: 124 break 125 else: 126 error_desc = response_data.get("error_description") or "" 127 error_code = response_data.get("error") or "" 128 if ( 129 any(e == "internal_failure" for e in (error_code, error_desc)) 130 and retry < 1 131 ): 132 retry += 1 133 continue 134 return response.status == http_client.OK, response_data 135 136 return response.status == http_client.OK, response_data 137 138 139def _token_endpoint_request( 140 request, token_uri, body, access_token=None, use_json=False 141): 142 """Makes a request to the OAuth 2.0 authorization server's token endpoint. 143 144 Args: 145 request (google.auth.transport.Request): A callable used to make 146 HTTP requests. 147 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 148 URI. 149 body (Mapping[str, str]): The parameters to send in the request body. 150 access_token (Optional(str)): The access token needed to make the request. 151 use_json (Optional(bool)): Use urlencoded format or json format for the 152 content type. The default value is False. 153 154 Returns: 155 Mapping[str, str]: The JSON-decoded response data. 156 157 Raises: 158 google.auth.exceptions.RefreshError: If the token endpoint returned 159 an error. 160 """ 161 response_status_ok, response_data = _token_endpoint_request_no_throw( 162 request, token_uri, body, access_token=access_token, use_json=use_json 163 ) 164 if not response_status_ok: 165 _handle_error_response(response_data) 166 return response_data 167 168 169def jwt_grant(request, token_uri, assertion): 170 """Implements the JWT Profile for OAuth 2.0 Authorization Grants. 171 172 For more details, see `rfc7523 section 4`_. 173 174 Args: 175 request (google.auth.transport.Request): A callable used to make 176 HTTP requests. 177 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 178 URI. 179 assertion (str): The OAuth 2.0 assertion. 180 181 Returns: 182 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, 183 expiration, and additional data returned by the token endpoint. 184 185 Raises: 186 google.auth.exceptions.RefreshError: If the token endpoint returned 187 an error. 188 189 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4 190 """ 191 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} 192 193 response_data = _token_endpoint_request(request, token_uri, body) 194 195 try: 196 access_token = response_data["access_token"] 197 except KeyError as caught_exc: 198 new_exc = exceptions.RefreshError("No access token in response.", response_data) 199 six.raise_from(new_exc, caught_exc) 200 201 expiry = _parse_expiry(response_data) 202 203 return access_token, expiry, response_data 204 205 206def id_token_jwt_grant(request, token_uri, assertion): 207 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but 208 requests an OpenID Connect ID Token instead of an access token. 209 210 This is a variant on the standard JWT Profile that is currently unique 211 to Google. This was added for the benefit of authenticating to services 212 that require ID Tokens instead of access tokens or JWT bearer tokens. 213 214 Args: 215 request (google.auth.transport.Request): A callable used to make 216 HTTP requests. 217 token_uri (str): The OAuth 2.0 authorization server's token endpoint 218 URI. 219 assertion (str): JWT token signed by a service account. The token's 220 payload must include a ``target_audience`` claim. 221 222 Returns: 223 Tuple[str, Optional[datetime], Mapping[str, str]]: 224 The (encoded) Open ID Connect ID Token, expiration, and additional 225 data returned by the endpoint. 226 227 Raises: 228 google.auth.exceptions.RefreshError: If the token endpoint returned 229 an error. 230 """ 231 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} 232 233 response_data = _token_endpoint_request(request, token_uri, body) 234 235 try: 236 id_token = response_data["id_token"] 237 except KeyError as caught_exc: 238 new_exc = exceptions.RefreshError("No ID token in response.", response_data) 239 six.raise_from(new_exc, caught_exc) 240 241 payload = jwt.decode(id_token, verify=False) 242 expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) 243 244 return id_token, expiry, response_data 245 246 247def _handle_refresh_grant_response(response_data, refresh_token): 248 """Extract tokens from refresh grant response. 249 250 Args: 251 response_data (Mapping[str, str]): Refresh grant response data. 252 refresh_token (str): Current refresh token. 253 254 Returns: 255 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token, 256 refresh token, expiration, and additional data returned by the token 257 endpoint. If response_data doesn't have refresh token, then the current 258 refresh token will be returned. 259 260 Raises: 261 google.auth.exceptions.RefreshError: If the token endpoint returned 262 an error. 263 """ 264 try: 265 access_token = response_data["access_token"] 266 except KeyError as caught_exc: 267 new_exc = exceptions.RefreshError("No access token in response.", response_data) 268 six.raise_from(new_exc, caught_exc) 269 270 refresh_token = response_data.get("refresh_token", refresh_token) 271 expiry = _parse_expiry(response_data) 272 273 return access_token, refresh_token, expiry, response_data 274 275 276def refresh_grant( 277 request, 278 token_uri, 279 refresh_token, 280 client_id, 281 client_secret, 282 scopes=None, 283 rapt_token=None, 284): 285 """Implements the OAuth 2.0 refresh token grant. 286 287 For more details, see `rfc678 section 6`_. 288 289 Args: 290 request (google.auth.transport.Request): A callable used to make 291 HTTP requests. 292 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 293 URI. 294 refresh_token (str): The refresh token to use to get a new access 295 token. 296 client_id (str): The OAuth 2.0 application's client ID. 297 client_secret (str): The Oauth 2.0 appliaction's client secret. 298 scopes (Optional(Sequence[str])): Scopes to request. If present, all 299 scopes must be authorized for the refresh token. Useful if refresh 300 token has a wild card scope (e.g. 301 'https://www.googleapis.com/auth/any-api'). 302 rapt_token (Optional(str)): The reauth Proof Token. 303 304 Returns: 305 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access 306 token, new or current refresh token, expiration, and additional data 307 returned by the token endpoint. 308 309 Raises: 310 google.auth.exceptions.RefreshError: If the token endpoint returned 311 an error. 312 313 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6 314 """ 315 body = { 316 "grant_type": _REFRESH_GRANT_TYPE, 317 "client_id": client_id, 318 "client_secret": client_secret, 319 "refresh_token": refresh_token, 320 } 321 if scopes: 322 body["scope"] = " ".join(scopes) 323 if rapt_token: 324 body["rapt"] = rapt_token 325 326 response_data = _token_endpoint_request(request, token_uri, body) 327 return _handle_refresh_grant_response(response_data, refresh_token) 328