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.oauth2 import _client39from google.oauth2 import _client_async40from google.oauth2 import challenges41from google.oauth2 import reauth42 43 44async def _get_challenges(45 request, supported_challenge_types, access_token, requested_scopes=None46):47 """Does initial request to reauth API to get the challenges.48 49 Args:50 request (google.auth.transport.Request): A callable used to make51 HTTP requests. This must be an aiohttp request.52 supported_challenge_types (Sequence[str]): list of challenge names53 supported by the manager.54 access_token (str): Access token with reauth scopes.55 requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.56 57 Returns:58 dict: The response from the reauth API.59 """60 body = {"supportedChallengeTypes": supported_challenge_types}61 if requested_scopes:62 body["oauthScopesForDomainPolicyLookup"] = requested_scopes63 64 return await _client_async._token_endpoint_request(65 request,66 reauth._REAUTH_API + ":start",67 body,68 access_token=access_token,69 use_json=True,70 )71 72 73async def _send_challenge_result(74 request, session_id, challenge_id, client_input, access_token75):76 """Attempt to refresh access token by sending next challenge result.77 78 Args:79 request (google.auth.transport.Request): A callable used to make80 HTTP requests. This must be an aiohttp request.81 session_id (str): session id returned by the initial reauth call.82 challenge_id (str): challenge id returned by the initial reauth call.83 client_input: dict with a challenge-specific client input. For example:84 ``{'credential': password}`` for password challenge.85 access_token (str): Access token with reauth scopes.86 87 Returns:88 dict: The response from the reauth API.89 """90 body = {91 "sessionId": session_id,92 "challengeId": challenge_id,93 "action": "RESPOND",94 "proposalResponse": client_input,95 }96 97 return await _client_async._token_endpoint_request(98 request,99 reauth._REAUTH_API + "/{}:continue".format(session_id),100 body,101 access_token=access_token,102 use_json=True,103 )104 105 106async def _run_next_challenge(msg, request, access_token):107 """Get the next challenge from msg and run it.108 109 Args:110 msg (dict): Reauth API response body (either from the initial request to111 https://reauth.googleapis.com/v2/sessions:start or from sending the112 previous challenge response to113 https://reauth.googleapis.com/v2/sessions/id:continue)114 request (google.auth.transport.Request): A callable used to make115 HTTP requests. This must be an aiohttp request.116 access_token (str): reauth access token117 118 Returns:119 dict: The response from the reauth API.120 121 Raises:122 google.auth.exceptions.ReauthError: if reauth failed.123 """124 for challenge in msg["challenges"]:125 if challenge["status"] != "READY":126 # Skip non-activated challenges.127 continue128 c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)129 if not c:130 raise exceptions.ReauthFailError(131 "Unsupported challenge type {0}. Supported types: {1}".format(132 challenge["challengeType"],133 ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),134 )135 )136 if not c.is_locally_eligible:137 raise exceptions.ReauthFailError(138 "Challenge {0} is not locally eligible".format(139 challenge["challengeType"]140 )141 )142 client_input = c.obtain_challenge_input(challenge)143 if not client_input:144 return None145 return await _send_challenge_result(146 request,147 msg["sessionId"],148 challenge["challengeId"],149 client_input,150 access_token,151 )152 return None153 154 155async def _obtain_rapt(request, access_token, requested_scopes):156 """Given an http request method and reauth access token, get rapt token.157 158 Args:159 request (google.auth.transport.Request): A callable used to make160 HTTP requests. This must be an aiohttp request.161 access_token (str): reauth access token162 requested_scopes (Sequence[str]): scopes required by the client application163 164 Returns:165 str: The rapt token.166 167 Raises:168 google.auth.exceptions.ReauthError: if reauth failed169 """170 msg = await _get_challenges(171 request,172 list(challenges.AVAILABLE_CHALLENGES.keys()),173 access_token,174 requested_scopes,175 )176 177 if msg["status"] == reauth._AUTHENTICATED:178 return msg["encodedProofOfReauthToken"]179 180 for _ in range(0, reauth.RUN_CHALLENGE_RETRY_LIMIT):181 if not (182 msg["status"] == reauth._CHALLENGE_REQUIRED183 or msg["status"] == reauth._CHALLENGE_PENDING184 ):185 raise exceptions.ReauthFailError(186 "Reauthentication challenge failed due to API error: {}".format(187 msg["status"]188 )189 )190 191 if not reauth.is_interactive():192 raise exceptions.ReauthFailError(193 "Reauthentication challenge could not be answered because you are not"194 " in an interactive session."195 )196 197 msg = await _run_next_challenge(msg, request, access_token)198 199 if msg["status"] == reauth._AUTHENTICATED:200 return msg["encodedProofOfReauthToken"]201 202 # If we got here it means we didn't get authenticated.203 raise exceptions.ReauthFailError("Failed to obtain rapt token.")204 205 206async def get_rapt_token(207 request, client_id, client_secret, refresh_token, token_uri, scopes=None208):209 """Given an http request method and refresh_token, get rapt token.210 211 Args:212 request (google.auth.transport.Request): A callable used to make213 HTTP requests. This must be an aiohttp request.214 client_id (str): client id to get access token for reauth scope.215 client_secret (str): client secret for the client_id216 refresh_token (str): refresh token to refresh access token217 token_uri (str): uri to refresh access token218 scopes (Optional(Sequence[str])): scopes required by the client application219 220 Returns:221 str: The rapt token.222 Raises:223 google.auth.exceptions.RefreshError: If reauth failed.224 """225 sys.stderr.write("Reauthentication required.\n")226 227 # Get access token for reauth.228 access_token, _, _, _ = await _client_async.refresh_grant(229 request=request,230 client_id=client_id,231 client_secret=client_secret,232 refresh_token=refresh_token,233 token_uri=token_uri,234 scopes=[reauth._REAUTH_SCOPE],235 )236 237 # Get rapt token from reauth API.238 rapt_token = await _obtain_rapt(request, access_token, requested_scopes=scopes)239 240 return rapt_token241 242 243async def refresh_grant(244 request,245 token_uri,246 refresh_token,247 client_id,248 client_secret,249 scopes=None,250 rapt_token=None,251 enable_reauth_refresh=False,252):253 """Implements the reauthentication flow.254 255 Args:256 request (google.auth.transport.Request): A callable used to make257 HTTP requests. This must be an aiohttp request.258 token_uri (str): The OAuth 2.0 authorizations server's token endpoint259 URI.260 refresh_token (str): The refresh token to use to get a new access261 token.262 client_id (str): The OAuth 2.0 application's client ID.263 client_secret (str): The Oauth 2.0 appliaction's client secret.264 scopes (Optional(Sequence[str])): Scopes to request. If present, all265 scopes must be authorized for the refresh token. Useful if refresh266 token has a wild card scope (e.g.267 'https://www.googleapis.com/auth/any-api').268 rapt_token (Optional(str)): The rapt token for reauth.269 enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow270 should be used. The default value is False. This option is for271 gcloud only, other users should use the default value.272 273 Returns:274 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The275 access token, new refresh token, expiration, the additional data276 returned by the token endpoint, and the rapt token.277 278 Raises:279 google.auth.exceptions.RefreshError: If the token endpoint returned280 an error.281 """282 body = {283 "grant_type": _client._REFRESH_GRANT_TYPE,284 "client_id": client_id,285 "client_secret": client_secret,286 "refresh_token": refresh_token,287 }288 if scopes:289 body["scope"] = " ".join(scopes)290 if rapt_token:291 body["rapt"] = rapt_token292 293 response_status_ok, response_data, retryable_error = await _client_async._token_endpoint_request_no_throw(294 request, token_uri, body295 )296 if (297 not response_status_ok298 and response_data.get("error") == reauth._REAUTH_NEEDED_ERROR299 and (300 response_data.get("error_subtype")301 == reauth._REAUTH_NEEDED_ERROR_INVALID_RAPT302 or response_data.get("error_subtype")303 == reauth._REAUTH_NEEDED_ERROR_RAPT_REQUIRED304 )305 ):306 if not enable_reauth_refresh:307 raise exceptions.RefreshError(308 "Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."309 )310 311 rapt_token = await get_rapt_token(312 request, client_id, client_secret, refresh_token, token_uri, scopes=scopes313 )314 body["rapt"] = rapt_token315 (316 response_status_ok,317 response_data,318 retryable_error,319 ) = await _client_async._token_endpoint_request_no_throw(320 request, token_uri, body321 )322 323 if not response_status_ok:324 _client._handle_error_response(response_data, retryable_error)325 refresh_response = _client._handle_refresh_grant_response(326 response_data, refresh_token327 )328 return refresh_response + (rapt_token,)329 