codekingpro/portable-devtools
114k
1# Copyright 2021 Google LLC2#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 at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# limitations under the License.14 15"""A module that provides functions for handling rapt authentication.16 17Reauth is a process of obtaining additional authentication (such as password,18security token, etc.) while refreshing OAuth 2.0 credentials for a user.19 20Credentials that use the Reauth flow must have the reauth scope,21``https://www.googleapis.com/auth/accounts.reauth``.22 23This module provides a high-level function for executing the Reauth process,24:func:`refresh_grant`, and lower-level helpers for doing the individual25steps of the reauth process.26 27Those steps are:28 291. Obtaining a list of challenges from the reauth server.302. Running through each challenge and sending the result back to the reauth31 server.323. Refreshing the access token using the returned rapt token.33"""34 35import sys36 37from google.auth import exceptions38from google.auth import metrics39from google.oauth2 import _client40from google.oauth2 import challenges41 42 43_REAUTH_SCOPE = "https://www.googleapis.com/auth/accounts.reauth"44_REAUTH_API = "https://reauth.googleapis.com/v2/sessions"45 46_REAUTH_NEEDED_ERROR = "invalid_grant"47_REAUTH_NEEDED_ERROR_INVALID_RAPT = "invalid_rapt"48_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = "rapt_required"49 50_AUTHENTICATED = "AUTHENTICATED"51_CHALLENGE_REQUIRED = "CHALLENGE_REQUIRED"52_CHALLENGE_PENDING = "CHALLENGE_PENDING"53 54 55# Override this global variable to set custom max number of rounds of reauth56# challenges should be run.57RUN_CHALLENGE_RETRY_LIMIT = 558 59 60def is_interactive():61 """Check if we are in an interractive environment.62 63 Override this function with a different logic if you are using this library64 outside a CLI.65 66 If the rapt token needs refreshing, the user needs to answer the challenges.67 If the user is not in an interractive environment, the challenges can not68 be answered and we just wait for timeout for no reason.69 70 Returns:71 bool: True if is interactive environment, False otherwise.72 """73 74 return sys.stdin.isatty()75 76 77def _get_challenges(78 request, supported_challenge_types, access_token, requested_scopes=None79):80 """Does initial request to reauth API to get the challenges.81 82 Args:83 request (google.auth.transport.Request): A callable used to make84 HTTP requests.85 supported_challenge_types (Sequence[str]): list of challenge names86 supported by the manager.87 access_token (str): Access token with reauth scopes.88 requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.89 90 Returns:91 dict: The response from the reauth API.92 """93 body = {"supportedChallengeTypes": supported_challenge_types}94 if requested_scopes:95 body["oauthScopesForDomainPolicyLookup"] = requested_scopes96 metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_start()}97 98 return _client._token_endpoint_request(99 request,100 _REAUTH_API + ":start",101 body,102 access_token=access_token,103 use_json=True,104 headers=metrics_header,105 )106 107 108def _send_challenge_result(109 request, session_id, challenge_id, client_input, access_token110):111 """Attempt to refresh access token by sending next challenge result.112 113 Args:114 request (google.auth.transport.Request): A callable used to make115 HTTP requests.116 session_id (str): session id returned by the initial reauth call.117 challenge_id (str): challenge id returned by the initial reauth call.118 client_input: dict with a challenge-specific client input. For example:119 ``{'credential': password}`` for password challenge.120 access_token (str): Access token with reauth scopes.121 122 Returns:123 dict: The response from the reauth API.124 """125 body = {126 "sessionId": session_id,127 "challengeId": challenge_id,128 "action": "RESPOND",129 "proposalResponse": client_input,130 }131 metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_continue()}132 133 return _client._token_endpoint_request(134 request,135 _REAUTH_API + "/{}:continue".format(session_id),136 body,137 access_token=access_token,138 use_json=True,139 headers=metrics_header,140 )141 142 143def _run_next_challenge(msg, request, access_token):144 """Get the next challenge from msg and run it.145 146 Args:147 msg (dict): Reauth API response body (either from the initial request to148 https://reauth.googleapis.com/v2/sessions:start or from sending the149 previous challenge response to150 https://reauth.googleapis.com/v2/sessions/id:continue)151 request (google.auth.transport.Request): A callable used to make152 HTTP requests.153 access_token (str): reauth access token154 155 Returns:156 dict: The response from the reauth API.157 158 Raises:159 google.auth.exceptions.ReauthError: if reauth failed.160 """161 for challenge in msg["challenges"]:162 if challenge["status"] != "READY":163 # Skip non-activated challenges.164 continue165 c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)166 if not c:167 raise exceptions.ReauthFailError(168 "Unsupported challenge type {0}. Supported types: {1}".format(169 challenge["challengeType"],170 ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),171 )172 )173 if not c.is_locally_eligible:174 raise exceptions.ReauthFailError(175 "Challenge {0} is not locally eligible".format(176 challenge["challengeType"]177 )178 )179 client_input = c.obtain_challenge_input(challenge)180 if not client_input:181 return None182 return _send_challenge_result(183 request,184 msg["sessionId"],185 challenge["challengeId"],186 client_input,187 access_token,188 )189 return None190 191 192def _obtain_rapt(request, access_token, requested_scopes):193 """Given an http request method and reauth access token, get rapt token.194 195 Args:196 request (google.auth.transport.Request): A callable used to make197 HTTP requests.198 access_token (str): reauth access token199 requested_scopes (Sequence[str]): scopes required by the client application200 201 Returns:202 str: The rapt token.203 204 Raises:205 google.auth.exceptions.ReauthError: if reauth failed206 """207 msg = _get_challenges(208 request,209 list(challenges.AVAILABLE_CHALLENGES.keys()),210 access_token,211 requested_scopes,212 )213 214 if msg["status"] == _AUTHENTICATED:215 return msg["encodedProofOfReauthToken"]216 217 for _ in range(0, RUN_CHALLENGE_RETRY_LIMIT):218 if not (219 msg["status"] == _CHALLENGE_REQUIRED or msg["status"] == _CHALLENGE_PENDING220 ):221 raise exceptions.ReauthFailError(222 "Reauthentication challenge failed due to API error: {}".format(223 msg["status"]224 )225 )226 227 if not is_interactive():228 raise exceptions.ReauthFailError(229 "Reauthentication challenge could not be answered because you are not"230 " in an interactive session."231 )232 233 msg = _run_next_challenge(msg, request, access_token)234 235 if not msg:236 raise exceptions.ReauthFailError("Failed to obtain rapt token.")237 if msg["status"] == _AUTHENTICATED:238 return msg["encodedProofOfReauthToken"]239 240 # If we got here it means we didn't get authenticated.241 raise exceptions.ReauthFailError("Failed to obtain rapt token.")242 243 244def get_rapt_token(245 request, client_id, client_secret, refresh_token, token_uri, scopes=None246):247 """Given an http request method and refresh_token, get rapt token.248 249 Args:250 request (google.auth.transport.Request): A callable used to make251 HTTP requests.252 client_id (str): client id to get access token for reauth scope.253 client_secret (str): client secret for the client_id254 refresh_token (str): refresh token to refresh access token255 token_uri (str): uri to refresh access token256 scopes (Optional(Sequence[str])): scopes required by the client application257 258 Returns:259 str: The rapt token.260 Raises:261 google.auth.exceptions.RefreshError: If reauth failed.262 """263 sys.stderr.write("Reauthentication required.\n")264 265 # Get access token for reauth.266 access_token, _, _, _ = _client.refresh_grant(267 request=request,268 client_id=client_id,269 client_secret=client_secret,270 refresh_token=refresh_token,271 token_uri=token_uri,272 scopes=[_REAUTH_SCOPE],273 )274 275 # Get rapt token from reauth API.276 rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes)277 278 return rapt_token279 280 281def refresh_grant(282 request,283 token_uri,284 refresh_token,285 client_id,286 client_secret,287 scopes=None,288 rapt_token=None,289 enable_reauth_refresh=False,290):291 """Implements the reauthentication flow.292 293 Args:294 request (google.auth.transport.Request): A callable used to make295 HTTP requests.296 token_uri (str): The OAuth 2.0 authorizations server's token endpoint297 URI.298 refresh_token (str): The refresh token to use to get a new access299 token.300 client_id (str): The OAuth 2.0 application's client ID.301 client_secret (str): The Oauth 2.0 appliaction's client secret.302 scopes (Optional(Sequence[str])): Scopes to request. If present, all303 scopes must be authorized for the refresh token. Useful if refresh304 token has a wild card scope (e.g.305 'https://www.googleapis.com/auth/any-api').306 rapt_token (Optional(str)): The rapt token for reauth.307 enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow308 should be used. The default value is False. This option is for309 gcloud only, other users should use the default value.310 311 Returns:312 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The313 access token, new refresh token, expiration, the additional data314 returned by the token endpoint, and the rapt token.315 316 Raises:317 google.auth.exceptions.RefreshError: If the token endpoint returned318 an error.319 """320 body = {321 "grant_type": _client._REFRESH_GRANT_TYPE,322 "client_id": client_id,323 "client_secret": client_secret,324 "refresh_token": refresh_token,325 }326 if scopes:327 body["scope"] = " ".join(scopes)328 if rapt_token:329 body["rapt"] = rapt_token330 metrics_header = {metrics.API_CLIENT_HEADER: metrics.token_request_user()}331 332 response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw(333 request, token_uri, body, headers=metrics_header334 )335 336 if not response_status_ok and isinstance(response_data, str):337 raise exceptions.RefreshError(response_data, retryable=False)338 339 if (340 not response_status_ok341 and response_data.get("error") == _REAUTH_NEEDED_ERROR342 and (343 response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_INVALID_RAPT344 or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED345 )346 ):347 if not enable_reauth_refresh:348 raise exceptions.RefreshError(349 "Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."350 )351 352 rapt_token = get_rapt_token(353 request, client_id, client_secret, refresh_token, token_uri, scopes=scopes354 )355 body["rapt"] = rapt_token356 (357 response_status_ok,358 response_data,359 retryable_error,360 ) = _client._token_endpoint_request_no_throw(361 request, token_uri, body, headers=metrics_header362 )363 364 if not response_status_ok:365 _client._handle_error_response(response_data, retryable_error)366 return _client._handle_refresh_grant_response(response_data, refresh_token) + (367 rapt_token,368 )369 