1# Copyright 2020 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 async 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 exceptions 34from google.auth import jwt 35from google.oauth2 import _client as client 36 37 38async def _token_endpoint_request_no_throw( 39 request, token_uri, body, access_token=None, use_json=False 40): 41 """Makes a request to the OAuth 2.0 authorization server's token endpoint. 42 This function doesn't throw on response errors. 43 44 Args: 45 request (google.auth.transport.Request): A callable used to make 46 HTTP requests. 47 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 48 URI. 49 body (Mapping[str, str]): The parameters to send in the request body. 50 access_token (Optional(str)): The access token needed to make the request. 51 use_json (Optional(bool)): Use urlencoded format or json format for the 52 content type. The default value is False. 53 54 Returns: 55 Tuple(bool, Mapping[str, str]): A boolean indicating if the request is 56 successful, and a mapping for the JSON-decoded response data. 57 """ 58 if use_json: 59 headers = {"Content-Type": client._JSON_CONTENT_TYPE} 60 body = json.dumps(body).encode("utf-8") 61 else: 62 headers = {"Content-Type": client._URLENCODED_CONTENT_TYPE} 63 body = urllib.parse.urlencode(body).encode("utf-8") 64 65 if access_token: 66 headers["Authorization"] = "Bearer {}".format(access_token) 67 68 retry = 0 69 # retry to fetch token for maximum of two times if any internal failure 70 # occurs. 71 while True: 72 73 response = await request( 74 method="POST", url=token_uri, headers=headers, body=body 75 ) 76 77 # Using data.read() resulted in zlib decompression errors. This may require future investigation. 78 response_body1 = await response.content() 79 80 response_body = ( 81 response_body1.decode("utf-8") 82 if hasattr(response_body1, "decode") 83 else response_body1 84 ) 85 86 response_data = json.loads(response_body) 87 88 if response.status == http_client.OK: 89 break 90 else: 91 error_desc = response_data.get("error_description") or "" 92 error_code = response_data.get("error") or "" 93 if ( 94 any(e == "internal_failure" for e in (error_code, error_desc)) 95 and retry < 1 96 ): 97 retry += 1 98 continue 99 return response.status == http_client.OK, response_data 100 101 return response.status == http_client.OK, response_data 102 103 104async def _token_endpoint_request( 105 request, token_uri, body, access_token=None, use_json=False 106): 107 """Makes a request to the OAuth 2.0 authorization server's token endpoint. 108 109 Args: 110 request (google.auth.transport.Request): A callable used to make 111 HTTP requests. 112 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 113 URI. 114 body (Mapping[str, str]): The parameters to send in the request body. 115 access_token (Optional(str)): The access token needed to make the request. 116 use_json (Optional(bool)): Use urlencoded format or json format for the 117 content type. The default value is False. 118 119 Returns: 120 Mapping[str, str]: The JSON-decoded response data. 121 122 Raises: 123 google.auth.exceptions.RefreshError: If the token endpoint returned 124 an error. 125 """ 126 response_status_ok, response_data = await _token_endpoint_request_no_throw( 127 request, token_uri, body, access_token=access_token, use_json=use_json 128 ) 129 if not response_status_ok: 130 client._handle_error_response(response_data) 131 return response_data 132 133 134async def jwt_grant(request, token_uri, assertion): 135 """Implements the JWT Profile for OAuth 2.0 Authorization Grants. 136 137 For more details, see `rfc7523 section 4`_. 138 139 Args: 140 request (google.auth.transport.Request): A callable used to make 141 HTTP requests. 142 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 143 URI. 144 assertion (str): The OAuth 2.0 assertion. 145 146 Returns: 147 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, 148 expiration, and additional data returned by the token endpoint. 149 150 Raises: 151 google.auth.exceptions.RefreshError: If the token endpoint returned 152 an error. 153 154 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4 155 """ 156 body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} 157 158 response_data = await _token_endpoint_request(request, token_uri, body) 159 160 try: 161 access_token = response_data["access_token"] 162 except KeyError as caught_exc: 163 new_exc = exceptions.RefreshError("No access token in response.", response_data) 164 six.raise_from(new_exc, caught_exc) 165 166 expiry = client._parse_expiry(response_data) 167 168 return access_token, expiry, response_data 169 170 171async def id_token_jwt_grant(request, token_uri, assertion): 172 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but 173 requests an OpenID Connect ID Token instead of an access token. 174 175 This is a variant on the standard JWT Profile that is currently unique 176 to Google. This was added for the benefit of authenticating to services 177 that require ID Tokens instead of access tokens or JWT bearer tokens. 178 179 Args: 180 request (google.auth.transport.Request): A callable used to make 181 HTTP requests. 182 token_uri (str): The OAuth 2.0 authorization server's token endpoint 183 URI. 184 assertion (str): JWT token signed by a service account. The token's 185 payload must include a ``target_audience`` claim. 186 187 Returns: 188 Tuple[str, Optional[datetime], Mapping[str, str]]: 189 The (encoded) Open ID Connect ID Token, expiration, and additional 190 data returned by the endpoint. 191 192 Raises: 193 google.auth.exceptions.RefreshError: If the token endpoint returned 194 an error. 195 """ 196 body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} 197 198 response_data = await _token_endpoint_request(request, token_uri, body) 199 200 try: 201 id_token = response_data["id_token"] 202 except KeyError as caught_exc: 203 new_exc = exceptions.RefreshError("No ID token in response.", response_data) 204 six.raise_from(new_exc, caught_exc) 205 206 payload = jwt.decode(id_token, verify=False) 207 expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) 208 209 return id_token, expiry, response_data 210 211 212async def refresh_grant( 213 request, 214 token_uri, 215 refresh_token, 216 client_id, 217 client_secret, 218 scopes=None, 219 rapt_token=None, 220): 221 """Implements the OAuth 2.0 refresh token grant. 222 223 For more details, see `rfc678 section 6`_. 224 225 Args: 226 request (google.auth.transport.Request): A callable used to make 227 HTTP requests. 228 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 229 URI. 230 refresh_token (str): The refresh token to use to get a new access 231 token. 232 client_id (str): The OAuth 2.0 application's client ID. 233 client_secret (str): The Oauth 2.0 appliaction's client secret. 234 scopes (Optional(Sequence[str])): Scopes to request. If present, all 235 scopes must be authorized for the refresh token. Useful if refresh 236 token has a wild card scope (e.g. 237 'https://www.googleapis.com/auth/any-api'). 238 rapt_token (Optional(str)): The reauth Proof Token. 239 240 Returns: 241 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The 242 access token, new or current refresh token, expiration, and additional data 243 returned by the token endpoint. 244 245 Raises: 246 google.auth.exceptions.RefreshError: If the token endpoint returned 247 an error. 248 249 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6 250 """ 251 body = { 252 "grant_type": client._REFRESH_GRANT_TYPE, 253 "client_id": client_id, 254 "client_secret": client_secret, 255 "refresh_token": refresh_token, 256 } 257 if scopes: 258 body["scope"] = " ".join(scopes) 259 if rapt_token: 260 body["rapt"] = rapt_token 261 262 response_data = await _token_endpoint_request(request, token_uri, body) 263 return client._handle_refresh_grant_response(response_data, refresh_token) 264